1pub 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;
18pub const MAX_FUSED_CANDIDATES: usize = 1_000_000;
19
20#[derive(Debug, Clone)]
23pub struct AiExecutionContext {
24 deadline: Option<std::time::Instant>,
25 remaining_work: std::sync::Arc<std::sync::atomic::AtomicUsize>,
26 cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
27}
28
29impl AiExecutionContext {
30 pub fn new(deadline: Option<std::time::Instant>, work_budget: usize) -> Self {
31 Self {
32 deadline,
33 remaining_work: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(work_budget)),
34 cancelled: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
35 }
36 }
37
38 pub fn with_timeout(timeout: std::time::Duration, work_budget: usize) -> Self {
39 Self::new(Some(std::time::Instant::now() + timeout), work_budget)
40 }
41
42 pub fn cancel(&self) {
43 self.cancelled
44 .store(true, std::sync::atomic::Ordering::Release);
45 }
46
47 pub fn checkpoint(&self) -> crate::Result<()> {
48 if self.cancelled.load(std::sync::atomic::Ordering::Acquire) {
49 return Err(crate::MongrelError::Cancelled);
50 }
51 if self
52 .deadline
53 .is_some_and(|deadline| std::time::Instant::now() >= deadline)
54 {
55 return Err(crate::MongrelError::DeadlineExceeded);
56 }
57 Ok(())
58 }
59
60 pub fn consume(&self, work: usize) -> crate::Result<()> {
61 self.checkpoint()?;
62 let result = self.remaining_work.fetch_update(
63 std::sync::atomic::Ordering::AcqRel,
64 std::sync::atomic::Ordering::Acquire,
65 |remaining| remaining.checked_sub(work),
66 );
67 if result.is_err() {
68 return Err(crate::MongrelError::WorkBudgetExceeded);
69 }
70 self.checkpoint()
71 }
72}
73
74#[derive(Debug, Clone)]
76pub enum Condition {
77 Pk(Vec<u8>),
79 BitmapEq { column_id: u16, value: Vec<u8> },
81 BitmapIn {
86 column_id: u16,
87 values: Vec<Vec<u8>>,
88 },
89 BytesPrefix { column_id: u16, prefix: Vec<u8> },
94 Ann {
96 column_id: u16,
97 query: Vec<f32>,
98 k: usize,
99 },
100 FmContains { column_id: u16, pattern: Vec<u8> },
102 FmContainsAll {
107 column_id: u16,
108 patterns: Vec<Vec<u8>>,
109 },
110 Range { column_id: u16, lo: i64, hi: i64 },
114 RangeF64 {
117 column_id: u16,
118 lo: f64,
119 lo_inclusive: bool,
120 hi: f64,
121 hi_inclusive: bool,
122 },
123 SparseMatch {
126 column_id: u16,
127 query: Vec<(u32, f32)>,
128 k: usize,
129 },
130 MinHashSimilar {
134 column_id: u16,
135 query: Vec<u64>,
136 k: usize,
137 },
138 IsNull { column_id: u16 },
142 IsNotNull { column_id: u16 },
145}
146
147#[derive(Debug, Clone)]
150pub enum Retriever {
151 Ann {
152 column_id: u16,
153 query: Vec<f32>,
154 k: usize,
155 },
156 Sparse {
157 column_id: u16,
158 query: Vec<(u32, f32)>,
159 k: usize,
160 },
161 MinHash {
162 column_id: u16,
163 members: Vec<SetMember>,
164 k: usize,
165 },
166}
167
168impl Retriever {
169 pub fn column_id(&self) -> u16 {
170 match self {
171 Self::Ann { column_id, .. }
172 | Self::Sparse { column_id, .. }
173 | Self::MinHash { column_id, .. } => *column_id,
174 }
175 }
176}
177
178pub fn encode_sparse_vector(terms: &[(u32, f32)]) -> crate::Result<Vec<u8>> {
179 Ok(bincode::serialize(terms)?)
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
183#[serde(untagged)]
184pub enum SetMember {
185 String(String),
186 Number(serde_json::Number),
187 Boolean(bool),
188}
189
190impl SetMember {
191 pub fn hash_v1(&self) -> u64 {
192 let value = match self {
193 Self::String(value) => serde_json::Value::String(value.clone()),
194 Self::Number(value) => serde_json::Value::Number(value.clone()),
195 Self::Boolean(value) => serde_json::Value::Bool(*value),
196 };
197 crate::index::minhash_member_hash_v1(&value).expect("SetMember is always scalar")
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq)]
202pub enum RetrieverScore {
203 AnnHammingDistance(u32),
204 SparseDotProduct(f64),
205 MinHashEstimatedJaccard(f32),
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum VectorMetric {
210 Cosine,
211 DotProduct,
212 Euclidean,
213}
214
215#[derive(Debug, Clone)]
216pub struct AnnRerankRequest {
217 pub column_id: u16,
218 pub query: Vec<f32>,
219 pub candidate_k: usize,
220 pub limit: usize,
221 pub metric: VectorMetric,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq)]
225pub struct AnnRerankHit {
226 pub row_id: crate::RowId,
227 pub hamming_distance: u32,
228 pub exact_score: f32,
229}
230
231#[derive(Debug, Clone, PartialEq)]
232pub struct RetrieverHit {
233 pub row_id: crate::RowId,
234 pub rank: usize,
236 pub score: RetrieverScore,
237}
238
239#[derive(Debug, Clone)]
240pub struct SetSimilarityRequest {
241 pub column_id: u16,
242 pub members: Vec<SetMember>,
243 pub candidate_k: usize,
244 pub min_jaccard: f32,
245 pub limit: usize,
246}
247
248#[derive(Debug, Clone, PartialEq)]
249pub struct SetSimilarityHit {
250 pub row_id: crate::RowId,
251 pub estimated_jaccard: f32,
252 pub exact_jaccard: f32,
253}
254
255#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
256pub struct SetSimilarityTrace {
257 pub candidate_generation_us: u64,
258 pub gather_us: u64,
259 pub parse_us: u64,
260 pub score_us: u64,
261 pub candidate_count: usize,
262 pub verified_count: usize,
263}
264
265#[derive(Debug, Clone)]
266pub struct SearchRequest {
267 pub must: Vec<Condition>,
268 pub retrievers: Vec<NamedRetriever>,
269 pub fusion: Fusion,
270 pub limit: usize,
271 pub projection: Option<Vec<u16>>,
272}
273
274#[derive(Debug, Clone)]
275pub struct NamedRetriever {
276 pub name: String,
277 pub weight: f64,
278 pub retriever: Retriever,
279}
280
281#[derive(Debug, Clone, Copy)]
282pub enum Fusion {
283 ReciprocalRank { constant: u32 },
284}
285
286#[derive(Debug, Clone, PartialEq)]
287pub struct ComponentScore {
288 pub retriever_name: String,
289 pub rank: usize,
290 pub raw_score: RetrieverScore,
291 pub contribution: f64,
292}
293
294#[derive(Debug, Clone, PartialEq)]
295pub struct SearchHit {
296 pub row_id: crate::RowId,
297 pub cells: Vec<(u16, crate::Value)>,
298 pub components: Vec<ComponentScore>,
299 pub fused_score: f64,
300}
301
302#[derive(Debug, Clone, Default)]
304pub struct Query {
305 pub conditions: Vec<Condition>,
306 pub limit: Option<usize>,
307 pub offset: usize,
308}
309
310impl Query {
311 pub fn new() -> Self {
312 Self::default()
313 }
314 pub fn and(mut self, c: Condition) -> Self {
315 self.conditions.push(c);
316 self
317 }
318 pub fn with_limit(mut self, limit: usize) -> Self {
319 self.limit = Some(limit);
320 self
321 }
322 pub fn with_offset(mut self, offset: usize) -> Self {
323 self.offset = offset;
324 self
325 }
326 pub fn pk(key: Vec<u8>) -> Self {
327 Self::new().and(Condition::Pk(key))
328 }
329}
330
331pub fn canonical_query_key(
340 conditions: &[Condition],
341 projection: Option<&[u16]>,
342 epoch: u64,
343) -> u64 {
344 canonical_query_key_with_version(QUERY_CACHE_KEY_VERSION, conditions, projection, epoch)
345}
346
347const QUERY_CACHE_KEY_VERSION: u64 = 2;
348
349fn canonical_query_key_with_version(
350 version: u64,
351 conditions: &[Condition],
352 projection: Option<&[u16]>,
353 epoch: u64,
354) -> u64 {
355 let fold = |seed: u64, b: u64| -> u64 { seed.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(b) };
356 let mut acc = fold(0xA5A5_A5A5_A5A5_A5A5, version);
357 acc = fold(acc, epoch);
358 let mut digests: Vec<u64> = conditions.iter().map(hash_condition).collect();
360 digests.sort_unstable();
361 let n = digests.len() as u64;
362 acc = fold(acc, n);
363 for d in digests {
364 acc = fold(acc, d);
365 }
366 match projection {
369 Some(p) => {
370 let mut p = p.to_vec();
371 p.sort_unstable();
372 p.dedup();
373 acc = fold(acc, 0x5E);
374 acc = fold(acc, p.len() as u64);
375 for id in p {
376 acc = fold(acc, id as u64);
377 }
378 }
379 None => {
380 acc = fold(acc, 0xA5);
381 }
382 }
383 acc
384}
385
386fn hash_condition(c: &Condition) -> u64 {
390 use std::hash::{Hash, Hasher};
391 let mut h = std::collections::hash_map::DefaultHasher::new();
392 match c {
393 Condition::Pk(k) => {
394 0u8.hash(&mut h);
395 k.hash(&mut h);
396 }
397 Condition::BitmapEq { column_id, value } => {
398 1u8.hash(&mut h);
399 column_id.hash(&mut h);
400 value.hash(&mut h);
401 }
402 Condition::BitmapIn { column_id, values } => {
403 2u8.hash(&mut h);
404 column_id.hash(&mut h);
405 let mut v: Vec<&Vec<u8>> = values.iter().collect();
406 v.sort();
407 v.dedup();
408 v.len().hash(&mut h);
409 for b in v {
410 b.hash(&mut h);
411 }
412 }
413 Condition::Ann {
414 column_id,
415 query,
416 k,
417 } => {
418 3u8.hash(&mut h);
419 column_id.hash(&mut h);
420 k.hash(&mut h);
421 for f in query {
422 f.to_bits().hash(&mut h);
423 }
424 }
425 Condition::FmContains { column_id, pattern } => {
426 4u8.hash(&mut h);
427 column_id.hash(&mut h);
428 pattern.hash(&mut h);
429 }
430 Condition::FmContainsAll {
431 column_id,
432 patterns,
433 } => {
434 5u8.hash(&mut h);
435 column_id.hash(&mut h);
436 let mut sorted: Vec<&[u8]> = patterns.iter().map(|p| p.as_slice()).collect();
437 sorted.sort();
438 sorted.dedup();
439 sorted.len().hash(&mut h);
440 for p in sorted {
441 p.hash(&mut h);
442 }
443 }
444 Condition::Range { column_id, lo, hi } => {
445 6u8.hash(&mut h);
446 column_id.hash(&mut h);
447 lo.hash(&mut h);
448 hi.hash(&mut h);
449 }
450 Condition::RangeF64 {
451 column_id,
452 lo,
453 lo_inclusive,
454 hi,
455 hi_inclusive,
456 } => {
457 7u8.hash(&mut h);
458 column_id.hash(&mut h);
459 lo.to_bits().hash(&mut h);
460 lo_inclusive.hash(&mut h);
461 hi.to_bits().hash(&mut h);
462 hi_inclusive.hash(&mut h);
463 }
464 Condition::SparseMatch {
465 column_id,
466 query,
467 k,
468 } => {
469 8u8.hash(&mut h);
470 column_id.hash(&mut h);
471 k.hash(&mut h);
472 let mut q: Vec<(u32, u32)> = query.iter().map(|(t, w)| (*t, w.to_bits())).collect();
473 q.sort_by_key(|(t, _)| *t);
474 for (t, wb) in q {
475 t.hash(&mut h);
476 wb.hash(&mut h);
477 }
478 }
479 Condition::MinHashSimilar {
480 column_id,
481 query,
482 k,
483 } => {
484 9u8.hash(&mut h);
485 column_id.hash(&mut h);
486 k.hash(&mut h);
487 let mut q = query.clone();
488 q.sort_unstable();
489 q.dedup();
490 for t in q {
491 t.hash(&mut h);
492 }
493 }
494 Condition::IsNull { column_id } => {
495 10u8.hash(&mut h);
496 column_id.hash(&mut h);
497 }
498 Condition::IsNotNull { column_id } => {
499 11u8.hash(&mut h);
500 column_id.hash(&mut h);
501 }
502 Condition::BytesPrefix { column_id, prefix } => {
503 12u8.hash(&mut h);
504 column_id.hash(&mut h);
505 prefix.hash(&mut h);
506 }
507 }
508 h.finish()
509}
510
511pub fn condition_columns(conditions: &[Condition]) -> Vec<u16> {
516 let mut cols: Vec<u16> = conditions
517 .iter()
518 .filter_map(|c| match c {
519 Condition::Pk(_) => None,
520 Condition::BitmapEq { column_id, .. }
521 | Condition::BitmapIn { column_id, .. }
522 | Condition::BytesPrefix { column_id, .. }
523 | Condition::Ann { column_id, .. }
524 | Condition::FmContains { column_id, .. }
525 | Condition::FmContainsAll { column_id, .. }
526 | Condition::Range { column_id, .. }
527 | Condition::RangeF64 { column_id, .. }
528 | Condition::SparseMatch { column_id, .. }
529 | Condition::MinHashSimilar { column_id, .. }
530 | Condition::IsNull { column_id }
531 | Condition::IsNotNull { column_id } => Some(*column_id),
532 })
533 .collect();
534 cols.sort_unstable();
535 cols.dedup();
536 cols
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn builder_chains() {
545 let q = Query::pk(b"k".to_vec()).and(Condition::Range {
546 column_id: 1,
547 lo: 0,
548 hi: 10,
549 });
550 assert_eq!(q.conditions.len(), 2);
551 }
552
553 #[test]
557 fn canonical_key_is_order_independent() {
558 let e = 7u64;
559 let a = Query::new()
560 .and(Condition::Range {
561 column_id: 1,
562 lo: 0,
563 hi: 10,
564 })
565 .and(Condition::BitmapEq {
566 column_id: 2,
567 value: b"x".to_vec(),
568 });
569 let b = Query::new()
570 .and(Condition::BitmapEq {
571 column_id: 2,
572 value: b"x".to_vec(),
573 })
574 .and(Condition::Range {
575 column_id: 1,
576 lo: 0,
577 hi: 10,
578 });
579 assert_eq!(
580 canonical_query_key(&a.conditions, None, e),
581 canonical_query_key(&b.conditions, None, e),
582 "condition order must not affect the key"
583 );
584
585 let minhash = Condition::MinHashSimilar {
586 column_id: 4,
587 query: vec![3, 1, 1, 2],
588 k: 5,
589 };
590 let minhash_canonical = Condition::MinHashSimilar {
591 column_id: 4,
592 query: vec![1, 2, 3],
593 k: 5,
594 };
595 assert_eq!(
596 canonical_query_key(std::slice::from_ref(&minhash), None, e),
597 canonical_query_key(&[minhash_canonical], None, e),
598 );
599
600 let fm = Condition::FmContainsAll {
601 column_id: 4,
602 patterns: vec![b"b".to_vec(), b"a".to_vec(), b"a".to_vec()],
603 };
604 let fm_canonical = Condition::FmContainsAll {
605 column_id: 4,
606 patterns: vec![b"a".to_vec(), b"b".to_vec()],
607 };
608 assert_eq!(
609 canonical_query_key(std::slice::from_ref(&fm), None, e),
610 canonical_query_key(&[fm_canonical], None, e),
611 );
612 assert_ne!(
613 canonical_query_key(std::slice::from_ref(&fm), None, e),
614 canonical_query_key(std::slice::from_ref(&minhash), None, e),
615 );
616 assert_ne!(
617 canonical_query_key_with_version(1, &a.conditions, None, e),
618 canonical_query_key_with_version(2, &a.conditions, None, e),
619 );
620
621 let ordered = Condition::BitmapIn {
623 column_id: 3,
624 values: vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()],
625 };
626 let shuffled = Condition::BitmapIn {
627 column_id: 3,
628 values: vec![b"c".to_vec(), b"a".to_vec(), b"a".to_vec(), b"b".to_vec()],
629 };
630 assert_eq!(
631 canonical_query_key(std::slice::from_ref(&ordered), None, e),
632 canonical_query_key(&[shuffled], None, e),
633 "BitmapIn values must dedup+sort"
634 );
635
636 assert_ne!(
638 canonical_query_key(&a.conditions, None, e),
639 canonical_query_key(&a.conditions, None, e + 1),
640 "epoch must fold into the key"
641 );
642
643 let proj = vec![1u16, 2];
645 assert_ne!(
646 canonical_query_key(&a.conditions, None, e),
647 canonical_query_key(&a.conditions, Some(&proj), e),
648 "None projection must differ from an explicit projection"
649 );
650 let proj_rev = vec![2u16, 1];
652 assert_eq!(
653 canonical_query_key(&a.conditions, Some(&proj), e),
654 canonical_query_key(&a.conditions, Some(&proj_rev), e),
655 );
656
657 let budget = AiExecutionContext::new(None, 2);
658 budget.consume(1).unwrap();
659 assert!(matches!(
660 budget.consume(2),
661 Err(crate::MongrelError::WorkBudgetExceeded)
662 ));
663 budget.cancel();
664 assert!(matches!(
665 budget.checkpoint(),
666 Err(crate::MongrelError::Cancelled)
667 ));
668
669 let expired = AiExecutionContext::new(
670 Some(std::time::Instant::now() - std::time::Duration::from_millis(1)),
671 1,
672 );
673 assert!(matches!(
674 expired.checkpoint(),
675 Err(crate::MongrelError::DeadlineExceeded)
676 ));
677 }
678}