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