1use std::sync::Arc;
15
16use uqa_core::{IndexStats, Payload, PostingEntry, PostingList};
17use uqa_storage::StorageBackendError;
18
19use crate::base::{require_finite_score, ExecutionContext, Operator, OperatorResult};
20
21pub struct SparseThresholdOperator {
22 pub source: Arc<dyn Operator>,
23 pub threshold: f64,
24}
25
26impl SparseThresholdOperator {
27 pub fn new(source: Arc<dyn Operator>, threshold: f64) -> Self {
28 Self { source, threshold }
29 }
30}
31
32impl Operator for SparseThresholdOperator {
33 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
34 if !self.threshold.is_finite() {
35 return Err(StorageBackendError::Other(
36 "sparse threshold must be finite".to_string(),
37 ));
38 }
39 let pl = self.source.execute(ctx)?;
40 for entry in pl.entries() {
41 require_finite_score(entry.payload.score, "sparse threshold")?;
42 }
43 let entries: Vec<PostingEntry> = pl
44 .entries()
45 .iter()
46 .filter_map(|e| {
47 let adjusted = e.payload.score - self.threshold;
48 if adjusted > 0.0 {
49 Some(PostingEntry::new(
50 e.doc_id,
51 Payload {
52 positions: e.payload.positions.clone(),
53 score: adjusted,
54 fields: e.payload.fields.clone(),
55 },
56 ))
57 } else {
58 None
59 }
60 })
61 .collect();
62 Ok(PostingList::from_sorted_unchecked(entries))
63 }
64
65 fn cost_estimate(&self, stats: &IndexStats) -> f64 {
66 self.source.cost_estimate(stats)
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 struct ConstOperator(Vec<PostingEntry>);
75
76 impl Operator for ConstOperator {
77 fn execute(&self, _ctx: &ExecutionContext) -> OperatorResult {
78 Ok(PostingList::from_sorted_unchecked(self.0.clone()))
79 }
80 }
81
82 #[test]
83 fn drops_entries_below_threshold_and_subtracts() {
84 let source = Arc::new(ConstOperator(vec![
85 PostingEntry::new(1, Payload::with_score(0.3)),
86 PostingEntry::new(2, Payload::with_score(0.7)),
87 PostingEntry::new(3, Payload::with_score(0.5)),
88 ])) as Arc<dyn Operator>;
89 let op = SparseThresholdOperator::new(source, 0.4);
90 let out = op.execute(&ExecutionContext::new()).unwrap();
91 let entries: Vec<(u64, f64)> = out
92 .entries()
93 .iter()
94 .map(|e| (e.doc_id, e.payload.score))
95 .collect();
96 assert_eq!(entries.len(), 2);
97 let pair_2 = entries.iter().find(|(id, _)| *id == 2).unwrap();
99 assert!((pair_2.1 - 0.3).abs() < 1e-9);
100 assert!(entries.iter().all(|(id, _)| *id != 1));
102 }
103}