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