1use crate::def::{Predicate, MAX_KEYMATCH_LIST};
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 ScalarOrElements {
349 field: &'a str,
350 },
351 NumericBucket {
352 field: &'a str,
353 tolerance: f64,
354 },
355 GeoGrid {
356 field: &'a str,
357 km: f64,
358 },
359 ScanAll {
360 field: &'a str,
361 },
362 VectorClusters {
369 field: &'a str,
370 min: f64,
371 },
372 Hnsw {
379 field: &'a str,
380 k: usize,
383 },
384 Union(Vec<CandidateSpec<'a>>),
391 Intersect(Vec<CandidateSpec<'a>>),
398}
399
400pub fn candidate_spec(p: &Predicate) -> CandidateSpec<'_> {
413 match p {
414 Predicate::KeyMatch { .. } => CandidateSpec::ByKey,
415 Predicate::FieldEqual { field } => CandidateSpec::Scalar { field },
416 Predicate::Overlap { field, .. } => CandidateSpec::Tokens { field },
417 Predicate::NumericWithin { field, tolerance } => CandidateSpec::NumericBucket {
418 field,
419 tolerance: *tolerance,
420 },
421 Predicate::GeoRadius { field, km } => CandidateSpec::GeoGrid { field, km: *km },
422 Predicate::VectorSimilar { field, .. } => CandidateSpec::ScanAll { field },
423 Predicate::All(parts) => {
424 debug_assert!(
425 !parts.is_empty(),
426 "candidate_spec requires a validated predicate"
427 );
428 CandidateSpec::Intersect(parts.iter().map(candidate_spec).collect())
429 }
430 Predicate::Any(parts) => {
431 debug_assert!(
432 !parts.is_empty(),
433 "candidate_spec requires a validated predicate"
434 );
435 CandidateSpec::Union(parts.iter().map(candidate_spec).collect())
436 }
437 }
438}
439
440pub fn candidate_spec_approx(p: &Predicate) -> CandidateSpec<'_> {
459 candidate_spec_approx_with_k(p, 64)
460}
461
462pub fn candidate_spec_approx_with_k(p: &Predicate, k: usize) -> CandidateSpec<'_> {
464 match p {
465 Predicate::VectorSimilar { field, .. } => CandidateSpec::Hnsw { field, k },
466 Predicate::All(parts) => {
467 debug_assert!(
468 !parts.is_empty(),
469 "candidate_spec_approx requires a validated predicate"
470 );
471 CandidateSpec::Intersect(
472 parts
473 .iter()
474 .map(|p| candidate_spec_approx_with_k(p, k))
475 .collect(),
476 )
477 }
478 other => candidate_spec(other),
479 }
480}
481
482pub(crate) fn as_finite_f64(v: &Value) -> Option<f64> {
483 match v {
484 Value::Int(i) => Some(*i as f64),
485 Value::Float(f) if f.is_finite() => Some(*f),
486 _ => None,
487 }
488}
489
490fn as_latlon(v: &Value) -> Option<(f64, f64)> {
491 let Value::List(items) = v else {
492 return None;
493 };
494 if items.len() != 2 {
495 return None;
496 }
497 let lat = as_finite_f64(&items[0])?;
498 let lon = as_finite_f64(&items[1])?;
499 if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
500 Some((lat, lon))
501 } else {
502 None
503 }
504}
505
506pub(crate) fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
507 let Value::List(items) = v else {
508 return None;
509 };
510 if items.is_empty() {
511 return None;
512 }
513 items.iter().map(as_finite_f64).collect()
514}
515
516fn vec_dim_norm(v: &Value) -> Option<(u32, f64)> {
517 let xs = as_numeric_list(v)?;
518 let mut n2 = 0.0;
519 for x in &xs {
520 n2 += *x * *x;
521 }
522 Some((xs.len() as u32, n2.sqrt()))
523}
524
525fn compute_ckpts(xs: &[f64]) -> [f64; 8] {
531 let dim = xs.len();
532 let mut ckpts = [0.0f64; 8];
533 if dim == 0 {
534 return ckpts;
535 }
536 let boundaries: [usize; 8] = std::array::from_fn(|i| i * dim / 8);
538 let mut suffix_sq = 0.0f64;
539 let mut ci = 7i32;
541 for j in (0..dim).rev() {
542 suffix_sq += xs[j] * xs[j];
543 while ci >= 0 && boundaries[ci as usize] == j {
545 ckpts[ci as usize] = suffix_sq.sqrt();
546 ci -= 1;
547 }
548 }
549 ckpts
550}
551
552fn floor_to_i64(x: f64) -> i64 {
553 let floored = x.floor();
554 if !floored.is_finite() {
555 return 0;
556 }
557 if floored >= i64::MAX as f64 {
558 i64::MAX
559 } else if floored <= i64::MIN as f64 {
560 i64::MIN
561 } else {
562 floored as i64
563 }
564}
565
566fn numeric_index_key(v: f64, tolerance: f64) -> Option<ValueKey> {
570 if !tolerance.is_finite() || tolerance < 0.0 {
571 return None;
572 }
573 if tolerance == 0.0 {
574 let v = if v == 0.0 { 0.0_f64 } else { v };
575 return Some(ValueKey::FloatBits(v.to_bits()));
576 }
577 Some(ValueKey::Int(floor_to_i64(v / tolerance)))
578}
579
580fn numeric_probe_keys(v: f64, tolerance: f64) -> BTreeSet<ValueKey> {
581 match numeric_index_key(v, tolerance) {
582 None => BTreeSet::new(),
583 Some(k @ ValueKey::FloatBits(_)) => BTreeSet::from([k]),
584 Some(ValueKey::Int(b)) => BTreeSet::from([
585 ValueKey::Int(b.saturating_sub(1)),
586 ValueKey::Int(b),
587 ValueKey::Int(b.saturating_add(1)),
588 ]),
589 Some(other) => BTreeSet::from([other]),
590 }
591}
592
593fn geo_cell(lat: f64, lon: f64, km: f64) -> Option<(i64, i64, f64, i64)> {
594 if !km.is_finite() || km <= 0.0 {
595 return None;
596 }
597 let cell_deg = (km / 111.0).max(1e-6);
598 let gx = floor_to_i64(lat / cell_deg);
599 let lon_cells = (360.0 / cell_deg).ceil() as i64;
602 let lon_cells = lon_cells.max(1);
603 let gy = floor_to_i64(lon / cell_deg).rem_euclid(lon_cells);
604 Some((gx, gy, cell_deg, lon_cells))
605}
606
607fn geo_index_key(lat: f64, lon: f64, km: f64) -> Option<ValueKey> {
608 let (gx, gy, _, _) = geo_cell(lat, lon, km)?;
609 Some(ValueKey::Str(format!("{gx}|{gy}")))
610}
611
612fn geo_probe_keys(lat: f64, lon: f64, km: f64) -> BTreeSet<ValueKey> {
613 let Some((gx, gy, cell_deg, lon_cells)) = geo_cell(lat, lon, km) else {
614 return BTreeSet::new();
615 };
616 let cos_lat = lat.to_radians().cos().max(0.05);
618 let n = ((km / (111.0 * cos_lat)) / cell_deg).ceil();
619 let n = if n.is_finite() {
620 floor_to_i64(n).max(0)
621 } else {
622 0
623 };
624 let mut out = BTreeSet::new();
625 for dx in -1..=1 {
626 for dy in -n..=n {
627 let cx = gx.saturating_add(dx);
628 let cy = gy.saturating_add(dy).rem_euclid(lon_cells);
629 out.insert(ValueKey::Str(format!("{cx}|{cy}")));
630 }
631 }
632 out
633}
634
635const SCAN_ALL_SENTINEL: ValueKey = ValueKey::Bool(true);
638
639fn ivf_cluster_key(cluster: usize) -> ValueKey {
642 ValueKey::Str(format!("\u{1}ivf:{cluster}"))
643}
644
645fn spec_is_scan_all_universe(spec: &CandidateSpec<'_>) -> bool {
648 match spec {
649 CandidateSpec::ScanAll { .. } => true,
650 CandidateSpec::Intersect(parts) => {
651 !parts.is_empty() && parts.iter().all(spec_is_scan_all_universe)
652 }
653 _ => false,
654 }
655}
656
657fn spec_is_bykey_external(spec: &CandidateSpec<'_>) -> bool {
660 match spec {
661 CandidateSpec::ByKey => true,
662 CandidateSpec::Intersect(parts) => {
663 !parts.is_empty() && parts.iter().all(spec_is_bykey_external)
664 }
665 _ => false,
666 }
667}
668
669impl SideIndex {
670 fn index_keys(spec: &CandidateSpec, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<ValueKey> {
671 match spec {
672 CandidateSpec::ByKey => BTreeSet::new(),
673 CandidateSpec::Scalar { field } => get(field)
674 .as_ref()
675 .and_then(ValueKey::from_value)
676 .into_iter()
677 .collect(),
678 CandidateSpec::Tokens { field } => get(field)
679 .as_ref()
680 .and_then(list_tokens)
681 .unwrap_or_default(),
682 CandidateSpec::ScalarOrElements { field } => match get(field) {
683 Some(Value::List(items)) => items
684 .iter()
685 .take(MAX_KEYMATCH_LIST)
686 .filter(|v| matches!(v, Value::Str(_)))
687 .filter_map(ValueKey::from_value)
688 .collect(),
689 Some(v) => ValueKey::from_value(&v).into_iter().collect(),
690 None => BTreeSet::new(),
691 },
692 CandidateSpec::NumericBucket { field, tolerance } => get(field)
693 .as_ref()
694 .and_then(as_finite_f64)
695 .and_then(|v| numeric_index_key(v, *tolerance))
696 .into_iter()
697 .collect(),
698 CandidateSpec::GeoGrid { field, km } => get(field)
699 .as_ref()
700 .and_then(as_latlon)
701 .and_then(|(lat, lon)| geo_index_key(lat, lon, *km))
702 .into_iter()
703 .collect(),
704 CandidateSpec::ScanAll { field } => get(field)
705 .as_ref()
706 .and_then(as_numeric_list)
707 .map(|_| SCAN_ALL_SENTINEL)
708 .into_iter()
709 .collect(),
710 CandidateSpec::VectorClusters { .. } => BTreeSet::new(),
715 CandidateSpec::Hnsw { .. } => BTreeSet::new(),
717 CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) => {
721 let mut out = BTreeSet::new();
722 for s in specs {
723 out.extend(Self::index_keys(s, get));
724 }
725 out
726 }
727 }
728 }
729
730 fn probe_keys(spec: &CandidateSpec, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<ValueKey> {
731 match spec {
732 CandidateSpec::ByKey
733 | CandidateSpec::Scalar { .. }
734 | CandidateSpec::Tokens { .. }
735 | CandidateSpec::ScalarOrElements { .. } => Self::index_keys(spec, get),
736 CandidateSpec::NumericBucket { field, tolerance } => get(field)
737 .as_ref()
738 .and_then(as_finite_f64)
739 .map(|v| numeric_probe_keys(v, *tolerance))
740 .unwrap_or_default(),
741 CandidateSpec::GeoGrid { field, km } => get(field)
742 .as_ref()
743 .and_then(as_latlon)
744 .map(|(lat, lon)| geo_probe_keys(lat, lon, *km))
745 .unwrap_or_default(),
746 CandidateSpec::ScanAll { field } => get(field)
747 .as_ref()
748 .and_then(as_numeric_list)
749 .map(|_| SCAN_ALL_SENTINEL)
750 .into_iter()
751 .collect(),
752 CandidateSpec::VectorClusters { .. } => BTreeSet::new(),
754 CandidateSpec::Hnsw { .. } => BTreeSet::new(),
756 CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) => {
759 let mut out = BTreeSet::new();
760 for s in specs {
761 out.extend(Self::probe_keys(s, get));
762 }
763 out
764 }
765 }
766 }
767
768 pub fn insert(&mut self, spec: &CandidateSpec, node: u32, get: &dyn Fn(&str) -> Option<Value>) {
769 if let CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) = spec {
772 for s in specs {
773 self.insert(s, node, get);
774 }
775 return;
776 }
777 if let CandidateSpec::Hnsw { field, .. } = spec {
779 if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
780 self.hnsw_tracked.insert(node);
781 if let Some(h) = &mut self.hnsw {
782 h.insert(node, &xs);
783 }
784 }
785 return;
786 }
787 if let CandidateSpec::VectorClusters { field, .. } = spec {
789 if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
790 self.ivf_raw.insert(node, xs.clone());
791 if !self.ivf_centroids.is_empty() {
792 if let Some(unit) = l2_normalize(&xs) {
794 let c = nearest_centroid(&self.ivf_centroids, &unit);
795 self.ivf_clusters.insert(node, c);
796 self.by_key
797 .entry(ivf_cluster_key(c))
798 .or_default()
799 .insert(node);
800 }
801 self.ivf_drift = self.ivf_drift.saturating_add(1);
802 }
803 }
804 return;
805 }
806
807 for k in Self::index_keys(spec, get) {
808 self.by_key.entry(k).or_default().insert(node);
809 }
810 if let CandidateSpec::ScanAll { field } = spec {
811 if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
812 let mut n2 = 0.0f64;
813 for x in &xs {
814 n2 += x * x;
815 }
816 let norm = n2.sqrt();
817 self.vec_meta.insert(node, (xs.len() as u32, norm));
818 self.vec_checkpoints.insert(node, compute_ckpts(&xs));
819 self.vec_anchor.insert(node, xs[0]);
821 }
822 }
823 }
824
825 pub fn remove(&mut self, spec: &CandidateSpec, node: u32, get: &dyn Fn(&str) -> Option<Value>) {
826 if let CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) = spec {
828 for s in specs {
829 self.remove(s, node, get);
830 }
831 return;
832 }
833 if let CandidateSpec::Hnsw { field, .. } = spec {
838 if get(field).as_ref().and_then(as_numeric_list).is_some() {
839 self.hnsw_tracked.remove(&node);
840 if let Some(h) = &mut self.hnsw {
841 h.remove(node);
842 }
843 self.ivf_drift = self.ivf_drift.saturating_add(1);
844 }
845 return;
846 }
847 if let CandidateSpec::VectorClusters { .. } = spec {
852 if self.ivf_raw.remove(&node).is_some() {
853 self.ivf_drift = self.ivf_drift.saturating_add(1);
854 if let Some(c) = self.ivf_clusters.remove(&node) {
855 let key = ivf_cluster_key(c);
856 if let Some(s) = self.by_key.get_mut(&key) {
857 s.remove(&node);
858 if s.is_empty() {
859 self.by_key.remove(&key);
860 }
861 }
862 }
863 }
864 return;
865 }
866
867 for k in Self::index_keys(spec, get) {
868 if let Some(set) = self.by_key.get_mut(&k) {
869 set.remove(&node);
870 if set.is_empty() {
871 self.by_key.remove(&k);
872 }
873 }
874 }
875 if let CandidateSpec::ScanAll { field } = spec {
876 if get(field).as_ref().and_then(as_numeric_list).is_some() {
877 self.vec_meta.remove(&node);
878 self.vec_checkpoints.remove(&node);
879 self.vec_anchor.remove(&node);
880 }
881 }
882 }
883
884 pub fn vec_dim(&self, node: u32) -> Option<u32> {
886 self.vec_meta.get(&node).map(|(d, _)| *d)
887 }
888
889 pub fn vec_meta(&self, node: u32) -> Option<(u32, f64)> {
891 self.vec_meta.get(&node).copied()
892 }
893
894 pub fn vec_ckpts(&self, node: u32) -> Option<&[f64; 8]> {
896 self.vec_checkpoints.get(&node)
897 }
898
899 pub(crate) fn fresh_ckpts_for<'a>(
926 &'a self,
927 node: u32,
928 live: &[f64],
929 ) -> Option<(f64, &'a [f64; 8])> {
930 let &(dim, norm) = self.vec_meta.get(&node)?;
931 if dim != live.len() as u32 {
932 return None;
933 }
934 let live_norm = {
937 let mut n2 = 0.0f64;
938 for x in live {
939 n2 += x * x;
940 }
941 n2.sqrt()
942 };
943 if norm != live_norm {
944 return None; }
946 let live_anchor = live[0];
950 let &cached_anchor = self.vec_anchor.get(&node)?;
951 if live_anchor != cached_anchor {
952 return None;
953 }
954 let ckpts = self.vec_checkpoints.get(&node)?;
955 Some((norm, ckpts))
956 }
957
958 pub fn candidates(
959 &self,
960 spec: &CandidateSpec,
961 get: &dyn Fn(&str) -> Option<Value>,
962 ) -> BTreeSet<u32> {
963 if let CandidateSpec::Hnsw { field, k } = spec {
965 return self.hnsw_candidates(field, *k, get);
966 }
967 if let CandidateSpec::VectorClusters { field, .. } = spec {
969 return self.ivf_candidates(field, get);
970 }
971 if let CandidateSpec::Union(specs) = spec {
973 return specs.iter().flat_map(|s| self.candidates(s, get)).collect();
974 }
975 if let CandidateSpec::Intersect(specs) = spec {
976 return self.intersect_candidates(specs, get);
977 }
978
979 let mut out = BTreeSet::new();
980 for k in Self::probe_keys(spec, get) {
981 if let Some(set) = self.by_key.get(&k) {
982 out.extend(set.iter().copied());
983 }
984 }
985 if vector_dim_reject_enabled() {
987 if let CandidateSpec::ScanAll { field } = spec {
988 if let Some((dim, _)) = get(field).as_ref().and_then(vec_dim_norm) {
989 out.retain(|id| self.vec_meta.get(id).is_none_or(|(d, _)| *d == dim));
990 }
991 }
992 }
993 out
994 }
995
996 fn intersect_candidates(
1000 &self,
1001 specs: &[CandidateSpec<'_>],
1002 get: &dyn Fn(&str) -> Option<Value>,
1003 ) -> BTreeSet<u32> {
1004 let mut restrictive = Vec::new();
1005 let mut scan_alls = Vec::new();
1006 for s in specs {
1007 if spec_is_scan_all_universe(s) {
1008 scan_alls.push(s);
1009 } else if spec_is_bykey_external(s) {
1010 continue;
1011 } else {
1012 restrictive.push(s);
1013 }
1014 }
1015 let to_intersect: &[&CandidateSpec<'_>] = if !restrictive.is_empty() {
1016 &restrictive
1017 } else if !scan_alls.is_empty() {
1018 &scan_alls
1019 } else {
1020 return BTreeSet::new();
1021 };
1022 let mut iter = to_intersect.iter();
1023 let Some(first) = iter.next() else {
1024 return BTreeSet::new();
1025 };
1026 let mut acc = self.candidates(first, get);
1027 if acc.is_empty() {
1028 return acc;
1029 }
1030 for s in iter {
1031 let other = self.candidates(s, get);
1032 if other.is_empty() {
1033 return BTreeSet::new();
1034 }
1035 acc = acc.intersection(&other).copied().collect();
1036 if acc.is_empty() {
1037 return acc;
1038 }
1039 }
1040 acc
1041 }
1042
1043 fn ivf_candidates(&self, field: &str, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<u32> {
1046 let Some(xs) = get(field).as_ref().and_then(as_numeric_list) else {
1047 return BTreeSet::new();
1048 };
1049 if self.ivf_centroids.is_empty() {
1050 return self.ivf_raw.keys().copied().collect();
1054 }
1055 if self.ivf_raw.len() <= self.ivf_centroids.len() {
1060 return self.ivf_raw.keys().copied().collect();
1061 }
1062 let k = self.ivf_centroids.len();
1063 let p = probe_count(k);
1064
1065 let Some(xs) = l2_normalize(&xs) else {
1068 return BTreeSet::new();
1069 };
1070
1071 let mut dists: Vec<(usize, f64)> = self
1073 .ivf_centroids
1074 .iter()
1075 .enumerate()
1076 .map(|(i, c)| (i, l2_sq(&xs, c)))
1077 .collect();
1078 dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1079
1080 let mut out = BTreeSet::new();
1081 for (ci, _) in dists.iter().take(p) {
1082 let key = ivf_cluster_key(*ci);
1083 if let Some(nodes) = self.by_key.get(&key) {
1084 out.extend(nodes.iter().copied());
1085 }
1086 }
1087 out
1088 }
1089
1090 pub fn fit_ivf_clusters(&mut self, rule_name: &str) {
1101 if self.ivf_raw.is_empty() {
1102 self.ivf_centroids.clear();
1103 self.ivf_clusters.clear();
1104 self.ivf_drift = 0;
1105 return;
1106 }
1107
1108 for c in self.ivf_clusters.values() {
1110 self.by_key.remove(&ivf_cluster_key(*c));
1111 }
1112 self.ivf_clusters.clear();
1113
1114 let vecs: Vec<(u32, Vec<f64>)> = self
1116 .ivf_raw
1117 .iter()
1118 .map(|(&id, xs)| (id, xs.clone()))
1119 .collect();
1120
1121 let n = vecs.len();
1122 let k = cluster_k(n);
1123 let seed = fnv1a_u64(rule_name.as_bytes());
1124
1125 self.ivf_centroids = kmeans_fit(&vecs, k, seed);
1126
1127 for (node, xs) in &vecs {
1129 let Some(unit) = l2_normalize(xs) else {
1130 continue;
1131 };
1132 let c = nearest_centroid(&self.ivf_centroids, &unit);
1133 self.ivf_clusters.insert(*node, c);
1134 self.by_key
1135 .entry(ivf_cluster_key(c))
1136 .or_default()
1137 .insert(*node);
1138 }
1139 self.ivf_drift = 0;
1140 }
1141
1142 pub fn ivf_k(&self) -> usize {
1144 self.ivf_centroids.len()
1145 }
1146
1147 pub fn ivf_cluster_of(&self, node: u32) -> Option<usize> {
1149 self.ivf_clusters.get(&node).copied()
1150 }
1151
1152 pub fn export_ivf_state(&self) -> (Vec<Vec<f64>>, BTreeMap<u32, usize>, u64) {
1157 (
1158 self.ivf_centroids.clone(),
1159 self.ivf_clusters.clone(),
1160 self.ivf_drift,
1161 )
1162 }
1163
1164 pub fn load_ivf_state(
1176 &mut self,
1177 centroids: Vec<Vec<f64>>,
1178 clusters: BTreeMap<u32, usize>,
1179 drift: u64,
1180 ) {
1181 for c in self.ivf_clusters.values() {
1184 self.by_key.remove(&ivf_cluster_key(*c));
1185 }
1186 self.ivf_clusters.clear();
1187
1188 self.ivf_centroids = centroids;
1189 self.ivf_drift = drift;
1190
1191 for (&node, &c) in &clusters {
1193 if !self.ivf_raw.contains_key(&node) {
1194 continue;
1196 }
1197 self.ivf_clusters.insert(node, c);
1198 self.by_key
1199 .entry(ivf_cluster_key(c))
1200 .or_default()
1201 .insert(node);
1202 }
1203 }
1204
1205 pub fn init_hnsw(&mut self, rule_name: &str) {
1214 let seed = fnv1a_u64(rule_name.as_bytes());
1215 self.hnsw = Some(HnswIndex::new(seed));
1216 }
1217
1218 fn hnsw_candidates(
1223 &self,
1224 field: &str,
1225 k: usize,
1226 get: &dyn Fn(&str) -> Option<Value>,
1227 ) -> BTreeSet<u32> {
1228 let Some(xs) = get(field).as_ref().and_then(as_numeric_list) else {
1229 return BTreeSet::new();
1230 };
1231 if let Some(h) = &self.hnsw {
1232 if !h.is_empty() {
1233 return h.search(&xs, k).into_iter().map(|(id, _)| id).collect();
1234 }
1235 }
1236 self.hnsw_tracked.clone()
1238 }
1239
1240 pub fn export_hnsw_blob(&self) -> Vec<u8> {
1244 self.hnsw
1245 .as_ref()
1246 .and_then(|h| bincode::serialize(h).ok())
1247 .unwrap_or_default()
1248 }
1249
1250 pub fn load_hnsw_blob(&mut self, blob: &[u8]) {
1256 if blob.is_empty() {
1257 return;
1258 }
1259 if let Ok(h) = bincode::deserialize::<HnswIndex>(blob) {
1260 self.hnsw_tracked = h.node_ids();
1262 self.hnsw = Some(h);
1263 }
1264 }
1265
1266 pub fn has_hnsw(&self) -> bool {
1268 self.hnsw.as_ref().is_some_and(|h| !h.is_empty())
1269 }
1270
1271 pub fn hnsw_ref(&self) -> Option<&HnswIndex> {
1273 self.hnsw.as_ref()
1274 }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279 use super::*;
1280 use crate::def::Predicate;
1281 use core_storage::Value;
1282 use std::collections::{BTreeMap, HashMap};
1283
1284 fn getter(map: &HashMap<String, Value>) -> impl Fn(&str) -> Option<Value> + '_ {
1285 move |f: &str| map.get(f).cloned()
1286 }
1287
1288 #[test]
1289 fn kmeans_centroids_are_unit_norm() {
1290 let vecs = vec![(0, vec![3.0, 0.0, 0.0]), (1, vec![0.0, 4.0, 0.0])];
1291 let cents = kmeans_fit(&vecs, 2, 1);
1292 for c in cents {
1293 let n = c.iter().map(|x| x * x).sum::<f64>().sqrt();
1294 assert!((n - 1.0).abs() < 1e-9, "{n}");
1295 }
1296 }
1297
1298 #[test]
1305 fn scaled_vector_joins_same_ivf_cluster_as_unit() {
1306 let spec = CandidateSpec::VectorClusters {
1308 field: "emb",
1309 min: 0.5,
1310 };
1311 let mut idx = SideIndex::default();
1312 idx.load_ivf_state(
1313 vec![vec![1.0, 0.0, 0.0], vec![2.5, 0.1, 0.0]],
1314 BTreeMap::new(),
1315 0,
1316 );
1317 idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0, 0.0])));
1318 idx.insert(&spec, 2, &getter(&emb(&[3.0, 0.0, 0.0])));
1319 assert_eq!(
1320 idx.ivf_cluster_of(1),
1321 idx.ivf_cluster_of(2),
1322 "scale-equivalent vectors must share an IVF cluster; got {:?} vs {:?}",
1323 idx.ivf_cluster_of(1),
1324 idx.ivf_cluster_of(2)
1325 );
1326 assert_eq!(idx.ivf_cluster_of(1), Some(0));
1327 }
1328
1329 #[test]
1330 fn scalar_index_buckets_by_value() {
1331 let pred = Predicate::FieldEqual {
1332 field: "ind".into(),
1333 };
1334 let spec = candidate_spec(&pred);
1335 let mut idx = SideIndex::default();
1336 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
1337 let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
1338 idx.insert(&spec, 1, &getter(&a));
1339 idx.insert(&spec, 2, &getter(&b));
1340 idx.insert(&spec, 3, &getter(&a));
1341 let c = idx.candidates(&spec, &getter(&a));
1342 assert_eq!(c.into_iter().collect::<Vec<_>>(), vec![1, 3]);
1343 idx.remove(&spec, 3, &getter(&a));
1344 assert_eq!(idx.candidates(&spec, &getter(&a)).len(), 1);
1345 let empty: HashMap<String, Value> = HashMap::new();
1347 idx.insert(&spec, 9, &getter(&empty));
1348 assert!(idx.candidates(&spec, &getter(&empty)).is_empty());
1349 }
1350
1351 #[test]
1352 fn token_index_unions_buckets() {
1353 let mk =
1354 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1355 let pred = Predicate::Overlap {
1356 field: "tags".into(),
1357 min: 0.5,
1358 };
1359 let spec = candidate_spec(&pred);
1360 let mut idx = SideIndex::default();
1361 let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
1362 let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
1363 let c: HashMap<_, _> = [("tags".to_string(), mk(&["q"]))].into();
1364 idx.insert(&spec, 1, &getter(&a));
1365 idx.insert(&spec, 2, &getter(&b));
1366 idx.insert(&spec, 3, &getter(&c));
1367 let probe: HashMap<_, _> = [("tags".to_string(), mk(&["y"]))].into();
1368 assert_eq!(
1369 idx.candidates(&spec, &getter(&probe))
1370 .into_iter()
1371 .collect::<Vec<_>>(),
1372 vec![1, 2]
1373 );
1374 idx.remove(&spec, 2, &getter(&b));
1375 assert_eq!(
1376 idx.candidates(&spec, &getter(&probe))
1377 .into_iter()
1378 .collect::<Vec<_>>(),
1379 vec![1]
1380 );
1381 }
1382
1383 #[test]
1384 fn all_intersects_parts_and_bykey_indexes_nothing() {
1385 let all = Predicate::All(vec![
1386 Predicate::FieldEqual {
1387 field: "ind".into(),
1388 },
1389 Predicate::Overlap {
1390 field: "tags".into(),
1391 min: 0.5,
1392 },
1393 ]);
1394 match candidate_spec(&all) {
1395 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1396 other => panic!("{other:?}"),
1397 }
1398 let km = Predicate::KeyMatch { field: "fk".into() };
1399 assert!(matches!(candidate_spec(&km), CandidateSpec::ByKey));
1400 let mut idx = SideIndex::default();
1401 let a: HashMap<_, _> = [("fk".to_string(), Value::Str("c1".into()))].into();
1402 idx.insert(&candidate_spec(&km), 1, &getter(&a));
1403 assert!(idx.candidates(&candidate_spec(&km), &getter(&a)).is_empty());
1404 }
1405
1406 fn year(v: Value) -> HashMap<String, Value> {
1407 [("year".to_string(), v)].into()
1408 }
1409
1410 fn loc(lat: f64, lon: f64) -> HashMap<String, Value> {
1411 [(
1412 "loc".to_string(),
1413 Value::List(vec![Value::Float(lat), Value::Float(lon)]),
1414 )]
1415 .into()
1416 }
1417
1418 fn emb(vals: &[f64]) -> HashMap<String, Value> {
1419 [(
1420 "emb".to_string(),
1421 Value::List(vals.iter().copied().map(Value::Float).collect()),
1422 )]
1423 .into()
1424 }
1425
1426 fn bucket_int(spec: &CandidateSpec, map: &HashMap<String, Value>) -> Option<i64> {
1427 match SideIndex::index_keys(spec, &getter(map)).into_iter().next() {
1428 Some(ValueKey::Int(b)) => Some(b),
1429 _ => None,
1430 }
1431 }
1432
1433 #[test]
1434 fn numeric_bucket_adjacency_and_far_value() {
1435 let pred = Predicate::NumericWithin {
1436 field: "year".into(),
1437 tolerance: 2.0,
1438 };
1439 let spec = candidate_spec(&pred);
1440 assert!(matches!(
1441 spec,
1442 CandidateSpec::NumericBucket {
1443 field: "year",
1444 tolerance
1445 } if tolerance == 2.0
1446 ));
1447
1448 let v10 = year(Value::Float(10.0));
1449 let v119 = year(Value::Float(11.9));
1450 let v99 = year(Value::Float(9.9));
1451 let v141 = year(Value::Float(14.1));
1452
1453 let b10 = bucket_int(&spec, &v10).unwrap();
1454 let b119 = bucket_int(&spec, &v119).unwrap();
1455 let b99 = bucket_int(&spec, &v99).unwrap();
1456 assert!((b10 - b119).abs() <= 1);
1458 assert!((b10 - b99).abs() <= 1);
1459
1460 let mut idx = SideIndex::default();
1461 idx.insert(&spec, 1, &getter(&v10));
1462 idx.insert(&spec, 2, &getter(&v119));
1463 idx.insert(&spec, 3, &getter(&v141));
1464 idx.insert(&spec, 4, &getter(&v99));
1465 let hits = idx.candidates(&spec, &getter(&v10));
1466 assert_eq!(hits.into_iter().collect::<Vec<_>>(), vec![1, 2, 4]);
1467 }
1468
1469 #[test]
1470 fn numeric_tol_zero_int_float_collide() {
1471 let pred = Predicate::NumericWithin {
1472 field: "year".into(),
1473 tolerance: 0.0,
1474 };
1475 let spec = candidate_spec(&pred);
1476 let mut idx = SideIndex::default();
1477 idx.insert(&spec, 1, &getter(&year(Value::Int(2))));
1478 assert_eq!(
1479 idx.candidates(&spec, &getter(&year(Value::Float(2.0))))
1480 .into_iter()
1481 .collect::<Vec<_>>(),
1482 vec![1]
1483 );
1484 assert!(idx
1485 .candidates(&spec, &getter(&year(Value::Float(2.1))))
1486 .is_empty());
1487 }
1488
1489 #[test]
1490 fn numeric_tol_zero_signed_zero_collides() {
1491 let pred = Predicate::NumericWithin {
1492 field: "year".into(),
1493 tolerance: 0.0,
1494 };
1495 let spec = candidate_spec(&pred);
1496 let neg = year(Value::Float(-0.0));
1497 let pos = year(Value::Float(0.0));
1498 let mut idx = SideIndex::default();
1499 idx.insert(&spec, 1, &getter(&neg));
1500 assert_eq!(
1501 idx.candidates(&spec, &getter(&pos))
1502 .into_iter()
1503 .collect::<Vec<_>>(),
1504 vec![1]
1505 );
1506 let mut idx2 = SideIndex::default();
1507 idx2.insert(&spec, 2, &getter(&pos));
1508 assert_eq!(
1509 idx2.candidates(&spec, &getter(&neg))
1510 .into_iter()
1511 .collect::<Vec<_>>(),
1512 vec![2]
1513 );
1514 }
1515
1516 #[test]
1517 fn geo_grid_same_cell_cross_cell_and_far_city() {
1518 let pred = Predicate::GeoRadius {
1519 field: "loc".into(),
1520 km: 400.0,
1521 };
1522 let spec = candidate_spec(&pred);
1523 assert!(matches!(
1524 spec,
1525 CandidateSpec::GeoGrid {
1526 field: "loc",
1527 km
1528 } if km == 400.0
1529 ));
1530
1531 let paris = loc(48.8566, 2.3522);
1532 let london = loc(51.5074, -0.1278);
1533 let nearby = loc(48.9, 2.4); let ny = loc(40.7128, -74.0060);
1535
1536 let mut idx = SideIndex::default();
1537 idx.insert(&spec, 1, &getter(&paris));
1538 idx.insert(&spec, 2, &getter(&london));
1539 idx.insert(&spec, 3, &getter(&nearby));
1540 idx.insert(&spec, 4, &getter(&ny));
1541
1542 let from_paris = idx.candidates(&spec, &getter(&paris));
1543 assert!(from_paris.contains(&1), "same-cell self");
1544 assert!(from_paris.contains(&3), "same-cell neighbor");
1545 assert!(from_paris.contains(&2), "cross-cell Paris↔London ~343.5 km");
1546 assert!(!from_paris.contains(&4), "New York not in 400 km probe");
1547 }
1548
1549 #[test]
1550 fn geo_grid_high_latitude_probe_is_superset() {
1551 let pred = Predicate::GeoRadius {
1552 field: "loc".into(),
1553 km: 340.0,
1554 };
1555 let spec = candidate_spec(&pred);
1556 let reyk = loc(64.1466, -21.9426);
1557 let lat = 64.0_f64;
1558 let dlon = 300.0 / (111.0 * lat.to_radians().cos());
1559 let east = loc(lat, -21.9426 + dlon);
1560
1561 let mut idx = SideIndex::default();
1562 idx.insert(&spec, 1, &getter(&reyk));
1563 idx.insert(&spec, 2, &getter(&east));
1564 let hits = idx.candidates(&spec, &getter(&reyk));
1565 assert!(
1566 hits.contains(&2),
1567 "300 km east of Reykjavik must stay in the high-lat probe"
1568 );
1569 }
1570
1571 #[test]
1572 fn geo_grid_antimeridian_wrap_and_evaluate_agree() {
1573 let pred = Predicate::GeoRadius {
1574 field: "loc".into(),
1575 km: 400.0,
1576 };
1577 let spec = candidate_spec(&pred);
1578 let east = loc(70.0, 179.9);
1579 let west = loc(70.0, -179.9);
1580
1581 let mut idx = SideIndex::default();
1582 idx.insert(&spec, 1, &getter(&east));
1583 assert!(
1584 idx.candidates(&spec, &getter(&west)).contains(&1),
1585 "±180 pair at lat 70 must land in the wrapped probe"
1586 );
1587
1588 let sp = |f: &str| east.get(f).cloned();
1589 let dp = |f: &str| west.get(f).cloned();
1590 let score = crate::def::evaluate(
1591 &pred,
1592 &crate::def::NodeView {
1593 key: "e",
1594 props: &sp,
1595 },
1596 &crate::def::NodeView {
1597 key: "w",
1598 props: &dp,
1599 },
1600 );
1601 assert!(
1602 score.is_some(),
1603 "haversine must match across the antimeridian"
1604 );
1605
1606 let paris = loc(48.8566, 2.3522);
1608 let ny = loc(40.7128, -74.0060);
1609 let mut idx2 = SideIndex::default();
1610 idx2.insert(&spec, 4, &getter(&ny));
1611 assert!(
1612 !idx2.candidates(&spec, &getter(&paris)).contains(&4),
1613 "New York still not in the Paris probe after wrap"
1614 );
1615 }
1616
1617 #[test]
1618 fn scan_all_returns_vector_nodes_skips_malformed() {
1619 let pred = Predicate::VectorSimilar {
1620 field: "emb".into(),
1621 min: 0.5,
1622 };
1623 let spec = candidate_spec(&pred);
1624 assert!(matches!(spec, CandidateSpec::ScanAll { field: "emb" }));
1625
1626 let mut idx = SideIndex::default();
1627 idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0])));
1628 idx.insert(&spec, 2, &getter(&emb(&[0.0, 1.0])));
1629 idx.insert(&spec, 3, &getter(&emb(&[1.0, 2.0, 3.0])));
1630 let empty: HashMap<_, _> = [("emb".to_string(), Value::List(vec![]))].into();
1631 let text: HashMap<_, _> =
1632 [("emb".to_string(), Value::List(vec![Value::Str("x".into())]))].into();
1633 let missing: HashMap<String, Value> = HashMap::new();
1634 idx.insert(&spec, 4, &getter(&empty));
1635 idx.insert(&spec, 5, &getter(&text));
1636 idx.insert(&spec, 6, &getter(&missing));
1637
1638 let hits = idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])));
1639 assert_eq!(
1640 hits.into_iter().collect::<Vec<_>>(),
1641 vec![1, 2],
1642 "dim-2 probe must drop the dim-3 member"
1643 );
1644 assert_eq!(
1645 idx.candidates(&spec, &getter(&emb(&[1.0, 2.0, 3.0])))
1646 .into_iter()
1647 .collect::<Vec<_>>(),
1648 vec![3]
1649 );
1650 with_vector_dim_reject(false, || {
1651 assert_eq!(
1652 idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])))
1653 .into_iter()
1654 .collect::<Vec<_>>(),
1655 vec![1, 2, 3],
1656 "unfiltered ScanAll still returns every vector node"
1657 );
1658 });
1659 assert_eq!(idx.vec_dim(1), Some(2));
1660 assert_eq!(idx.vec_dim(3), Some(3));
1661 assert!(idx.vec_meta(1).is_some());
1662 assert!(idx.vec_dim(4).is_none());
1663 assert!(idx.candidates(&spec, &getter(&empty)).is_empty());
1664 assert!(idx.candidates(&spec, &getter(&text)).is_empty());
1665 assert!(idx.candidates(&spec, &getter(&missing)).is_empty());
1666 idx.remove(&spec, 1, &getter(&emb(&[1.0, 0.0])));
1667 assert!(idx.vec_dim(1).is_none());
1668 }
1669
1670 #[test]
1671 fn legacy_specs_probe_keys_equal_index_keys() {
1672 let a: HashMap<_, _> = [
1673 ("ind".to_string(), Value::Str("arch".into())),
1674 (
1675 "tags".to_string(),
1676 Value::List(vec![Value::Str("x".into()), Value::Str("y".into())]),
1677 ),
1678 ("fk".to_string(), Value::Str("c1".into())),
1679 ]
1680 .into();
1681 let get = getter(&a);
1682 for pred in [
1683 Predicate::KeyMatch { field: "fk".into() },
1684 Predicate::FieldEqual {
1685 field: "ind".into(),
1686 },
1687 Predicate::Overlap {
1688 field: "tags".into(),
1689 min: 0.5,
1690 },
1691 ] {
1692 let spec = candidate_spec(&pred);
1693 assert_eq!(
1694 SideIndex::index_keys(&spec, &get),
1695 SideIndex::probe_keys(&spec, &get)
1696 );
1697 }
1698 }
1699
1700 #[test]
1701 fn all_vector_then_field_equal_does_not_scan_all() {
1702 let p = Predicate::All(vec![
1703 Predicate::VectorSimilar {
1704 field: "e".into(),
1705 min: 0.8,
1706 },
1707 Predicate::FieldEqual {
1708 field: "industry".into(),
1709 },
1710 ]);
1711 match candidate_spec(&p) {
1712 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1713 other => panic!("{other:?}"),
1714 }
1715
1716 let spec = candidate_spec(&p);
1717 let mut idx = SideIndex::default();
1718 let mk = |industry: &str, e: &[f64]| {
1719 [
1720 ("industry".to_string(), Value::Str(industry.into())),
1721 (
1722 "e".to_string(),
1723 Value::List(e.iter().copied().map(Value::Float).collect()),
1724 ),
1725 ]
1726 .into()
1727 };
1728 let same: HashMap<_, _> = mk("tech", &[1.0, 0.0]);
1729 let other_ind: HashMap<_, _> = mk("law", &[1.0, 0.0]);
1730 let no_vec: HashMap<_, _> = [("industry".to_string(), Value::Str("tech".into()))].into();
1731 idx.insert(&spec, 1, &getter(&same));
1732 idx.insert(&spec, 2, &getter(&other_ind));
1733 idx.insert(&spec, 3, &getter(&no_vec));
1734
1735 let hits = idx.candidates(&spec, &getter(&same));
1736 assert!(hits.contains(&1), "matching industry must stay a candidate");
1737 assert!(
1738 !hits.contains(&2),
1739 "different industry must not be scanned in via VectorSimilar"
1740 );
1741 assert!(
1742 hits.contains(&3),
1743 "ScanAll is universe: extra Scalar-only candidates are allowed"
1744 );
1745
1746 let empty_ind: HashMap<_, _> = mk("finance", &[1.0, 0.0]);
1747 assert!(
1748 idx.candidates(&spec, &getter(&empty_ind)).is_empty(),
1749 "empty Scalar child → empty intersect"
1750 );
1751 }
1752
1753 #[test]
1754 fn all_approx_vector_then_field_equal_is_intersect() {
1755 let p = Predicate::All(vec![
1756 Predicate::VectorSimilar {
1757 field: "e".into(),
1758 min: 0.8,
1759 },
1760 Predicate::FieldEqual {
1761 field: "industry".into(),
1762 },
1763 ]);
1764 match candidate_spec_approx(&p) {
1765 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1766 other => panic!("{other:?}"),
1767 }
1768
1769 let spec = candidate_spec_approx(&p);
1770 let mut idx = SideIndex::default();
1771 idx.init_hnsw("test-rule");
1773 let mk = |industry: &str, e: &[f64]| {
1774 [
1775 ("industry".to_string(), Value::Str(industry.into())),
1776 (
1777 "e".to_string(),
1778 Value::List(e.iter().copied().map(Value::Float).collect()),
1779 ),
1780 ]
1781 .into()
1782 };
1783 let same: HashMap<_, _> = mk("tech", &[1.0, 0.0]);
1784 let other_ind: HashMap<_, _> = mk("law", &[1.0, 0.0]);
1785 idx.insert(&spec, 1, &getter(&same));
1786 idx.insert(&spec, 2, &getter(&other_ind));
1787 let hits = idx.candidates(&spec, &getter(&same));
1788 assert!(hits.contains(&1), "matching industry must stay a candidate");
1789 assert!(
1790 !hits.contains(&2),
1791 "FieldEqual must be probed on the approximate All path"
1792 );
1793 }
1794
1795 #[test]
1796 fn all_of_scan_all_stays_scan_all() {
1797 let p = Predicate::All(vec![
1798 Predicate::VectorSimilar {
1799 field: "emb".into(),
1800 min: 0.5,
1801 },
1802 Predicate::VectorSimilar {
1803 field: "emb".into(),
1804 min: 0.9,
1805 },
1806 ]);
1807 match candidate_spec(&p) {
1808 CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
1809 other => panic!("{other:?}"),
1810 }
1811 let spec = candidate_spec(&p);
1812 let mut idx = SideIndex::default();
1813 idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0])));
1814 idx.insert(&spec, 2, &getter(&emb(&[0.0, 1.0])));
1815 let hits = idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])));
1816 assert_eq!(hits.into_iter().collect::<Vec<_>>(), vec![1, 2]);
1817 }
1818
1819 #[test]
1820 fn any_stays_union() {
1821 let p = Predicate::Any(vec![
1822 Predicate::FieldEqual {
1823 field: "industry".into(),
1824 },
1825 Predicate::Overlap {
1826 field: "tags".into(),
1827 min: 0.5,
1828 },
1829 ]);
1830 match candidate_spec(&p) {
1831 CandidateSpec::Union(v) => assert_eq!(v.len(), 2),
1832 other => panic!("{other:?}"),
1833 }
1834 }
1835
1836 #[test]
1839 fn checkpoint_populated_and_consistent_with_norm() {
1840 let pred = Predicate::VectorSimilar {
1841 field: "emb".into(),
1842 min: 0.8,
1843 };
1844 let spec = candidate_spec(&pred);
1845 let xs = [3.0f64, 4.0]; let mut idx = SideIndex::default();
1847 idx.insert(&spec, 1, &getter(&emb(&xs)));
1848
1849 let ckpts = idx
1850 .vec_ckpts(1)
1851 .expect("checkpoints must exist after insert");
1852 let (_, norm) = idx.vec_meta(1).unwrap();
1853 assert!(
1854 (ckpts[0] - norm).abs() < 1e-12,
1855 "ckpts[0] must equal the full L2 norm; got {} vs {}",
1856 ckpts[0],
1857 norm
1858 );
1859 assert!(
1860 (norm - 5.0).abs() < 1e-12,
1861 "norm of [3,4] must be 5.0, got {norm}"
1862 );
1863
1864 idx.remove(&spec, 1, &getter(&emb(&xs)));
1866 assert!(
1867 idx.vec_ckpts(1).is_none(),
1868 "checkpoints must be removed after remove()"
1869 );
1870 }
1871
1872 #[test]
1875 fn fresh_ckpts_for_freshness_gate() {
1876 let pred = Predicate::VectorSimilar {
1877 field: "emb".into(),
1878 min: 0.8,
1879 };
1880 let spec = candidate_spec(&pred);
1881 let xs = [1.0f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1882 let mut idx = SideIndex::default();
1883 idx.insert(&spec, 7, &getter(&emb(&xs)));
1884
1885 let result = idx.fresh_ckpts_for(7, &xs);
1887 assert!(
1888 result.is_some(),
1889 "fresh_ckpts_for must succeed with matching live vector"
1890 );
1891 let (norm, ckpts) = result.unwrap();
1892 assert!((norm - 1.0).abs() < 1e-12);
1893 assert!((ckpts[0] - 1.0).abs() < 1e-12);
1894
1895 let wrong = [2.0f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; assert!(
1898 idx.fresh_ckpts_for(7, &wrong).is_none(),
1899 "freshness gate must reject mismatched norm"
1900 );
1901
1902 let short = [1.0f64, 0.0];
1904 assert!(
1905 idx.fresh_ckpts_for(7, &short).is_none(),
1906 "freshness gate must reject mismatched dim"
1907 );
1908
1909 assert!(idx.fresh_ckpts_for(99, &xs).is_none());
1911 }
1912
1913 #[test]
1916 fn checkpoint_suffix_norms_non_increasing() {
1917 let pred = Predicate::VectorSimilar {
1918 field: "emb".into(),
1919 min: 0.5,
1920 };
1921 let spec = candidate_spec(&pred);
1922 let xs: Vec<f64> = (1..=16).map(|i| i as f64).collect();
1923 let mut idx = SideIndex::default();
1924 idx.insert(&spec, 42, &getter(&emb(&xs)));
1925
1926 let ckpts = *idx.vec_ckpts(42).unwrap();
1927 for c in 0..7 {
1928 assert!(
1929 ckpts[c] >= ckpts[c + 1] - 1e-12,
1930 "suffix norm must be non-increasing: ckpts[{c}]={} < ckpts[{}]={}",
1931 ckpts[c],
1932 c + 1,
1933 ckpts[c + 1]
1934 );
1935 }
1936 let expected_last = (15.0f64 * 15.0 + 16.0 * 16.0).sqrt();
1938 assert!(
1939 (ckpts[7] - expected_last).abs() < 1e-9,
1940 "ckpts[7] should be norm of last segment; got {} vs {}",
1941 ckpts[7],
1942 expected_last
1943 );
1944 }
1945}