Skip to main content

qualia_core_db/solvers/fuzzy_query/
evaluate.rs

1//! The bridge from the crisp SPARQL executor to the fuzzy algebra.
2//!
3//! The engine evaluates a pattern and yields [`BindingRow`]s; these helpers attach a
4//! degree to each (from a fuzzy `FILTER` membership, a fuzzy-RDF triple degree, or a
5//! similarity score) and assemble a [`FuzzyResultSet`]. A multi-pattern (BGP) query is
6//! then the conjunctive **join** of the per-pattern fuzzy sets.
7
8use super::solution::{FuzzyResultSet, FuzzySolution};
9use super::DegreeNorm;
10use crate::sparql_ast::BindingRow;
11
12/// Annotate a slice of already-evaluated rows with a degree per row.
13pub fn annotate<F>(rows: &[BindingRow], degree_of: F) -> FuzzyResultSet
14where
15    F: Fn(&BindingRow) -> f64,
16{
17    FuzzyResultSet::from_solutions(
18        rows.iter()
19            .map(|r| FuzzySolution::new(*r, degree_of(r)))
20            .collect(),
21    )
22}
23
24/// Pull rows from a live engine operator and annotate them. `pull` is the engine's
25/// row-at-a-time step (typically a closure wrapping `PhysicalOperator::next(ctx, row)`):
26/// it fills the row and returns `true` while more solutions remain. This is the genuine
27/// integration shape — no copy of the executor, no `SparqlQueryContext` leak into this
28/// library. `cap` bounds the pull (fail-safe against an unbounded operator).
29pub fn collect_from<P, F>(mut pull: P, degree_of: F, cap: usize) -> FuzzyResultSet
30where
31    P: FnMut(&mut BindingRow) -> bool,
32    F: Fn(&BindingRow) -> f64,
33{
34    let mut out = Vec::new();
35    let mut row = BindingRow::new();
36    while out.len() < cap {
37        row.clear();
38        if !pull(&mut row) {
39            break;
40        }
41        out.push(FuzzySolution::new(row, degree_of(&row)));
42    }
43    FuzzyResultSet::from_solutions(out)
44}
45
46/// A conjunctive (basic graph pattern) query over per-pattern fuzzy result sets: join
47/// them all under `norm`, then apply the α-cut `threshold`. An empty pattern list is an
48/// empty result. This is the f-SPARQL `AND` of graded triple patterns.
49pub fn conjunctive_query(
50    pattern_sets: &[FuzzyResultSet],
51    norm: DegreeNorm,
52    threshold: f64,
53) -> FuzzyResultSet {
54    let mut it = pattern_sets.iter();
55    let mut acc = match it.next() {
56        Some(first) => first.clone(),
57        None => return FuzzyResultSet::new(),
58    };
59    for next in it {
60        acc = acc.join(next, norm);
61    }
62    acc.threshold(threshold)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::super::membership::approximately;
68    use super::*;
69    use crate::sparql_ast::{BindingRow, VariableId};
70
71    fn row(pairs: &[(VariableId, u64)]) -> BindingRow {
72        let mut r = BindingRow::new();
73        for &(v, val) in pairs {
74            r.set(v, val);
75        }
76        r
77    }
78
79    #[test]
80    fn annotate_with_a_fuzzy_filter() {
81        // Slot 1 holds an age; "≈ 30 ± 10" is the fuzzy FILTER.
82        let rows = [
83            row(&[(0, 100), (1, 30)]),
84            row(&[(0, 101), (1, 25)]),
85            row(&[(0, 102), (1, 50)]),
86        ];
87        let set = annotate(&rows, |r| {
88            approximately(r.get(1).unwrap() as f64, 30.0, 10.0)
89        });
90        let ranked = set.order_by_degree_desc();
91        assert_eq!(ranked.solutions[0].row.get(0), Some(100)); // exactly 30 → degree 1
92        assert!(ranked.solutions.last().unwrap().degree.abs() < 1e-9); // 50 → 0
93    }
94
95    #[test]
96    fn collect_from_a_simulated_operator() {
97        // A fake engine operator yielding three rows, then false.
98        let data = [row(&[(0, 1), (1, 30)]), row(&[(0, 2), (1, 28)])];
99        let mut i = 0;
100        let pull = |out: &mut BindingRow| {
101            if i < data.len() {
102                *out = data[i];
103                i += 1;
104                true
105            } else {
106                false
107            }
108        };
109        let set = collect_from(
110            pull,
111            |r| approximately(r.get(1).unwrap() as f64, 30.0, 5.0),
112            1000,
113        );
114        assert_eq!(set.len(), 2);
115    }
116
117    #[test]
118    fn collect_respects_cap() {
119        // An unbounded operator must be reined in by `cap`.
120        let pull = |out: &mut BindingRow| {
121            out.set(0, 1);
122            true
123        };
124        let set = collect_from(pull, |_| 1.0, 10);
125        assert_eq!(set.len(), 10);
126    }
127
128    #[test]
129    fn conjunctive_bgp_join_and_threshold() {
130        // Pattern A: ?x age≈30 ; Pattern B: ?x linked to ?y. Join on ?x, threshold.
131        let a = annotate(&[row(&[(0, 1), (1, 30)]), row(&[(0, 2), (1, 20)])], |r| {
132            approximately(r.get(1).unwrap() as f64, 30.0, 10.0)
133        });
134        let b = annotate(&[row(&[(0, 1), (2, 99)])], |_| 1.0);
135        let q = conjunctive_query(&[a, b], DegreeNorm::Godel, 0.5);
136        assert_eq!(q.len(), 1);
137        assert_eq!(q.solutions[0].row.get(2), Some(99));
138        assert!(q.solutions[0].degree >= 0.5);
139    }
140
141    #[test]
142    fn empty_patterns_is_empty() {
143        assert!(conjunctive_query(&[], DegreeNorm::Godel, 0.0).is_empty());
144    }
145}