Skip to main content

uqa_operators/
sparse.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Sparse threshold operator (Section 6.5, Paper 4).
8//!
9//! `SparseThresholdOperator` shifts every score by `-threshold` and
10//! drops entries whose adjusted score is non-positive. This realizes
11//! the MAP-estimation interpretation of `ReLU` activation: documents
12//! below the threshold have zero posterior under the sparse prior.
13
14use 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        // Doc 2: 0.7 - 0.4 = 0.3.
98        let pair_2 = entries.iter().find(|(id, _)| *id == 2).unwrap();
99        assert!((pair_2.1 - 0.3).abs() < 1e-9);
100        // Doc 1 is below threshold so dropped.
101        assert!(entries.iter().all(|(id, _)| *id != 1));
102    }
103}