use uqa_operators::{OperatorTree, TextTopKPlan, TextTopKStrategy};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextTopKCapabilities {
pub analyzed_term_count: usize,
pub indexed_document_count: u64,
}
#[must_use]
pub fn plan_text_top_k(
tree: OperatorTree,
k: usize,
capabilities: TextTopKCapabilities,
) -> OperatorTree {
let (query, field, scoring, top_k) = match tree {
OperatorTree::Term {
query,
field,
scoring,
top_k,
} => (query, field, scoring, top_k),
other => return other,
};
let eligible = field.is_some()
&& scoring.is_some()
&& top_k.is_none()
&& capabilities.analyzed_term_count >= 2
&& (k == 0 || (k as u128) < u128::from(capabilities.indexed_document_count));
if !eligible {
return OperatorTree::Term {
query,
field,
scoring,
top_k,
};
}
OperatorTree::Term {
query,
field,
scoring,
top_k: Some(TextTopKPlan {
k,
strategy: TextTopKStrategy::BlockMaxWand,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use uqa_operators::TextScoringMode;
fn term() -> OperatorTree {
OperatorTree::Term {
query: "rust search".into(),
field: Some("body".into()),
scoring: Some(TextScoringMode::BM25),
top_k: None,
}
}
#[test]
fn eligible_query_defers_block_validation_to_execution() {
let planned = plan_text_top_k(
term(),
10,
TextTopKCapabilities {
analyzed_term_count: 2,
indexed_document_count: 100,
},
);
assert!(matches!(
planned,
OperatorTree::Term {
top_k: Some(TextTopKPlan {
strategy: TextTopKStrategy::BlockMaxWand,
..
}),
..
}
));
}
#[test]
fn single_term_and_unbounded_inputs_stay_exhaustive() {
for (term_count, k, documents) in [(1, 10, 100), (2, 100, 100)] {
let planned = plan_text_top_k(
term(),
k,
TextTopKCapabilities {
analyzed_term_count: term_count,
indexed_document_count: documents,
},
);
assert!(matches!(planned, OperatorTree::Term { top_k: None, .. }));
}
}
}