Skip to main content

mongreldb_core/
query.rs

1//! Tool-call-native query surface.
2//!
3//! A [`Query`] is a conjunction of [`Condition`]s. Each condition resolves to a
4//! set of row ids in the shared [`crate::rowid::RowId`] space (PK exact, bitmap
5//! equality, ANN semantic, FM substring, or a column range). [`crate::Table`]
6//! intersects the sets and materializes the survivors — letting an agent express
7//! `semsearch ∩ fm_contains ∩ cat_in`, which no SQL FTS pipeline can.
8
9/// One predicate over the row-id space.
10#[derive(Debug, Clone)]
11pub enum Condition {
12    /// Primary-key exact match (encoded key bytes).
13    Pk(Vec<u8>),
14    /// Low-cardinality equality via the roaring bitmap index.
15    BitmapEq { column_id: u16, value: Vec<u8> },
16    /// Multi-value equality via the roaring bitmap index (Phase 13.5). Resolves
17    /// to the **union** of `bitmap[col].get(v)` for each value — the index-
18    /// accelerated equivalent of `col IN (v1, v2, …)` or a semi-join's runtime
19    /// value set.
20    BitmapIn {
21        column_id: u16,
22        values: Vec<Vec<u8>>,
23    },
24    /// Prefix match on a Bytes column with a bitmap index: all row-ids whose
25    /// indexed value starts with `prefix`. Exact (no residual needed) — the
26    /// bitmap's distinct keys are enumerated and filtered by prefix. Tighter
27    /// than `FmContains` for anchored `LIKE 'prefix%'`. (§5.6)
28    BytesPrefix { column_id: u16, prefix: Vec<u8> },
29    /// Semantic search via the binary-quantized ANN index.
30    Ann {
31        column_id: u16,
32        query: Vec<f32>,
33        k: usize,
34    },
35    /// Arbitrary substring via the FM index (no tokenization).
36    FmContains { column_id: u16, pattern: Vec<u8> },
37    /// Multi-segment FM intersection for `LIKE '%seg1%seg2%...'` (Priority 12).
38    /// Resolves to the **intersection** of FM lookups for each segment — a much
39    /// tighter superset than the single longest segment. DataFusion still
40    /// re-applies the real wildcard semantics (`Inexact` pushdown).
41    FmContainsAll {
42        column_id: u16,
43        patterns: Vec<Vec<u8>>,
44    },
45    /// Inclusive integer range (served by scanning the int column, later by the
46    /// learned PGM index / page-index pruning). Exclusive bounds (`>`,`<`) are
47    /// expressed exactly via ±1 in the translator.
48    Range { column_id: u16, lo: i64, hi: i64 },
49    /// Floating-point range with per-bound inclusivity (exact for `>`/`<`/`>=`/
50    /// `<=`/`BETWEEN`), served the same way as [`Condition::Range`].
51    RangeF64 {
52        column_id: u16,
53        lo: f64,
54        lo_inclusive: bool,
55        hi: f64,
56        hi_inclusive: bool,
57    },
58    /// SPLADE-style sparse retrieval: top-k row ids by sparse dot product over
59    /// shared tokens. `query` is a sparse vector `(token id → weight)`.
60    SparseMatch {
61        column_id: u16,
62        query: Vec<(u32, f32)>,
63        k: usize,
64    },
65    /// MinHash/LSH set-similarity: candidate row ids whose set is similar to
66    /// `query` (a set of 64-bit token hashes), ranked by estimated Jaccard,
67    /// truncated to `k`. Approximate (LSH recall) — the caller re-verifies.
68    MinHashSimilar {
69        column_id: u16,
70        query: Vec<u64>,
71        k: usize,
72    },
73    /// Rows where `column_id` is NULL. Resolved by decoding the column and
74    /// collecting null positions — a column scan, but no row materialization.
75    /// Page-stat aware: pages with `null_count == 0` are skipped.
76    IsNull { column_id: u16 },
77    /// Rows where `column_id` is NOT NULL. The complement of [`Self::IsNull`].
78    /// Page-stat aware: pages with `null_count == row_count` are skipped.
79    IsNotNull { column_id: u16 },
80}
81
82/// Ordered candidate generator. Unlike [`Condition`], retrieval preserves the
83/// index score and rank.
84#[derive(Debug, Clone)]
85pub enum Retriever {
86    Ann {
87        column_id: u16,
88        query: Vec<f32>,
89        k: usize,
90    },
91    Sparse {
92        column_id: u16,
93        query: Vec<(u32, f32)>,
94        k: usize,
95    },
96    MinHash {
97        column_id: u16,
98        members: Vec<SetMember>,
99        k: usize,
100    },
101}
102
103impl Retriever {
104    pub fn column_id(&self) -> u16 {
105        match self {
106            Self::Ann { column_id, .. }
107            | Self::Sparse { column_id, .. }
108            | Self::MinHash { column_id, .. } => *column_id,
109        }
110    }
111}
112
113pub fn encode_sparse_vector(terms: &[(u32, f32)]) -> crate::Result<Vec<u8>> {
114    Ok(bincode::serialize(terms)?)
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
118#[serde(untagged)]
119pub enum SetMember {
120    String(String),
121    Number(serde_json::Number),
122    Boolean(bool),
123}
124
125impl SetMember {
126    pub fn hash_v1(&self) -> u64 {
127        let value = match self {
128            Self::String(value) => serde_json::Value::String(value.clone()),
129            Self::Number(value) => serde_json::Value::Number(value.clone()),
130            Self::Boolean(value) => serde_json::Value::Bool(*value),
131        };
132        crate::index::minhash_member_hash_v1(&value).expect("SetMember is always scalar")
133    }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
137pub enum RetrieverScore {
138    AnnHammingDistance(u32),
139    SparseDotProduct(f32),
140    MinHashEstimatedJaccard(f32),
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum VectorMetric {
145    Cosine,
146    DotProduct,
147    Euclidean,
148}
149
150#[derive(Debug, Clone)]
151pub struct AnnRerankRequest {
152    pub column_id: u16,
153    pub query: Vec<f32>,
154    pub candidate_k: usize,
155    pub limit: usize,
156    pub metric: VectorMetric,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct AnnRerankHit {
161    pub row_id: crate::RowId,
162    pub hamming_distance: u32,
163    pub exact_score: f32,
164}
165
166#[derive(Debug, Clone, PartialEq)]
167pub struct RetrieverHit {
168    pub row_id: crate::RowId,
169    /// One-based rank.
170    pub rank: usize,
171    pub score: RetrieverScore,
172}
173
174#[derive(Debug, Clone)]
175pub struct SetSimilarityRequest {
176    pub column_id: u16,
177    pub members: Vec<SetMember>,
178    pub candidate_k: usize,
179    pub min_jaccard: f32,
180    pub limit: usize,
181}
182
183#[derive(Debug, Clone, PartialEq)]
184pub struct SetSimilarityHit {
185    pub row_id: crate::RowId,
186    pub estimated_jaccard: f32,
187    pub exact_jaccard: f32,
188}
189
190#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
191pub struct SetSimilarityTrace {
192    pub candidate_generation_us: u64,
193    pub gather_us: u64,
194    pub parse_us: u64,
195    pub score_us: u64,
196    pub candidate_count: usize,
197    pub verified_count: usize,
198}
199
200#[derive(Debug, Clone)]
201pub struct SearchRequest {
202    pub must: Vec<Condition>,
203    pub retrievers: Vec<NamedRetriever>,
204    pub fusion: Fusion,
205    pub limit: usize,
206    pub projection: Option<Vec<u16>>,
207}
208
209#[derive(Debug, Clone)]
210pub struct NamedRetriever {
211    pub name: String,
212    pub weight: f64,
213    pub retriever: Retriever,
214}
215
216#[derive(Debug, Clone, Copy)]
217pub enum Fusion {
218    ReciprocalRank { constant: u32 },
219}
220
221#[derive(Debug, Clone, PartialEq)]
222pub struct ComponentScore {
223    pub retriever_name: String,
224    pub rank: usize,
225    pub raw_score: RetrieverScore,
226    pub contribution: f64,
227}
228
229#[derive(Debug, Clone, PartialEq)]
230pub struct SearchHit {
231    pub row_id: crate::RowId,
232    pub cells: Vec<(u16, crate::Value)>,
233    pub components: Vec<ComponentScore>,
234    pub fused_score: f64,
235}
236
237/// A conjunctive query. Empty ⇒ all rows.
238#[derive(Debug, Default, Clone)]
239pub struct Query {
240    pub conditions: Vec<Condition>,
241}
242
243impl Query {
244    pub fn new() -> Self {
245        Self::default()
246    }
247    pub fn and(mut self, c: Condition) -> Self {
248        self.conditions.push(c);
249        self
250    }
251    pub fn pk(key: Vec<u8>) -> Self {
252        Self::new().and(Condition::Pk(key))
253    }
254}
255
256/// Canonical 64-bit cache key for a conjunctive native query + optional
257/// projection at `epoch` (Phase 19.1 / 19.6). Conditions are commutative (they
258/// are ANDed), so each condition is hashed into its own 64-bit digest, the
259/// digests are sorted, then folded together — two queries with the same
260/// semantics in a different order produce the same key. Within a condition,
261/// `BitmapIn` values are deduped+sorted and the `SparseMatch` query is sorted
262/// by token id. `epoch` is folded in so a `commit()` (which bumps it) orphans
263/// every prior entry without an explicit sweep.
264pub fn canonical_query_key(
265    conditions: &[Condition],
266    projection: Option<&[u16]>,
267    epoch: u64,
268) -> u64 {
269    canonical_query_key_with_version(QUERY_CACHE_KEY_VERSION, conditions, projection, epoch)
270}
271
272const QUERY_CACHE_KEY_VERSION: u64 = 2;
273
274fn canonical_query_key_with_version(
275    version: u64,
276    conditions: &[Condition],
277    projection: Option<&[u16]>,
278    epoch: u64,
279) -> u64 {
280    let fold = |seed: u64, b: u64| -> u64 { seed.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(b) };
281    let mut acc = fold(0xA5A5_A5A5_A5A5_A5A5, version);
282    acc = fold(acc, epoch);
283    // Order-independent: per-condition digests, sorted, then folded.
284    let mut digests: Vec<u64> = conditions.iter().map(hash_condition).collect();
285    digests.sort_unstable();
286    let n = digests.len() as u64;
287    acc = fold(acc, n);
288    for d in digests {
289        acc = fold(acc, d);
290    }
291    // Projection: sorted column ids (None ⇒ "all columns", distinct from any
292    // explicit projection incl. one listing every column, by intent).
293    match projection {
294        Some(p) => {
295            let mut p = p.to_vec();
296            p.sort_unstable();
297            p.dedup();
298            acc = fold(acc, 0x5E);
299            acc = fold(acc, p.len() as u64);
300            for id in p {
301                acc = fold(acc, id as u64);
302            }
303        }
304        None => {
305            acc = fold(acc, 0xA5);
306        }
307    }
308    acc
309}
310
311/// Hash a single condition into a 64-bit digest (order-independent w.r.t. its
312/// siblings; see [`canonical_query_key`]). Floats are hashed via `to_bits` for
313/// determinism.
314fn hash_condition(c: &Condition) -> u64 {
315    use std::hash::{Hash, Hasher};
316    let mut h = std::collections::hash_map::DefaultHasher::new();
317    match c {
318        Condition::Pk(k) => {
319            0u8.hash(&mut h);
320            k.hash(&mut h);
321        }
322        Condition::BitmapEq { column_id, value } => {
323            1u8.hash(&mut h);
324            column_id.hash(&mut h);
325            value.hash(&mut h);
326        }
327        Condition::BitmapIn { column_id, values } => {
328            2u8.hash(&mut h);
329            column_id.hash(&mut h);
330            let mut v: Vec<&Vec<u8>> = values.iter().collect();
331            v.sort();
332            v.dedup();
333            v.len().hash(&mut h);
334            for b in v {
335                b.hash(&mut h);
336            }
337        }
338        Condition::Ann {
339            column_id,
340            query,
341            k,
342        } => {
343            3u8.hash(&mut h);
344            column_id.hash(&mut h);
345            k.hash(&mut h);
346            for f in query {
347                f.to_bits().hash(&mut h);
348            }
349        }
350        Condition::FmContains { column_id, pattern } => {
351            4u8.hash(&mut h);
352            column_id.hash(&mut h);
353            pattern.hash(&mut h);
354        }
355        Condition::FmContainsAll {
356            column_id,
357            patterns,
358        } => {
359            5u8.hash(&mut h);
360            column_id.hash(&mut h);
361            let mut sorted: Vec<&[u8]> = patterns.iter().map(|p| p.as_slice()).collect();
362            sorted.sort();
363            sorted.dedup();
364            sorted.len().hash(&mut h);
365            for p in sorted {
366                p.hash(&mut h);
367            }
368        }
369        Condition::Range { column_id, lo, hi } => {
370            6u8.hash(&mut h);
371            column_id.hash(&mut h);
372            lo.hash(&mut h);
373            hi.hash(&mut h);
374        }
375        Condition::RangeF64 {
376            column_id,
377            lo,
378            lo_inclusive,
379            hi,
380            hi_inclusive,
381        } => {
382            7u8.hash(&mut h);
383            column_id.hash(&mut h);
384            lo.to_bits().hash(&mut h);
385            lo_inclusive.hash(&mut h);
386            hi.to_bits().hash(&mut h);
387            hi_inclusive.hash(&mut h);
388        }
389        Condition::SparseMatch {
390            column_id,
391            query,
392            k,
393        } => {
394            8u8.hash(&mut h);
395            column_id.hash(&mut h);
396            k.hash(&mut h);
397            let mut q: Vec<(u32, u32)> = query.iter().map(|(t, w)| (*t, w.to_bits())).collect();
398            q.sort_by_key(|(t, _)| *t);
399            for (t, wb) in q {
400                t.hash(&mut h);
401                wb.hash(&mut h);
402            }
403        }
404        Condition::MinHashSimilar {
405            column_id,
406            query,
407            k,
408        } => {
409            9u8.hash(&mut h);
410            column_id.hash(&mut h);
411            k.hash(&mut h);
412            let mut q = query.clone();
413            q.sort_unstable();
414            q.dedup();
415            for t in q {
416                t.hash(&mut h);
417            }
418        }
419        Condition::IsNull { column_id } => {
420            10u8.hash(&mut h);
421            column_id.hash(&mut h);
422        }
423        Condition::IsNotNull { column_id } => {
424            11u8.hash(&mut h);
425            column_id.hash(&mut h);
426        }
427        Condition::BytesPrefix { column_id, prefix } => {
428            12u8.hash(&mut h);
429            column_id.hash(&mut h);
430            prefix.hash(&mut h);
431        }
432    }
433    h.finish()
434}
435
436/// Extract the column IDs referenced by a slice of conditions (Phase 19.1
437/// hardening (c)). `Pk` references no user column (it's a row-id lookup) so it
438/// contributes nothing. Used for conservative column-based cache invalidation:
439/// a commit touching any of these columns may change the result.
440pub fn condition_columns(conditions: &[Condition]) -> Vec<u16> {
441    let mut cols: Vec<u16> = conditions
442        .iter()
443        .filter_map(|c| match c {
444            Condition::Pk(_) => None,
445            Condition::BitmapEq { column_id, .. }
446            | Condition::BitmapIn { column_id, .. }
447            | Condition::BytesPrefix { column_id, .. }
448            | Condition::Ann { column_id, .. }
449            | Condition::FmContains { column_id, .. }
450            | Condition::FmContainsAll { column_id, .. }
451            | Condition::Range { column_id, .. }
452            | Condition::RangeF64 { column_id, .. }
453            | Condition::SparseMatch { column_id, .. }
454            | Condition::MinHashSimilar { column_id, .. }
455            | Condition::IsNull { column_id }
456            | Condition::IsNotNull { column_id } => Some(*column_id),
457        })
458        .collect();
459    cols.sort_unstable();
460    cols.dedup();
461    cols
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn builder_chains() {
470        let q = Query::pk(b"k".to_vec()).and(Condition::Range {
471            column_id: 1,
472            lo: 0,
473            hi: 10,
474        });
475        assert_eq!(q.conditions.len(), 2);
476    }
477
478    /// Phase 19.6: order-independent canonicalization — the same conditions in a
479    /// different order, and a `BitmapIn` with shuffled/duplicate values, all
480    /// produce the same key.
481    #[test]
482    fn canonical_key_is_order_independent() {
483        let e = 7u64;
484        let a = Query::new()
485            .and(Condition::Range {
486                column_id: 1,
487                lo: 0,
488                hi: 10,
489            })
490            .and(Condition::BitmapEq {
491                column_id: 2,
492                value: b"x".to_vec(),
493            });
494        let b = Query::new()
495            .and(Condition::BitmapEq {
496                column_id: 2,
497                value: b"x".to_vec(),
498            })
499            .and(Condition::Range {
500                column_id: 1,
501                lo: 0,
502                hi: 10,
503            });
504        assert_eq!(
505            canonical_query_key(&a.conditions, None, e),
506            canonical_query_key(&b.conditions, None, e),
507            "condition order must not affect the key"
508        );
509
510        let minhash = Condition::MinHashSimilar {
511            column_id: 4,
512            query: vec![3, 1, 1, 2],
513            k: 5,
514        };
515        let minhash_canonical = Condition::MinHashSimilar {
516            column_id: 4,
517            query: vec![1, 2, 3],
518            k: 5,
519        };
520        assert_eq!(
521            canonical_query_key(std::slice::from_ref(&minhash), None, e),
522            canonical_query_key(&[minhash_canonical], None, e),
523        );
524
525        let fm = Condition::FmContainsAll {
526            column_id: 4,
527            patterns: vec![b"b".to_vec(), b"a".to_vec(), b"a".to_vec()],
528        };
529        let fm_canonical = Condition::FmContainsAll {
530            column_id: 4,
531            patterns: vec![b"a".to_vec(), b"b".to_vec()],
532        };
533        assert_eq!(
534            canonical_query_key(std::slice::from_ref(&fm), None, e),
535            canonical_query_key(&[fm_canonical], None, e),
536        );
537        assert_ne!(
538            canonical_query_key(std::slice::from_ref(&fm), None, e),
539            canonical_query_key(std::slice::from_ref(&minhash), None, e),
540        );
541        assert_ne!(
542            canonical_query_key_with_version(1, &a.conditions, None, e),
543            canonical_query_key_with_version(2, &a.conditions, None, e),
544        );
545
546        // BitmapIn dedup + sort.
547        let ordered = Condition::BitmapIn {
548            column_id: 3,
549            values: vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()],
550        };
551        let shuffled = Condition::BitmapIn {
552            column_id: 3,
553            values: vec![b"c".to_vec(), b"a".to_vec(), b"a".to_vec(), b"b".to_vec()],
554        };
555        assert_eq!(
556            canonical_query_key(std::slice::from_ref(&ordered), None, e),
557            canonical_query_key(&[shuffled], None, e),
558            "BitmapIn values must dedup+sort"
559        );
560
561        // Epoch changes the key (invalidation).
562        assert_ne!(
563            canonical_query_key(&a.conditions, None, e),
564            canonical_query_key(&a.conditions, None, e + 1),
565            "epoch must fold into the key"
566        );
567
568        // Projection None vs explicit differs (by intent).
569        let proj = vec![1u16, 2];
570        assert_ne!(
571            canonical_query_key(&a.conditions, None, e),
572            canonical_query_key(&a.conditions, Some(&proj), e),
573            "None projection must differ from an explicit projection"
574        );
575        // Projection order-independence.
576        let proj_rev = vec![2u16, 1];
577        assert_eq!(
578            canonical_query_key(&a.conditions, Some(&proj), e),
579            canonical_query_key(&a.conditions, Some(&proj_rev), e),
580        );
581    }
582}