162 lines
4.3 KiB
Rust
162 lines
4.3 KiB
Rust
//! M3.6.5 — Query Levels and Floor: Multi-tier query filtering
|
|||
|
|
//!
|
||
|
|
//! Allows queries to filter results by level (L0/L1/L2 vs R),
|
||
|
|
//! set floor thresholds, and control which tiers contribute to responses.
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
/// Query filtering parameters
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct QueryLevels {
|
||
|
|
/// Include L0/L1/L2 evidence? (default: true)
|
||
|
|
pub include_evidence: bool,
|
||
|
|
|
||
|
|
/// Include R (reference) docs? (default: true)
|
||
|
|
pub include_reference: bool,
|
||
|
|
|
||
|
|
/// Minimum relevance score (0.0-1.0) to include a result
|
||
|
|
pub floor: f32,
|
||
|
|
|
||
|
|
/// Specific levels to include (empty = all)
|
||
|
|
pub level_filter: Vec<String>, // ["L1", "L2"] or empty for all
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for QueryLevels {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
include_evidence: true,
|
||
|
|
include_reference: false, // R opt-in only
|
||
|
|
floor: 0.0,
|
||
|
|
level_filter: vec![],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl QueryLevels {
|
||
|
|
/// Create evidence-only filter (R excluded by default)
|
||
|
|
pub fn evidence_only() -> Self {
|
||
|
|
Self {
|
||
|
|
include_evidence: true,
|
||
|
|
include_reference: false,
|
||
|
|
floor: 0.0,
|
||
|
|
level_filter: vec![],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create reference-only filter
|
||
|
|
pub fn reference_only() -> Self {
|
||
|
|
Self {
|
||
|
|
include_evidence: false,
|
||
|
|
include_reference: true,
|
||
|
|
floor: 0.0,
|
||
|
|
level_filter: vec!["R".to_string()],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create hybrid (both evidence and reference)
|
||
|
|
pub fn hybrid(floor: f32) -> Self {
|
||
|
|
Self {
|
||
|
|
include_evidence: true,
|
||
|
|
include_reference: true,
|
||
|
|
floor,
|
||
|
|
level_filter: vec![],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if a result should be included
|
||
|
|
pub fn should_include(&self, level: &str, score: f32) -> bool {
|
||
|
|
// Check floor threshold first
|
||
|
|
if score < self.floor {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check level filter
|
||
|
|
if !self.level_filter.is_empty() {
|
||
|
|
if !self.level_filter.contains(&level.to_string()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check evidence/reference flags
|
||
|
|
if level == "R" {
|
||
|
|
return self.include_reference;
|
||
|
|
} else if level.starts_with("L") {
|
||
|
|
return self.include_evidence;
|
||
|
|
}
|
||
|
|
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_default_levels() {
|
||
|
|
let levels = QueryLevels::default();
|
||
|
|
assert!(levels.include_evidence);
|
||
|
|
assert!(!levels.include_reference);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_evidence_only_filter() {
|
||
|
|
let levels = QueryLevels::evidence_only();
|
||
|
|
assert!(levels.should_include("L1", 0.9));
|
||
|
|
assert!(levels.should_include("L2", 0.7));
|
||
|
|
assert!(!levels.should_include("R", 0.99));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_reference_only_filter() {
|
||
|
|
let levels = QueryLevels::reference_only();
|
||
|
|
assert!(!levels.should_include("L1", 0.9));
|
||
|
|
assert!(levels.should_include("R", 0.5));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_hybrid_filter() {
|
||
|
|
let levels = QueryLevels::hybrid(0.6);
|
||
|
|
assert!(levels.should_include("L1", 0.8));
|
||
|
|
assert!(levels.should_include("R", 0.7));
|
||
|
|
assert!(!levels.should_include("L0", 0.5)); // Below floor
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_floor_threshold() {
|
||
|
|
let levels = QueryLevels {
|
||
|
|
include_evidence: true,
|
||
|
|
include_reference: true,
|
||
|
|
floor: 0.75,
|
||
|
|
level_filter: vec![],
|
||
|
|
};
|
||
|
|
|
||
|
|
assert!(levels.should_include("L1", 0.80));
|
||
|
|
assert!(!levels.should_include("L1", 0.70));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_level_filter_specific() {
|
||
|
|
let levels = QueryLevels {
|
||
|
|
include_evidence: true,
|
||
|
|
include_reference: true,
|
||
|
|
floor: 0.0,
|
||
|
|
level_filter: vec!["L1".to_string(), "L2".to_string()],
|
||
|
|
};
|
||
|
|
|
||
|
|
assert!(levels.should_include("L1", 0.5));
|
||
|
|
assert!(levels.should_include("L2", 0.5));
|
||
|
|
assert!(!levels.should_include("L0", 0.9)); // Filtered out
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_r_opt_in() {
|
||
|
|
// R should require explicit opt-in
|
||
|
|
let default_levels = QueryLevels::default();
|
||
|
|
assert!(!default_levels.should_include("R", 1.0));
|
||
|
|
|
||
|
|
let with_r = QueryLevels::hybrid(0.0);
|
||
|
|
assert!(with_r.should_include("R", 1.0));
|
||
|
|
}
|
||
|
|
}
|