1use crate::def::Predicate;
2use crate::hnsw::HnswIndex;
3use core_storage::{list_tokens, Value, ValueKey};
4use std::collections::{BTreeMap, BTreeSet};
5
6pub const IVF_K_MIN: usize = 4;
13
14pub const IVF_K_MAX: usize = 1024;
16
17pub const IVF_ITERATIONS: usize = 12;
19
20pub const IVF_PROBE_DENOM: usize = 16;
23
24pub const IVF_DRIFT_REBUILD: u64 = 256;
28
29thread_local! {
30 static IVF_DRIFT_REBUILD_OVERRIDE: std::cell::Cell<Option<u64>> =
31 const { std::cell::Cell::new(None) };
32}
33
34pub(crate) fn ivf_drift_rebuild_threshold() -> u64 {
35 IVF_DRIFT_REBUILD_OVERRIDE.with(|c| c.get().unwrap_or(IVF_DRIFT_REBUILD))
36}
37
38pub fn with_ivf_drift_rebuild<R>(threshold: u64, f: impl FnOnce() -> R) -> R {
41 IVF_DRIFT_REBUILD_OVERRIDE.with(|c| {
42 let prev = c.replace(Some(threshold));
43 let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
44 c.set(prev);
45 match out {
46 Ok(v) => v,
47 Err(p) => std::panic::resume_unwind(p),
48 }
49 })
50}
51
52pub fn cluster_k(n: usize) -> usize {
54 if n == 0 {
55 return IVF_K_MIN;
56 }
57 let k = (n as f64).sqrt().ceil() as usize;
58 k.clamp(IVF_K_MIN, IVF_K_MAX)
59}
60
61pub fn probe_count(k: usize) -> usize {
63 k.div_ceil(IVF_PROBE_DENOM).max(1)
64}
65
66fn l2_normalize(xs: &[f64]) -> Option<Vec<f64>> {
68 let n = xs.iter().map(|x| x * x).sum::<f64>().sqrt();
69 if n == 0.0 {
70 return None;
71 }
72 Some(xs.iter().map(|x| x / n).collect())
73}
74
75fn l2_sq(a: &[f64], b: &[f64]) -> f64 {
78 if a.len() != b.len() {
79 return f64::MAX;
80 }
81 a.iter().zip(b.iter()).map(|(x, y)| (x - y) * (x - y)).sum()
82}
83
84pub fn nearest_centroid(centroids: &[Vec<f64>], xs: &[f64]) -> usize {
87 centroids
88 .iter()
89 .enumerate()
90 .min_by(|(_, a), (_, b)| {
91 l2_sq(xs, a)
92 .partial_cmp(&l2_sq(xs, b))
93 .unwrap_or(std::cmp::Ordering::Equal)
94 })
95 .map(|(i, _)| i)
96 .unwrap_or(0)
97}
98
99pub fn fnv1a_u64(data: &[u8]) -> u64 {
103 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
104 const FNV_PRIME: u64 = 1_099_511_628_211;
105 let mut h = FNV_OFFSET;
106 for &b in data {
107 h ^= b as u64;
108 h = h.wrapping_mul(FNV_PRIME);
109 }
110 h
111}
112
113#[inline]
116fn lcg_next(state: u64) -> u64 {
117 state
118 .wrapping_mul(6_364_136_223_846_793_005)
119 .wrapping_add(1_442_695_040_888_963_407)
120}
121
122pub fn kmeans_fit(vecs: &[(u32, Vec<f64>)], k: usize, seed: u64) -> Vec<Vec<f64>> {
132 let vecs: Vec<(u32, Vec<f64>)> = vecs
133 .iter()
134 .filter_map(|(id, xs)| l2_normalize(xs).map(|n| (*id, n)))
135 .collect();
136 if vecs.is_empty() || k == 0 {
137 return vec![];
138 }
139 let n = vecs.len();
140 let k = k.min(n);
141 let dim = vecs[0].1.len();
142 if dim == 0 {
143 return vec![];
144 }
145
146 let mut state = seed;
148 let mut used = vec![false; n];
149 let mut init_idxs: Vec<usize> = Vec::with_capacity(k);
150 let mut attempts = 0usize;
151 while init_idxs.len() < k && attempts < n * 4 {
152 state = lcg_next(state);
153 let idx = (state >> 33) as usize % n;
154 if !used[idx] {
155 used[idx] = true;
156 init_idxs.push(idx);
157 }
158 attempts += 1;
159 }
160 if init_idxs.len() < k {
163 for (i, in_use) in used.iter().enumerate().take(n) {
164 if !in_use {
165 init_idxs.push(i);
166 if init_idxs.len() == k {
167 break;
168 }
169 }
170 }
171 }
172 let mut centroids: Vec<Vec<f64>> = init_idxs.iter().map(|&i| vecs[i].1.clone()).collect();
173 let mut assignments = vec![0usize; n];
174
175 for iter in 0..IVF_ITERATIONS {
177 for (j, (_, xs)) in vecs.iter().enumerate() {
179 assignments[j] = nearest_centroid(¢roids, xs);
180 }
181
182 let mut sums = vec![vec![0.0f64; dim]; k];
184 let mut counts = vec![0usize; k];
185 for (j, (_, xs)) in vecs.iter().enumerate() {
186 let c = assignments[j];
187 counts[c] += 1;
188 for d in 0..dim {
189 sums[c][d] += xs[d];
190 }
191 }
192
193 let mut new_centroids = vec![vec![0.0f64; dim]; k];
195 let mut empty: Vec<usize> = Vec::new();
196 for c in 0..k {
197 if counts[c] == 0 {
198 empty.push(c);
199 } else {
200 for d in 0..dim {
201 new_centroids[c][d] = sums[c][d] / counts[c] as f64;
202 }
203 }
204 }
205
206 for (ei, ec) in empty.into_iter().enumerate() {
209 let reseed =
210 seed ^ (iter as u64).wrapping_mul(0x9E37) ^ (ei as u64).wrapping_mul(0x1234_5679);
211 let mut rs = lcg_next(reseed);
212 rs = lcg_next(rs);
213 let pick = (rs >> 33) as usize % n;
214 new_centroids[ec] = vecs[pick].1.clone();
215 }
216
217 centroids = new_centroids;
218 }
219
220 centroids
221}
222
223#[cfg(test)]
224thread_local! {
225 static VECTOR_DIM_REJECT: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
226 static VECTOR_EARLY_EXIT: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
227}
228
229fn vector_dim_reject_enabled() -> bool {
230 #[cfg(test)]
231 {
232 VECTOR_DIM_REJECT.with(|c| c.get())
233 }
234 #[cfg(not(test))]
235 {
236 true
237 }
238}
239
240pub(crate) fn vector_early_exit_enabled() -> bool {
241 #[cfg(test)]
242 {
243 VECTOR_EARLY_EXIT.with(|c| c.get())
244 }
245 #[cfg(not(test))]
246 {
247 true
248 }
249}
250
251#[cfg(test)]
253pub fn with_vector_dim_reject<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
254 VECTOR_DIM_REJECT.with(|c| {
255 let prev = c.replace(enabled);
256 let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
257 c.set(prev);
258 match out {
259 Ok(v) => v,
260 Err(p) => std::panic::resume_unwind(p),
261 }
262 })
263}
264
265#[cfg(test)]
267pub fn with_vector_early_exit<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
268 VECTOR_EARLY_EXIT.with(|c| {
269 let prev = c.replace(enabled);
270 let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
271 c.set(prev);
272 match out {
273 Ok(v) => v,
274 Err(p) => std::panic::resume_unwind(p),
275 }
276 })
277}
278
279#[derive(Debug, Default)]
280pub struct SideIndex {
281 by_key: BTreeMap<ValueKey, BTreeSet<u32>>,
282 vec_meta: BTreeMap<u32, (u32, f64)>,
287 vec_checkpoints: BTreeMap<u32, [f64; 8]>,
294 vec_anchor: BTreeMap<u32, f64>,
301
302 ivf_raw: BTreeMap<u32, Vec<f64>>,
307 ivf_centroids: Vec<Vec<f64>>,
309 ivf_clusters: BTreeMap<u32, usize>,
312 pub ivf_drift: u64,
316
317 hnsw: Option<HnswIndex>,
320 hnsw_tracked: BTreeSet<u32>,
323}
324
325#[derive(Debug, Default)]
326pub struct RuleIndex {
327 pub src_side: SideIndex,
328 pub dst_side: SideIndex,
329}
330
331#[derive(Debug)]
332pub enum CandidateSpec<'a> {
333 ByKey,
334 Scalar {
335 field: &'a str,
336 },
337 Tokens {
338 field: &'a str,
339 },
340 NumericBucket {
341 field: &'a str,
342 tolerance: f64,
343 },
344 GeoGrid {
345 field: &'a str,
346 km: f64,
347 },
348 ScanAll {
349 field: &'a str,
350 },
351 VectorClusters {
358 field: &'a str,
359 min: f64,
360 },
361 Hnsw {
368 field: &'a str,
369 k: usize,
372 },
373 Union(Vec<CandidateSpec<'a>>),
380 Intersect(Vec<CandidateSpec<'a>>),
387}
388
389pub fn candidate_spec(p: &Predicate) -> CandidateSpec<'_> {
402 match p {
403 Predicate::KeyMatch { .. } => CandidateSpec::ByKey,
404 Predicate::FieldEqual { field } => CandidateSpec::Scalar { field },
405 Predicate::Overlap { field, .. } => CandidateSpec::Tokens { field },
406 Predicate::NumericWithin { field, tolerance } => CandidateSpec::NumericBucket {
407 field,
408 tolerance: *tolerance,
409 },
410 Predicate::GeoRadius { field, km } => CandidateSpec::GeoGrid { field, km: *km },
411 Predicate::VectorSimilar { field, .. } => CandidateSpec::ScanAll { field },
412 Predicate::All(parts) => {
413 debug_assert!(
414 !parts.is_empty(),
415 "candidate_spec requires a validated predicate"
416 );
417 CandidateSpec::Intersect(parts.iter().map(candidate_spec).collect())
418 }
419 Predicate::Any(parts) => {
420 debug_assert!(
421 !parts.is_empty(),
422 "candidate_spec requires a validated predicate"
423 );
424 CandidateSpec::Union(parts.iter().map(candidate_spec).collect())
425 }
426 }
427}
428
429pub fn candidate_spec_approx(p: &Predicate) -> CandidateSpec<'_> {
448 candidate_spec_approx_with_k(p, 64)
449}
450
451pub fn candidate_spec_approx_with_k(p: &Predicate, k: usize) -> CandidateSpec<'_> {
453 match p {
454 Predicate::VectorSimilar { field, .. } => CandidateSpec::Hnsw { field, k },
455 Predicate::All(parts) => {
456 debug_assert!(
457 !parts.is_empty(),
458 "candidate_spec_approx requires a validated predicate"
459 );
460 CandidateSpec::Intersect(
461 parts
462 .iter()
463 .map(|p| candidate_spec_approx_with_k(p, k))
464 .collect(),
465 )
466 }
467 other => candidate_spec(other),
468 }
469}
470
471pub(crate) fn as_finite_f64(v: &Value) -> Option<f64> {
472 match v {
473 Value::Int(i) => Some(*i as f64),
474 Value::Float(f) if f.is_finite() => Some(*f),
475 _ => None,
476 }
477}
478
479fn as_latlon(v: &Value) -> Option<(f64, f64)> {
480 let Value::List(items) = v else {
481 return None;
482 };
483 if items.len() != 2 {
484 return None;
485 }
486 let lat = as_finite_f64(&items[0])?;
487 let lon = as_finite_f64(&items[1])?;
488 if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
489 Some((lat, lon))
490 } else {
491 None
492 }
493}
494
495pub(crate) fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
496 let Value::List(items) = v else {
497 return None;
498 };
499 if items.is_empty() {
500 return None;
501 }
502 items.iter().map(as_finite_f64).collect()
503}
504
505fn vec_dim_norm(v: &Value) -> Option<(u32, f64)> {
506 let xs = as_numeric_list(v)?;
507 let mut n2 = 0.0;
508 for x in &xs {
509 n2 += *x * *x;
510 }
511 Some((xs.len() as u32, n2.sqrt()))
512}
513
514fn compute_ckpts(xs: &[f64]) -> [f64; 8] {
520 let dim = xs.len();
521 let mut ckpts = [0.0f64; 8];
522 if dim == 0 {
523 return ckpts;
524 }
525 let boundaries: [usize; 8] = std::array::from_fn(|i| i * dim / 8);
527 let mut suffix_sq = 0.0f64;
528 let mut ci = 7i32;
530 for j in (0..dim).rev() {
531 suffix_sq += xs[j] * xs[j];
532 while ci >= 0 && boundaries[ci as usize] == j {
534 ckpts[ci as usize] = suffix_sq.sqrt();
535 ci -= 1;
536 }
537 }
538 ckpts
539}
540
541fn floor_to_i64(x: f64) -> i64 {
542 let floored = x.floor();
543 if !floored.is_finite() {
544 return 0;
545 }
546 if floored >= i64::MAX as f64 {
547 i64::MAX
548 } else if floored <= i64::MIN as f64 {
549 i64::MIN
550 } else {
551 floored as i64
552 }
553}
554
555fn numeric_index_key(v: f64, tolerance: f64) -> Option<ValueKey> {
559 if !tolerance.is_finite() || tolerance < 0.0 {
560 return None;
561 }
562 if tolerance == 0.0 {
563 let v = if v == 0.0 { 0.0_f64 } else { v };
564 return Some(ValueKey::FloatBits(v.to_bits()));
565 }
566 Some(ValueKey::Int(floor_to_i64(v / tolerance)))
567}
568
569fn numeric_probe_keys(v: f64, tolerance: f64) -> BTreeSet<ValueKey> {
570 match numeric_index_key(v, tolerance) {
571 None => BTreeSet::new(),
572 Some(k @ ValueKey::FloatBits(_)) => BTreeSet::from([k]),
573 Some(ValueKey::Int(b)) => BTreeSet::from([
574 ValueKey::Int(b.saturating_sub(1)),
575 ValueKey::Int(b),
576 ValueKey::Int(b.saturating_add(1)),
577 ]),
578 Some(other) => BTreeSet::from([other]),
579 }
580}
581
582fn geo_cell(lat: f64, lon: f64, km: f64) -> Option<(i64, i64, f64, i64)> {
583 if !km.is_finite() || km <= 0.0 {
584 return None;
585 }
586 let cell_deg = (km / 111.0).max(1e-6);
587 let gx = floor_to_i64(lat / cell_deg);
588 let lon_cells = (360.0 / cell_deg).ceil() as i64;
591 let lon_cells = lon_cells.max(1);
592 let gy = floor_to_i64(lon / cell_deg).rem_euclid(lon_cells);
593 Some((gx, gy, cell_deg, lon_cells))
594}
595
596fn geo_index_key(lat: f64, lon: f64, km: f64) -> Option<ValueKey> {
597 let (gx, gy, _, _) = geo_cell(lat, lon, km)?;
598 Some(ValueKey::Str(format!("{gx}|{gy}")))
599}
600
601fn geo_probe_keys(lat: f64, lon: f64, km: f64) -> BTreeSet<ValueKey> {
602 let Some((gx, gy, cell_deg, lon_cells)) = geo_cell(lat, lon, km) else {
603 return BTreeSet::new();
604 };
605 let cos_lat = lat.to_radians().cos().max(0.05);
607 let n = ((km / (111.0 * cos_lat)) / cell_deg).ceil();
608 let n = if n.is_finite() {
609 floor_to_i64(n).max(0)
610 } else {
611 0
612 };
613 let mut out = BTreeSet::new();
614 for dx in -1..=1 {
615 for dy in -n..=n {
616 let cx = gx.saturating_add(dx);
617 let cy = gy.saturating_add(dy).rem_euclid(lon_cells);
618 out.insert(ValueKey::Str(format!("{cx}|{cy}")));
619 }
620 }
621 out
622}
623
624const SCAN_ALL_SENTINEL: ValueKey = ValueKey::Bool(true);
627
628fn ivf_cluster_key(cluster: usize) -> ValueKey {
631 ValueKey::Str(format!("\u{1}ivf:{cluster}"))
632}
633
634fn spec_is_scan_all_universe(spec: &CandidateSpec<'_>) -> bool {
637 match spec {
638 CandidateSpec::ScanAll { .. } => true,
639 CandidateSpec::Intersect(parts) => {
640 !parts.is_empty() && parts.iter().all(spec_is_scan_all_universe)
641 }
642 _ => false,
643 }
644}
645
646fn spec_is_bykey_external(spec: &CandidateSpec<'_>) -> bool {
649 match spec {
650 CandidateSpec::ByKey => true,
651 CandidateSpec::Intersect(parts) => {
652 !parts.is_empty() && parts.iter().all(spec_is_bykey_external)
653 }
654 _ => false,
655 }
656}
657
658impl SideIndex {
659 fn index_keys(spec: &CandidateSpec, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<ValueKey> {
660 match spec {
661 CandidateSpec::ByKey => BTreeSet::new(),
662 CandidateSpec::Scalar { field } => get(field)
663 .as_ref()
664 .and_then(ValueKey::from_value)
665 .into_iter()
666 .collect(),
667 CandidateSpec::Tokens { field } => get(field)
668 .as_ref()
669 .and_then(list_tokens)
670 .unwrap_or_default(),
671 CandidateSpec::NumericBucket { field, tolerance } => get(field)
672 .as_ref()
673 .and_then(as_finite_f64)
674 .and_then(|v| numeric_index_key(v, *tolerance))
675 .into_iter()
676 .collect(),
677 CandidateSpec::GeoGrid { field, km } => get(field)
678 .as_ref()
679 .and_then(as_latlon)
680 .and_then(|(lat, lon)| geo_index_key(lat, lon, *km))
681 .into_iter()
682 .collect(),
683 CandidateSpec::ScanAll { field } => get(field)
684 .as_ref()
685 .and_then(as_numeric_list)
686 .map(|_| SCAN_ALL_SENTINEL)
687 .into_iter()
688 .collect(),
689 CandidateSpec::VectorClusters { .. } => BTreeSet::new(),
694 CandidateSpec::Hnsw { .. } => BTreeSet::new(),
696 CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) => {
700 let mut out = BTreeSet::new();
701 for s in specs {
702 out.extend(Self::index_keys(s, get));
703 }
704 out
705 }
706 }
707 }
708
709 fn probe_keys(spec: &CandidateSpec, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<ValueKey> {
710 match spec {
711 CandidateSpec::ByKey | CandidateSpec::Scalar { .. } | CandidateSpec::Tokens { .. } => {
712 Self::index_keys(spec, get)
713 }
714 CandidateSpec::NumericBucket { field, tolerance } => get(field)
715 .as_ref()
716 .and_then(as_finite_f64)
717 .map(|v| numeric_probe_keys(v, *tolerance))
718 .unwrap_or_default(),
719 CandidateSpec::GeoGrid { field, km } => get(field)
720 .as_ref()
721 .and_then(as_latlon)
722 .map(|(lat, lon)| geo_probe_keys(lat, lon, *km))
723 .unwrap_or_default(),
724 CandidateSpec::ScanAll { field } => get(field)
725 .as_ref()
726 .and_then(as_numeric_list)
727 .map(|_| SCAN_ALL_SENTINEL)
728 .into_iter()
729 .collect(),
730 CandidateSpec::VectorClusters { .. } => BTreeSet::new(),
732 CandidateSpec::Hnsw { .. } => BTreeSet::new(),
734 CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) => {
737 let mut out = BTreeSet::new();
738 for s in specs {
739 out.extend(Self::probe_keys(s, get));
740 }
741 out
742 }
743 }
744 }
745
746 pub fn insert(&mut self, spec: &CandidateSpec, node: u32, get: &dyn Fn(&str) -> Option<Value>) {
747 if let CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) = spec {
750 for s in specs {
751 self.insert(s, node, get);
752 }
753 return;
754 }
755 if let CandidateSpec::Hnsw { field, .. } = spec {
757 if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
758 self.hnsw_tracked.insert(node);
759 if let Some(h) = &mut self.hnsw {
760 h.insert(node, &xs);
761 }
762 }
763 return;
764 }
765 if let CandidateSpec::VectorClusters { field, .. } = spec {
767 if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
768 self.ivf_raw.insert(node, xs.clone());
769 if !self.ivf_centroids.is_empty() {
770 if let Some(unit) = l2_normalize(&xs) {
772 let c = nearest_centroid(&self.ivf_centroids, &unit);
773 self.ivf_clusters.insert(node, c);
774 self.by_key
775 .entry(ivf_cluster_key(c))
776 .or_default()
777 .insert(node);
778 }
779 self.ivf_drift = self.ivf_drift.saturating_add(1);
780 }
781 }
782 return;
783 }
784
785 for k in Self::index_keys(spec, get) {
786 self.by_key.entry(k).or_default().insert(node);
787 }
788 if let CandidateSpec::ScanAll { field } = spec {
789 if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
790 let mut n2 = 0.0f64;
791 for x in &xs {
792 n2 += x * x;
793 }
794 let norm = n2.sqrt();
795 self.vec_meta.insert(node, (xs.len() as u32, norm));
796 self.vec_checkpoints.insert(node, compute_ckpts(&xs));
797 self.vec_anchor.insert(node, xs[0]);
799 }
800 }
801 }
802
803 pub fn remove(&mut self, spec: &CandidateSpec, node: u32, get: &dyn Fn(&str) -> Option<Value>) {
804 if let CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) = spec {
806 for s in specs {
807 self.remove(s, node, get);
808 }
809 return;
810 }
811 if let CandidateSpec::Hnsw { field, .. } = spec {
816 if get(field).as_ref().and_then(as_numeric_list).is_some() {
817 self.hnsw_tracked.remove(&node);
818 if let Some(h) = &mut self.hnsw {
819 h.remove(node);
820 }
821 self.ivf_drift = self.ivf_drift.saturating_add(1);
822 }
823 return;
824 }
825 if let CandidateSpec::VectorClusters { .. } = spec {
830 if self.ivf_raw.remove(&node).is_some() {
831 self.ivf_drift = self.ivf_drift.saturating_add(1);
832 if let Some(c) = self.ivf_clusters.remove(&node) {
833 let key = ivf_cluster_key(c);
834 if let Some(s) = self.by_key.get_mut(&key) {
835 s.remove(&node);
836 if s.is_empty() {
837 self.by_key.remove(&key);
838 }
839 }
840 }
841 }
842 return;
843 }
844
845 for k in Self::index_keys(spec, get) {
846 if let Some(set) = self.by_key.get_mut(&k) {
847 set.remove(&node);
848 if set.is_empty() {
849 self.by_key.remove(&k);
850 }
851 }
852 }
853 if let CandidateSpec::ScanAll { field } = spec {
854 if get(field).as_ref().and_then(as_numeric_list).is_some() {
855 self.vec_meta.remove(&node);
856 self.vec_checkpoints.remove(&node);
857 self.vec_anchor.remove(&node);
858 }
859 }
860 }
861
862 pub fn vec_dim(&self, node: u32) -> Option<u32> {
864 self.vec_meta.get(&node).map(|(d, _)| *d)
865 }
866
867 pub fn vec_meta(&self, node: u32) -> Option<(u32, f64)> {
869 self.vec_meta.get(&node).copied()
870 }
871
872 pub fn vec_ckpts(&self, node: u32) -> Option<&[f64; 8]> {
874 self.vec_checkpoints.get(&node)
875 }
876
877 pub(crate) fn fresh_ckpts_for<'a>(
904 &'a self,
905 node: u32,
906 live: &[f64],
907 ) -> Option<(f64, &'a [f64; 8])> {
908 let &(dim, norm) = self.vec_meta.get(&node)?;
909 if dim != live.len() as u32 {
910 return None;
911 }
912 let live_norm = {
915 let mut n2 = 0.0f64;
916 for x in live {
917 n2 += x * x;
918 }
919 n2.sqrt()
920 };
921 if norm != live_norm {
922 return None; }
924 let live_anchor = live[0];
928 let &cached_anchor = self.vec_anchor.get(&node)?;
929 if live_anchor != cached_anchor {
930 return None;
931 }
932 let ckpts = self.vec_checkpoints.get(&node)?;
933 Some((norm, ckpts))
934 }
935
936 pub fn candidates(
937 &self,
938 spec: &CandidateSpec,
939 get: &dyn Fn(&str) -> Option<Value>,
940 ) -> BTreeSet<u32> {
941 if let CandidateSpec::Hnsw { field, k } = spec {
943 return self.hnsw_candidates(field, *k, get);
944 }
945 if let CandidateSpec::VectorClusters { field, .. } = spec {
947 return self.ivf_candidates(field, get);
948 }
949 if let CandidateSpec::Union(specs) = spec {
951 return specs.iter().flat_map(|s| self.candidates(s, get)).collect();
952 }
953 if let CandidateSpec::Intersect(specs) = spec {
954 return self.intersect_candidates(specs, get);
955 }
956
957 let mut out = BTreeSet::new();
958 for k in Self::probe_keys(spec, get) {
959 if let Some(set) = self.by_key.get(&k) {
960 out.extend(set.iter().copied());
961 }
962 }
963 if vector_dim_reject_enabled() {
965 if let CandidateSpec::ScanAll { field } = spec {
966 if let Some((dim, _)) = get(field).as_ref().and_then(vec_dim_norm) {
967 out.retain(|id| self.vec_meta.get(id).is_none_or(|(d, _)| *d == dim));
968 }
969 }
970 }
971 out
972 }
973
974 fn intersect_candidates(
978 &self,
979 specs: &[CandidateSpec<'_>],
980 get: &dyn Fn(&str) -> Option<Value>,
981 ) -> BTreeSet<u32> {
982 let mut restrictive = Vec::new();
983 let mut scan_alls = Vec::new();
984 for s in specs {
985 if spec_is_scan_all_universe(s) {
986 scan_alls.push(s);
987 } else if spec_is_bykey_external(s) {
988 continue;
989 } else {
990 restrictive.push(s);
991 }
992 }
993 let to_intersect: &[&CandidateSpec<'_>] = if !restrictive.is_empty() {
994 &restrictive
995 } else if !scan_alls.is_empty() {
996 &scan_alls
997 } else {
998 return BTreeSet::new();
999 };
1000 let mut iter = to_intersect.iter();
1001 let Some(first) = iter.next() else {
1002 return BTreeSet::new();
1003 };
1004 let mut acc = self.candidates(first, get);
1005 if acc.is_empty() {
1006 return acc;
1007 }
1008 for s in iter {
1009 let other = self.candidates(s, get);
1010 if other.is_empty() {
1011 return BTreeSet::new();
1012 }
1013 acc = acc.intersection(&other).copied().collect();
1014 if acc.is_empty() {
1015 return acc;
1016 }
1017 }
1018 acc
1019 }
1020
1021 fn ivf_candidates(&self, field: &str, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<u32> {
1024 let Some(xs) = get(field).as_ref().and_then(as_numeric_list) else {
1025 return BTreeSet::new();
1026 };
1027 if self.ivf_centroids.is_empty() {
1028 return self.ivf_raw.keys().copied().collect();
1032 }
1033 if self.ivf_raw.len() <= self.ivf_centroids.len() {
1038 return self.ivf_raw.keys().copied().collect();
1039 }
1040 let k = self.ivf_centroids.len();
1041 let p = probe_count(k);
1042
1043 let Some(xs) = l2_normalize(&xs) else {
1046 return BTreeSet::new();
1047 };
1048
1049 let mut dists: Vec<(usize, f64)> = self
1051 .ivf_centroids
1052 .iter()
1053 .enumerate()
1054 .map(|(i, c)| (i, l2_sq(&xs, c)))
1055 .collect();
1056 dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1057
1058 let mut out = BTreeSet::new();
1059 for (ci, _) in dists.iter().take(p) {
1060 let key = ivf_cluster_key(*ci);
1061 if let Some(nodes) = self.by_key.get(&key) {
1062 out.extend(nodes.iter().copied());
1063 }
1064 }
1065 out
1066 }
1067
1068 pub fn fit_ivf_clusters(&mut self, rule_name: &str) {
1079 if self.ivf_raw.is_empty() {
1080 self.ivf_centroids.clear();
1081 self.ivf_clusters.clear();
1082 self.ivf_drift = 0;
1083 return;
1084 }
1085
1086 for c in self.ivf_clusters.values() {
1088 self.by_key.remove(&ivf_cluster_key(*c));
1089 }
1090 self.ivf_clusters.clear();
1091
1092 let vecs: Vec<(u32, Vec<f64>)> = self
1094 .ivf_raw
1095 .iter()
1096 .map(|(&id, xs)| (id, xs.clone()))
1097 .collect();
1098
1099 let n = vecs.len();
1100 let k = cluster_k(n);
1101 let seed = fnv1a_u64(rule_name.as_bytes());
1102
1103 self.ivf_centroids = kmeans_fit(&vecs, k, seed);
1104
1105 for (node, xs) in &vecs {
1107 let Some(unit) = l2_normalize(xs) else {
1108 continue;
1109 };
1110 let c = nearest_centroid(&self.ivf_centroids, &unit);
1111 self.ivf_clusters.insert(*node, c);
1112 self.by_key
1113 .entry(ivf_cluster_key(c))
1114 .or_default()
1115 .insert(*node);
1116 }
1117 self.ivf_drift = 0;
1118 }
1119
1120 pub fn ivf_k(&self) -> usize {
1122 self.ivf_centroids.len()
1123 }
1124
1125 pub fn ivf_cluster_of(&self, node: u32) -> Option<usize> {
1127 self.ivf_clusters.get(&node).copied()
1128 }
1129
1130 pub fn export_ivf_state(&self) -> (Vec<Vec<f64>>, BTreeMap<u32, usize>, u64) {
1135 (
1136 self.ivf_centroids.clone(),
1137 self.ivf_clusters.clone(),
1138 self.ivf_drift,
1139 )
1140 }
1141
1142 pub fn load_ivf_state(
1154 &mut self,
1155 centroids: Vec<Vec<f64>>,
1156 clusters: BTreeMap<u32, usize>,
1157 drift: u64,
1158 ) {
1159 for c in self.ivf_clusters.values() {
1162 self.by_key.remove(&ivf_cluster_key(*c));
1163 }
1164 self.ivf_clusters.clear();
1165
1166 self.ivf_centroids = centroids;
1167 self.ivf_drift = drift;
1168
1169 for (&node, &c) in &clusters {
1171 if !self.ivf_raw.contains_key(&node) {
1172 continue;
1174 }
1175 self.ivf_clusters.insert(node, c);
1176 self.by_key
1177 .entry(ivf_cluster_key(c))
1178 .or_default()
1179 .insert(node);
1180 }
1181 }
1182
1183 pub fn init_hnsw(&mut self, rule_name: &str) {
1192 let seed = fnv1a_u64(rule_name.as_bytes());
1193 self.hnsw = Some(HnswIndex::new(seed));
1194 }
1195
1196 fn hnsw_candidates(
1201 &self,
1202 field: &str,
1203 k: usize,
1204 get: &dyn Fn(&str) -> Option<Value>,
1205 ) -> BTreeSet<u32> {
1206 let Some(xs) = get(field).as_ref().and_then(as_numeric_list) else {
1207 return BTreeSet::new();
1208 };
1209 if let Some(h) = &self.hnsw {
1210 if !h.is_empty() {
1211 return h.search(&xs, k).into_iter().map(|(id, _)| id).collect();
1212 }
1213 }
1214 self.hnsw_tracked.clone()
1216 }
1217
1218 pub fn export_hnsw_blob(&self) -> Vec<u8> {
1222 self.hnsw
1223 .as_ref()
1224 .and_then(|h| bincode::serialize(h).ok())
1225 .unwrap_or_default()
1226 }
1227
1228 pub fn load_hnsw_blob(&mut self, blob: &[u8]) {
1234 if blob.is_empty() {
1235 return;
1236 }
1237 if let Ok(h) = bincode::deserialize::<HnswIndex>(blob) {
1238 self.hnsw_tracked = h.node_ids();
1240 self.hnsw = Some(h);
1241 }
1242 }
1243
1244 pub fn has_hnsw(&self) -> bool {
1246 self.hnsw.as_ref().is_some_and(|h| !h.is_empty())
1247 }
1248
1249 pub fn hnsw_ref(&self) -> Option<&HnswIndex> {
1251 self.hnsw.as_ref()
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258 use crate::def::Predicate;
1259 use core_storage::Value;
1260 use std::collections::{BTreeMap, HashMap};
1261
1262 fn getter(map: &HashMap<String, Value>) -> impl Fn(&str) -> Option<Value> + '_ {
1263 move |f: &str| map.get(f).cloned()
1264 }
1265
1266 #[test]
1267 fn kmeans_centroids_are_unit_norm() {
1268 let vecs = vec![(0, vec![3.0, 0.0, 0.0]), (1, vec![0.0, 4.0, 0.0])];
1269 let cents = kmeans_fit(&vecs, 2, 1);
1270 for c in cents {
1271 let n = c.iter().map(|x| x * x).sum::<f64>().sqrt();
1272 assert!((n - 1.0).abs() < 1e-9, "{n}");
1273 }
1274 }
1275
1276 #[test]
1283 fn scaled_vector_joins_same_ivf_cluster_as_unit() {
1284 let spec = CandidateSpec::VectorClusters {
1286 field: "emb",
1287 min: 0.5,
1288 };
1289 let mut idx = SideIndex::default();
1290 idx.load_ivf_state(
1291 vec![vec![1.0, 0.0, 0.0], vec![2.5, 0.1, 0.0]],
1292 BTreeMap::new(),
1293 0,
1294 );
1295 idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0, 0.0])));
1296 idx.insert(&spec, 2, &getter(&emb(&[3.0, 0.0, 0.0])));
1297 assert_eq!(
1298 idx.ivf_cluster_of(1),
1299 idx.ivf_cluster_of(2),
1300 "scale-equivalent vectors must share an IVF cluster; got {:?} vs {:?}",
1301 idx.ivf_cluster_of(1),
1302 idx.ivf_cluster_of(2)
1303 );
1304 assert_eq!(idx.ivf_cluster_of(1), Some(0));
1305 }
1306
1307 #[test]
1308 fn scalar_index_buckets_by_value() {
1309 let pred = Predicate::FieldEqual {
1310 field: "ind".into(),
1311 };
1312 let spec = candidate_spec(&pred);
1313 let mut idx = SideIndex::default();
1314 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
1315 let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
1316 idx.insert(&spec, 1, &getter(&a));
1317 idx.insert(&spec, 2, &getter(&b));
1318 idx.insert(&spec, 3, &getter(&a));
1319 let c = idx.candidates(&spec, &getter(&a));
1320 assert_eq!(c.into_iter().collect::<Vec<_>>(), vec![1, 3]);
1321 idx.remove(&spec, 3, &getter(&a));
1322 assert_eq!(idx.candidates(&spec, &getter(&a)).len(), 1);
1323 let empty: HashMap<String, Value> = HashMap::new();
1325 idx.insert(&spec, 9, &getter(&empty));
1326 assert!(idx.candidates(&spec, &getter(&empty)).is_empty());
1327 }
1328
1329 #[test]
1330 fn token_index_unions_buckets() {
1331 let mk =
1332 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1333 let pred = Predicate::Overlap {
1334 field: "tags".into(),
1335 min: 0.5,
1336 };
1337 let spec = candidate_spec(&pred);
1338 let mut idx = SideIndex::default();
1339 let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
1340 let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
1341 let c: HashMap<_, _> = [("tags".to_string(), mk(&["q"]))].into();
1342 idx.insert(&spec, 1, &getter(&a));
1343 idx.insert(&spec, 2, &getter(&b));
1344 idx.insert(&spec, 3, &getter(&c));
1345 let probe: HashMap<_, _> = [("tags".to_string(), mk(&["y"]))].into();
1346 assert_eq!(
1347 idx.candidates(&spec, &getter(&probe))
1348 .into_iter()
1349 .collect::<Vec<_>>(),
1350 vec![1, 2]
1351 );
1352 idx.remove(&spec, 2, &getter(&b));
1353 assert_eq!(
1354 idx.candidates(&spec, &getter(&probe))
1355 .into_iter()
1356 .collect::<Vec<_>>(),
1357 vec![1]
1358 );
1359 }
1360
1361 #[test]
1362 fn all_intersects_parts_and_bykey_indexes_nothing() {
1363 let all = Predicate::All(vec![
1364 Predicate::FieldEqual {
1365 field: "ind".into(),
1366 },
1367 Predicate::Overlap {
1368 field: "tags".into(),
1369 min: 0.5,
1370 },
1371 ]);
1372 match candidate_spec(&all) {
1373 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1374 other => panic!("{other:?}"),
1375 }
1376 let km = Predicate::KeyMatch { field: "fk".into() };
1377 assert!(matches!(candidate_spec(&km), CandidateSpec::ByKey));
1378 let mut idx = SideIndex::default();
1379 let a: HashMap<_, _> = [("fk".to_string(), Value::Str("c1".into()))].into();
1380 idx.insert(&candidate_spec(&km), 1, &getter(&a));
1381 assert!(idx.candidates(&candidate_spec(&km), &getter(&a)).is_empty());
1382 }
1383
1384 fn year(v: Value) -> HashMap<String, Value> {
1385 [("year".to_string(), v)].into()
1386 }
1387
1388 fn loc(lat: f64, lon: f64) -> HashMap<String, Value> {
1389 [(
1390 "loc".to_string(),
1391 Value::List(vec![Value::Float(lat), Value::Float(lon)]),
1392 )]
1393 .into()
1394 }
1395
1396 fn emb(vals: &[f64]) -> HashMap<String, Value> {
1397 [(
1398 "emb".to_string(),
1399 Value::List(vals.iter().copied().map(Value::Float).collect()),
1400 )]
1401 .into()
1402 }
1403
1404 fn bucket_int(spec: &CandidateSpec, map: &HashMap<String, Value>) -> Option<i64> {
1405 match SideIndex::index_keys(spec, &getter(map)).into_iter().next() {
1406 Some(ValueKey::Int(b)) => Some(b),
1407 _ => None,
1408 }
1409 }
1410
1411 #[test]
1412 fn numeric_bucket_adjacency_and_far_value() {
1413 let pred = Predicate::NumericWithin {
1414 field: "year".into(),
1415 tolerance: 2.0,
1416 };
1417 let spec = candidate_spec(&pred);
1418 assert!(matches!(
1419 spec,
1420 CandidateSpec::NumericBucket {
1421 field: "year",
1422 tolerance
1423 } if tolerance == 2.0
1424 ));
1425
1426 let v10 = year(Value::Float(10.0));
1427 let v119 = year(Value::Float(11.9));
1428 let v99 = year(Value::Float(9.9));
1429 let v141 = year(Value::Float(14.1));
1430
1431 let b10 = bucket_int(&spec, &v10).unwrap();
1432 let b119 = bucket_int(&spec, &v119).unwrap();
1433 let b99 = bucket_int(&spec, &v99).unwrap();
1434 assert!((b10 - b119).abs() <= 1);
1436 assert!((b10 - b99).abs() <= 1);
1437
1438 let mut idx = SideIndex::default();
1439 idx.insert(&spec, 1, &getter(&v10));
1440 idx.insert(&spec, 2, &getter(&v119));
1441 idx.insert(&spec, 3, &getter(&v141));
1442 idx.insert(&spec, 4, &getter(&v99));
1443 let hits = idx.candidates(&spec, &getter(&v10));
1444 assert_eq!(hits.into_iter().collect::<Vec<_>>(), vec![1, 2, 4]);
1445 }
1446
1447 #[test]
1448 fn numeric_tol_zero_int_float_collide() {
1449 let pred = Predicate::NumericWithin {
1450 field: "year".into(),
1451 tolerance: 0.0,
1452 };
1453 let spec = candidate_spec(&pred);
1454 let mut idx = SideIndex::default();
1455 idx.insert(&spec, 1, &getter(&year(Value::Int(2))));
1456 assert_eq!(
1457 idx.candidates(&spec, &getter(&year(Value::Float(2.0))))
1458 .into_iter()
1459 .collect::<Vec<_>>(),
1460 vec![1]
1461 );
1462 assert!(idx
1463 .candidates(&spec, &getter(&year(Value::Float(2.1))))
1464 .is_empty());
1465 }
1466
1467 #[test]
1468 fn numeric_tol_zero_signed_zero_collides() {
1469 let pred = Predicate::NumericWithin {
1470 field: "year".into(),
1471 tolerance: 0.0,
1472 };
1473 let spec = candidate_spec(&pred);
1474 let neg = year(Value::Float(-0.0));
1475 let pos = year(Value::Float(0.0));
1476 let mut idx = SideIndex::default();
1477 idx.insert(&spec, 1, &getter(&neg));
1478 assert_eq!(
1479 idx.candidates(&spec, &getter(&pos))
1480 .into_iter()
1481 .collect::<Vec<_>>(),
1482 vec![1]
1483 );
1484 let mut idx2 = SideIndex::default();
1485 idx2.insert(&spec, 2, &getter(&pos));
1486 assert_eq!(
1487 idx2.candidates(&spec, &getter(&neg))
1488 .into_iter()
1489 .collect::<Vec<_>>(),
1490 vec![2]
1491 );
1492 }
1493
1494 #[test]
1495 fn geo_grid_same_cell_cross_cell_and_far_city() {
1496 let pred = Predicate::GeoRadius {
1497 field: "loc".into(),
1498 km: 400.0,
1499 };
1500 let spec = candidate_spec(&pred);
1501 assert!(matches!(
1502 spec,
1503 CandidateSpec::GeoGrid {
1504 field: "loc",
1505 km
1506 } if km == 400.0
1507 ));
1508
1509 let paris = loc(48.8566, 2.3522);
1510 let london = loc(51.5074, -0.1278);
1511 let nearby = loc(48.9, 2.4); let ny = loc(40.7128, -74.0060);
1513
1514 let mut idx = SideIndex::default();
1515 idx.insert(&spec, 1, &getter(&paris));
1516 idx.insert(&spec, 2, &getter(&london));
1517 idx.insert(&spec, 3, &getter(&nearby));
1518 idx.insert(&spec, 4, &getter(&ny));
1519
1520 let from_paris = idx.candidates(&spec, &getter(&paris));
1521 assert!(from_paris.contains(&1), "same-cell self");
1522 assert!(from_paris.contains(&3), "same-cell neighbor");
1523 assert!(from_paris.contains(&2), "cross-cell Paris↔London ~343.5 km");
1524 assert!(!from_paris.contains(&4), "New York not in 400 km probe");
1525 }
1526
1527 #[test]
1528 fn geo_grid_high_latitude_probe_is_superset() {
1529 let pred = Predicate::GeoRadius {
1530 field: "loc".into(),
1531 km: 340.0,
1532 };
1533 let spec = candidate_spec(&pred);
1534 let reyk = loc(64.1466, -21.9426);
1535 let lat = 64.0_f64;
1536 let dlon = 300.0 / (111.0 * lat.to_radians().cos());
1537 let east = loc(lat, -21.9426 + dlon);
1538
1539 let mut idx = SideIndex::default();
1540 idx.insert(&spec, 1, &getter(&reyk));
1541 idx.insert(&spec, 2, &getter(&east));
1542 let hits = idx.candidates(&spec, &getter(&reyk));
1543 assert!(
1544 hits.contains(&2),
1545 "300 km east of Reykjavik must stay in the high-lat probe"
1546 );
1547 }
1548
1549 #[test]
1550 fn geo_grid_antimeridian_wrap_and_evaluate_agree() {
1551 let pred = Predicate::GeoRadius {
1552 field: "loc".into(),
1553 km: 400.0,
1554 };
1555 let spec = candidate_spec(&pred);
1556 let east = loc(70.0, 179.9);
1557 let west = loc(70.0, -179.9);
1558
1559 let mut idx = SideIndex::default();
1560 idx.insert(&spec, 1, &getter(&east));
1561 assert!(
1562 idx.candidates(&spec, &getter(&west)).contains(&1),
1563 "±180 pair at lat 70 must land in the wrapped probe"
1564 );
1565
1566 let sp = |f: &str| east.get(f).cloned();
1567 let dp = |f: &str| west.get(f).cloned();
1568 let score = crate::def::evaluate(
1569 &pred,
1570 &crate::def::NodeView {
1571 key: "e",
1572 props: &sp,
1573 },
1574 &crate::def::NodeView {
1575 key: "w",
1576 props: &dp,
1577 },
1578 );
1579 assert!(
1580 score.is_some(),
1581 "haversine must match across the antimeridian"
1582 );
1583
1584 let paris = loc(48.8566, 2.3522);
1586 let ny = loc(40.7128, -74.0060);
1587 let mut idx2 = SideIndex::default();
1588 idx2.insert(&spec, 4, &getter(&ny));
1589 assert!(
1590 !idx2.candidates(&spec, &getter(&paris)).contains(&4),
1591 "New York still not in the Paris probe after wrap"
1592 );
1593 }
1594
1595 #[test]
1596 fn scan_all_returns_vector_nodes_skips_malformed() {
1597 let pred = Predicate::VectorSimilar {
1598 field: "emb".into(),
1599 min: 0.5,
1600 };
1601 let spec = candidate_spec(&pred);
1602 assert!(matches!(spec, CandidateSpec::ScanAll { field: "emb" }));
1603
1604 let mut idx = SideIndex::default();
1605 idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0])));
1606 idx.insert(&spec, 2, &getter(&emb(&[0.0, 1.0])));
1607 idx.insert(&spec, 3, &getter(&emb(&[1.0, 2.0, 3.0])));
1608 let empty: HashMap<_, _> = [("emb".to_string(), Value::List(vec![]))].into();
1609 let text: HashMap<_, _> =
1610 [("emb".to_string(), Value::List(vec![Value::Str("x".into())]))].into();
1611 let missing: HashMap<String, Value> = HashMap::new();
1612 idx.insert(&spec, 4, &getter(&empty));
1613 idx.insert(&spec, 5, &getter(&text));
1614 idx.insert(&spec, 6, &getter(&missing));
1615
1616 let hits = idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])));
1617 assert_eq!(
1618 hits.into_iter().collect::<Vec<_>>(),
1619 vec![1, 2],
1620 "dim-2 probe must drop the dim-3 member"
1621 );
1622 assert_eq!(
1623 idx.candidates(&spec, &getter(&emb(&[1.0, 2.0, 3.0])))
1624 .into_iter()
1625 .collect::<Vec<_>>(),
1626 vec![3]
1627 );
1628 with_vector_dim_reject(false, || {
1629 assert_eq!(
1630 idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])))
1631 .into_iter()
1632 .collect::<Vec<_>>(),
1633 vec![1, 2, 3],
1634 "unfiltered ScanAll still returns every vector node"
1635 );
1636 });
1637 assert_eq!(idx.vec_dim(1), Some(2));
1638 assert_eq!(idx.vec_dim(3), Some(3));
1639 assert!(idx.vec_meta(1).is_some());
1640 assert!(idx.vec_dim(4).is_none());
1641 assert!(idx.candidates(&spec, &getter(&empty)).is_empty());
1642 assert!(idx.candidates(&spec, &getter(&text)).is_empty());
1643 assert!(idx.candidates(&spec, &getter(&missing)).is_empty());
1644 idx.remove(&spec, 1, &getter(&emb(&[1.0, 0.0])));
1645 assert!(idx.vec_dim(1).is_none());
1646 }
1647
1648 #[test]
1649 fn legacy_specs_probe_keys_equal_index_keys() {
1650 let a: HashMap<_, _> = [
1651 ("ind".to_string(), Value::Str("arch".into())),
1652 (
1653 "tags".to_string(),
1654 Value::List(vec![Value::Str("x".into()), Value::Str("y".into())]),
1655 ),
1656 ("fk".to_string(), Value::Str("c1".into())),
1657 ]
1658 .into();
1659 let get = getter(&a);
1660 for pred in [
1661 Predicate::KeyMatch { field: "fk".into() },
1662 Predicate::FieldEqual {
1663 field: "ind".into(),
1664 },
1665 Predicate::Overlap {
1666 field: "tags".into(),
1667 min: 0.5,
1668 },
1669 ] {
1670 let spec = candidate_spec(&pred);
1671 assert_eq!(
1672 SideIndex::index_keys(&spec, &get),
1673 SideIndex::probe_keys(&spec, &get)
1674 );
1675 }
1676 }
1677
1678 #[test]
1679 fn all_vector_then_field_equal_does_not_scan_all() {
1680 let p = Predicate::All(vec![
1681 Predicate::VectorSimilar {
1682 field: "e".into(),
1683 min: 0.8,
1684 },
1685 Predicate::FieldEqual {
1686 field: "industry".into(),
1687 },
1688 ]);
1689 match candidate_spec(&p) {
1690 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1691 other => panic!("{other:?}"),
1692 }
1693
1694 let spec = candidate_spec(&p);
1695 let mut idx = SideIndex::default();
1696 let mk = |industry: &str, e: &[f64]| {
1697 [
1698 ("industry".to_string(), Value::Str(industry.into())),
1699 (
1700 "e".to_string(),
1701 Value::List(e.iter().copied().map(Value::Float).collect()),
1702 ),
1703 ]
1704 .into()
1705 };
1706 let same: HashMap<_, _> = mk("tech", &[1.0, 0.0]);
1707 let other_ind: HashMap<_, _> = mk("law", &[1.0, 0.0]);
1708 let no_vec: HashMap<_, _> = [("industry".to_string(), Value::Str("tech".into()))].into();
1709 idx.insert(&spec, 1, &getter(&same));
1710 idx.insert(&spec, 2, &getter(&other_ind));
1711 idx.insert(&spec, 3, &getter(&no_vec));
1712
1713 let hits = idx.candidates(&spec, &getter(&same));
1714 assert!(hits.contains(&1), "matching industry must stay a candidate");
1715 assert!(
1716 !hits.contains(&2),
1717 "different industry must not be scanned in via VectorSimilar"
1718 );
1719 assert!(
1720 hits.contains(&3),
1721 "ScanAll is universe: extra Scalar-only candidates are allowed"
1722 );
1723
1724 let empty_ind: HashMap<_, _> = mk("finance", &[1.0, 0.0]);
1725 assert!(
1726 idx.candidates(&spec, &getter(&empty_ind)).is_empty(),
1727 "empty Scalar child → empty intersect"
1728 );
1729 }
1730
1731 #[test]
1732 fn all_approx_vector_then_field_equal_is_intersect() {
1733 let p = Predicate::All(vec![
1734 Predicate::VectorSimilar {
1735 field: "e".into(),
1736 min: 0.8,
1737 },
1738 Predicate::FieldEqual {
1739 field: "industry".into(),
1740 },
1741 ]);
1742 match candidate_spec_approx(&p) {
1743 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1744 other => panic!("{other:?}"),
1745 }
1746
1747 let spec = candidate_spec_approx(&p);
1748 let mut idx = SideIndex::default();
1749 idx.init_hnsw("test-rule");
1751 let mk = |industry: &str, e: &[f64]| {
1752 [
1753 ("industry".to_string(), Value::Str(industry.into())),
1754 (
1755 "e".to_string(),
1756 Value::List(e.iter().copied().map(Value::Float).collect()),
1757 ),
1758 ]
1759 .into()
1760 };
1761 let same: HashMap<_, _> = mk("tech", &[1.0, 0.0]);
1762 let other_ind: HashMap<_, _> = mk("law", &[1.0, 0.0]);
1763 idx.insert(&spec, 1, &getter(&same));
1764 idx.insert(&spec, 2, &getter(&other_ind));
1765 let hits = idx.candidates(&spec, &getter(&same));
1766 assert!(hits.contains(&1), "matching industry must stay a candidate");
1767 assert!(
1768 !hits.contains(&2),
1769 "FieldEqual must be probed on the approximate All path"
1770 );
1771 }
1772
1773 #[test]
1774 fn all_of_scan_all_stays_scan_all() {
1775 let p = Predicate::All(vec![
1776 Predicate::VectorSimilar {
1777 field: "emb".into(),
1778 min: 0.5,
1779 },
1780 Predicate::VectorSimilar {
1781 field: "emb".into(),
1782 min: 0.9,
1783 },
1784 ]);
1785 match candidate_spec(&p) {
1786 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1787 other => panic!("{other:?}"),
1788 }
1789 let spec = candidate_spec(&p);
1790 let mut idx = SideIndex::default();
1791 idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0])));
1792 idx.insert(&spec, 2, &getter(&emb(&[0.0, 1.0])));
1793 let hits = idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])));
1794 assert_eq!(hits.into_iter().collect::<Vec<_>>(), vec![1, 2]);
1795 }
1796
1797 #[test]
1798 fn any_stays_union() {
1799 let p = Predicate::Any(vec![
1800 Predicate::FieldEqual {
1801 field: "industry".into(),
1802 },
1803 Predicate::Overlap {
1804 field: "tags".into(),
1805 min: 0.5,
1806 },
1807 ]);
1808 match candidate_spec(&p) {
1809 CandidateSpec::Union(v) => assert_eq!(v.len(), 2),
1810 other => panic!("{other:?}"),
1811 }
1812 }
1813
1814 #[test]
1817 fn checkpoint_populated_and_consistent_with_norm() {
1818 let pred = Predicate::VectorSimilar {
1819 field: "emb".into(),
1820 min: 0.8,
1821 };
1822 let spec = candidate_spec(&pred);
1823 let xs = [3.0f64, 4.0]; let mut idx = SideIndex::default();
1825 idx.insert(&spec, 1, &getter(&emb(&xs)));
1826
1827 let ckpts = idx
1828 .vec_ckpts(1)
1829 .expect("checkpoints must exist after insert");
1830 let (_, norm) = idx.vec_meta(1).unwrap();
1831 assert!(
1832 (ckpts[0] - norm).abs() < 1e-12,
1833 "ckpts[0] must equal the full L2 norm; got {} vs {}",
1834 ckpts[0],
1835 norm
1836 );
1837 assert!(
1838 (norm - 5.0).abs() < 1e-12,
1839 "norm of [3,4] must be 5.0, got {norm}"
1840 );
1841
1842 idx.remove(&spec, 1, &getter(&emb(&xs)));
1844 assert!(
1845 idx.vec_ckpts(1).is_none(),
1846 "checkpoints must be removed after remove()"
1847 );
1848 }
1849
1850 #[test]
1853 fn fresh_ckpts_for_freshness_gate() {
1854 let pred = Predicate::VectorSimilar {
1855 field: "emb".into(),
1856 min: 0.8,
1857 };
1858 let spec = candidate_spec(&pred);
1859 let xs = [1.0f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1860 let mut idx = SideIndex::default();
1861 idx.insert(&spec, 7, &getter(&emb(&xs)));
1862
1863 let result = idx.fresh_ckpts_for(7, &xs);
1865 assert!(
1866 result.is_some(),
1867 "fresh_ckpts_for must succeed with matching live vector"
1868 );
1869 let (norm, ckpts) = result.unwrap();
1870 assert!((norm - 1.0).abs() < 1e-12);
1871 assert!((ckpts[0] - 1.0).abs() < 1e-12);
1872
1873 let wrong = [2.0f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; assert!(
1876 idx.fresh_ckpts_for(7, &wrong).is_none(),
1877 "freshness gate must reject mismatched norm"
1878 );
1879
1880 let short = [1.0f64, 0.0];
1882 assert!(
1883 idx.fresh_ckpts_for(7, &short).is_none(),
1884 "freshness gate must reject mismatched dim"
1885 );
1886
1887 assert!(idx.fresh_ckpts_for(99, &xs).is_none());
1889 }
1890
1891 #[test]
1894 fn checkpoint_suffix_norms_non_increasing() {
1895 let pred = Predicate::VectorSimilar {
1896 field: "emb".into(),
1897 min: 0.5,
1898 };
1899 let spec = candidate_spec(&pred);
1900 let xs: Vec<f64> = (1..=16).map(|i| i as f64).collect();
1901 let mut idx = SideIndex::default();
1902 idx.insert(&spec, 42, &getter(&emb(&xs)));
1903
1904 let ckpts = *idx.vec_ckpts(42).unwrap();
1905 for c in 0..7 {
1906 assert!(
1907 ckpts[c] >= ckpts[c + 1] - 1e-12,
1908 "suffix norm must be non-increasing: ckpts[{c}]={} < ckpts[{}]={}",
1909 ckpts[c],
1910 c + 1,
1911 ckpts[c + 1]
1912 );
1913 }
1914 let expected_last = (15.0f64 * 15.0 + 16.0 * 16.0).sqrt();
1916 assert!(
1917 (ckpts[7] - expected_last).abs() < 1e-9,
1918 "ckpts[7] should be norm of last segment; got {} vs {}",
1919 ckpts[7],
1920 expected_last
1921 );
1922 }
1923}