use crate::def::{Predicate, MAX_KEYMATCH_LIST};
use crate::hnsw::HnswIndex;
use core_storage::{list_tokens, Value, ValueKey};
use std::collections::{BTreeMap, BTreeSet};
pub const IVF_K_MIN: usize = 4;
pub const IVF_K_MAX: usize = 1024;
pub const IVF_ITERATIONS: usize = 12;
pub const IVF_PROBE_DENOM: usize = 16;
pub const IVF_DRIFT_REBUILD: u64 = 256;
thread_local! {
static IVF_DRIFT_REBUILD_OVERRIDE: std::cell::Cell<Option<u64>> =
const { std::cell::Cell::new(None) };
}
pub(crate) fn ivf_drift_rebuild_threshold() -> u64 {
IVF_DRIFT_REBUILD_OVERRIDE.with(|c| c.get().unwrap_or(IVF_DRIFT_REBUILD))
}
pub fn with_ivf_drift_rebuild<R>(threshold: u64, f: impl FnOnce() -> R) -> R {
IVF_DRIFT_REBUILD_OVERRIDE.with(|c| {
let prev = c.replace(Some(threshold));
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
c.set(prev);
match out {
Ok(v) => v,
Err(p) => std::panic::resume_unwind(p),
}
})
}
pub fn cluster_k(n: usize) -> usize {
if n == 0 {
return IVF_K_MIN;
}
let k = (n as f64).sqrt().ceil() as usize;
k.clamp(IVF_K_MIN, IVF_K_MAX)
}
pub fn probe_count(k: usize) -> usize {
k.div_ceil(IVF_PROBE_DENOM).max(1)
}
fn l2_normalize(xs: &[f64]) -> Option<Vec<f64>> {
let n = xs.iter().map(|x| x * x).sum::<f64>().sqrt();
if n == 0.0 {
return None;
}
Some(xs.iter().map(|x| x / n).collect())
}
fn l2_sq(a: &[f64], b: &[f64]) -> f64 {
if a.len() != b.len() {
return f64::MAX;
}
a.iter().zip(b.iter()).map(|(x, y)| (x - y) * (x - y)).sum()
}
pub fn nearest_centroid(centroids: &[Vec<f64>], xs: &[f64]) -> usize {
centroids
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
l2_sq(xs, a)
.partial_cmp(&l2_sq(xs, b))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0)
}
pub fn fnv1a_u64(data: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut h = FNV_OFFSET;
for &b in data {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h
}
#[inline]
fn lcg_next(state: u64) -> u64 {
state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407)
}
pub fn kmeans_fit(vecs: &[(u32, Vec<f64>)], k: usize, seed: u64) -> Vec<Vec<f64>> {
let vecs: Vec<(u32, Vec<f64>)> = vecs
.iter()
.filter_map(|(id, xs)| l2_normalize(xs).map(|n| (*id, n)))
.collect();
if vecs.is_empty() || k == 0 {
return vec![];
}
let n = vecs.len();
let k = k.min(n);
let dim = vecs[0].1.len();
if dim == 0 {
return vec![];
}
let mut state = seed;
let mut used = vec![false; n];
let mut init_idxs: Vec<usize> = Vec::with_capacity(k);
let mut attempts = 0usize;
while init_idxs.len() < k && attempts < n * 4 {
state = lcg_next(state);
let idx = (state >> 33) as usize % n;
if !used[idx] {
used[idx] = true;
init_idxs.push(idx);
}
attempts += 1;
}
if init_idxs.len() < k {
for (i, in_use) in used.iter().enumerate().take(n) {
if !in_use {
init_idxs.push(i);
if init_idxs.len() == k {
break;
}
}
}
}
let mut centroids: Vec<Vec<f64>> = init_idxs.iter().map(|&i| vecs[i].1.clone()).collect();
let mut assignments = vec![0usize; n];
for iter in 0..IVF_ITERATIONS {
for (j, (_, xs)) in vecs.iter().enumerate() {
assignments[j] = nearest_centroid(¢roids, xs);
}
let mut sums = vec![vec![0.0f64; dim]; k];
let mut counts = vec![0usize; k];
for (j, (_, xs)) in vecs.iter().enumerate() {
let c = assignments[j];
counts[c] += 1;
for d in 0..dim {
sums[c][d] += xs[d];
}
}
let mut new_centroids = vec![vec![0.0f64; dim]; k];
let mut empty: Vec<usize> = Vec::new();
for c in 0..k {
if counts[c] == 0 {
empty.push(c);
} else {
for d in 0..dim {
new_centroids[c][d] = sums[c][d] / counts[c] as f64;
}
}
}
for (ei, ec) in empty.into_iter().enumerate() {
let reseed =
seed ^ (iter as u64).wrapping_mul(0x9E37) ^ (ei as u64).wrapping_mul(0x1234_5679);
let mut rs = lcg_next(reseed);
rs = lcg_next(rs);
let pick = (rs >> 33) as usize % n;
new_centroids[ec] = vecs[pick].1.clone();
}
centroids = new_centroids;
}
centroids
}
#[cfg(test)]
thread_local! {
static VECTOR_DIM_REJECT: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
static VECTOR_EARLY_EXIT: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
}
fn vector_dim_reject_enabled() -> bool {
#[cfg(test)]
{
VECTOR_DIM_REJECT.with(|c| c.get())
}
#[cfg(not(test))]
{
true
}
}
pub(crate) fn vector_early_exit_enabled() -> bool {
#[cfg(test)]
{
VECTOR_EARLY_EXIT.with(|c| c.get())
}
#[cfg(not(test))]
{
true
}
}
#[cfg(test)]
pub fn with_vector_dim_reject<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
VECTOR_DIM_REJECT.with(|c| {
let prev = c.replace(enabled);
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
c.set(prev);
match out {
Ok(v) => v,
Err(p) => std::panic::resume_unwind(p),
}
})
}
#[cfg(test)]
pub fn with_vector_early_exit<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
VECTOR_EARLY_EXIT.with(|c| {
let prev = c.replace(enabled);
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
c.set(prev);
match out {
Ok(v) => v,
Err(p) => std::panic::resume_unwind(p),
}
})
}
#[derive(Debug, Default)]
pub struct SideIndex {
by_key: BTreeMap<ValueKey, BTreeSet<u32>>,
vec_meta: BTreeMap<u32, (u32, f64)>,
vec_checkpoints: BTreeMap<u32, [f64; 8]>,
vec_anchor: BTreeMap<u32, f64>,
ivf_raw: BTreeMap<u32, Vec<f64>>,
ivf_centroids: Vec<Vec<f64>>,
ivf_clusters: BTreeMap<u32, usize>,
pub ivf_drift: u64,
hnsw: Option<HnswIndex>,
hnsw_tracked: BTreeSet<u32>,
}
#[derive(Debug, Default)]
pub struct RuleIndex {
pub src_side: SideIndex,
pub dst_side: SideIndex,
}
#[derive(Debug)]
pub enum CandidateSpec<'a> {
ByKey,
Scalar {
field: &'a str,
},
Tokens {
field: &'a str,
},
ScalarOrElements {
field: &'a str,
},
NumericBucket {
field: &'a str,
tolerance: f64,
},
GeoGrid {
field: &'a str,
km: f64,
},
ScanAll {
field: &'a str,
},
VectorClusters {
field: &'a str,
min: f64,
},
Hnsw {
field: &'a str,
k: usize,
},
Union(Vec<CandidateSpec<'a>>),
Intersect(Vec<CandidateSpec<'a>>),
}
pub fn candidate_spec(p: &Predicate) -> CandidateSpec<'_> {
match p {
Predicate::KeyMatch { .. } => CandidateSpec::ByKey,
Predicate::FieldEqual { field } => CandidateSpec::Scalar { field },
Predicate::Overlap { field, .. } => CandidateSpec::Tokens { field },
Predicate::NumericWithin { field, tolerance } => CandidateSpec::NumericBucket {
field,
tolerance: *tolerance,
},
Predicate::GeoRadius { field, km } => CandidateSpec::GeoGrid { field, km: *km },
Predicate::VectorSimilar { field, .. } => CandidateSpec::ScanAll { field },
Predicate::All(parts) => {
debug_assert!(
!parts.is_empty(),
"candidate_spec requires a validated predicate"
);
CandidateSpec::Intersect(parts.iter().map(candidate_spec).collect())
}
Predicate::Any(parts) => {
debug_assert!(
!parts.is_empty(),
"candidate_spec requires a validated predicate"
);
CandidateSpec::Union(parts.iter().map(candidate_spec).collect())
}
}
}
pub fn candidate_spec_approx(p: &Predicate) -> CandidateSpec<'_> {
candidate_spec_approx_with_k(p, 64)
}
pub fn candidate_spec_approx_with_k(p: &Predicate, k: usize) -> CandidateSpec<'_> {
match p {
Predicate::VectorSimilar { field, .. } => CandidateSpec::Hnsw { field, k },
Predicate::All(parts) => {
debug_assert!(
!parts.is_empty(),
"candidate_spec_approx requires a validated predicate"
);
CandidateSpec::Intersect(
parts
.iter()
.map(|p| candidate_spec_approx_with_k(p, k))
.collect(),
)
}
other => candidate_spec(other),
}
}
pub(crate) fn as_finite_f64(v: &Value) -> Option<f64> {
match v {
Value::Int(i) => Some(*i as f64),
Value::Float(f) if f.is_finite() => Some(*f),
_ => None,
}
}
fn as_latlon(v: &Value) -> Option<(f64, f64)> {
let Value::List(items) = v else {
return None;
};
if items.len() != 2 {
return None;
}
let lat = as_finite_f64(&items[0])?;
let lon = as_finite_f64(&items[1])?;
if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
Some((lat, lon))
} else {
None
}
}
pub(crate) fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
let Value::List(items) = v else {
return None;
};
if items.is_empty() {
return None;
}
items.iter().map(as_finite_f64).collect()
}
fn vec_dim_norm(v: &Value) -> Option<(u32, f64)> {
let xs = as_numeric_list(v)?;
let mut n2 = 0.0;
for x in &xs {
n2 += *x * *x;
}
Some((xs.len() as u32, n2.sqrt()))
}
fn compute_ckpts(xs: &[f64]) -> [f64; 8] {
let dim = xs.len();
let mut ckpts = [0.0f64; 8];
if dim == 0 {
return ckpts;
}
let boundaries: [usize; 8] = std::array::from_fn(|i| i * dim / 8);
let mut suffix_sq = 0.0f64;
let mut ci = 7i32;
for j in (0..dim).rev() {
suffix_sq += xs[j] * xs[j];
while ci >= 0 && boundaries[ci as usize] == j {
ckpts[ci as usize] = suffix_sq.sqrt();
ci -= 1;
}
}
ckpts
}
fn floor_to_i64(x: f64) -> i64 {
let floored = x.floor();
if !floored.is_finite() {
return 0;
}
if floored >= i64::MAX as f64 {
i64::MAX
} else if floored <= i64::MIN as f64 {
i64::MIN
} else {
floored as i64
}
}
fn numeric_index_key(v: f64, tolerance: f64) -> Option<ValueKey> {
if !tolerance.is_finite() || tolerance < 0.0 {
return None;
}
if tolerance == 0.0 {
let v = if v == 0.0 { 0.0_f64 } else { v };
return Some(ValueKey::FloatBits(v.to_bits()));
}
Some(ValueKey::Int(floor_to_i64(v / tolerance)))
}
fn numeric_probe_keys(v: f64, tolerance: f64) -> BTreeSet<ValueKey> {
match numeric_index_key(v, tolerance) {
None => BTreeSet::new(),
Some(k @ ValueKey::FloatBits(_)) => BTreeSet::from([k]),
Some(ValueKey::Int(b)) => BTreeSet::from([
ValueKey::Int(b.saturating_sub(1)),
ValueKey::Int(b),
ValueKey::Int(b.saturating_add(1)),
]),
Some(other) => BTreeSet::from([other]),
}
}
fn geo_cell(lat: f64, lon: f64, km: f64) -> Option<(i64, i64, f64, i64)> {
if !km.is_finite() || km <= 0.0 {
return None;
}
let cell_deg = (km / 111.0).max(1e-6);
let gx = floor_to_i64(lat / cell_deg);
let lon_cells = (360.0 / cell_deg).ceil() as i64;
let lon_cells = lon_cells.max(1);
let gy = floor_to_i64(lon / cell_deg).rem_euclid(lon_cells);
Some((gx, gy, cell_deg, lon_cells))
}
fn geo_index_key(lat: f64, lon: f64, km: f64) -> Option<ValueKey> {
let (gx, gy, _, _) = geo_cell(lat, lon, km)?;
Some(ValueKey::Str(format!("{gx}|{gy}")))
}
fn geo_probe_keys(lat: f64, lon: f64, km: f64) -> BTreeSet<ValueKey> {
let Some((gx, gy, cell_deg, lon_cells)) = geo_cell(lat, lon, km) else {
return BTreeSet::new();
};
let cos_lat = lat.to_radians().cos().max(0.05);
let n = ((km / (111.0 * cos_lat)) / cell_deg).ceil();
let n = if n.is_finite() {
floor_to_i64(n).max(0)
} else {
0
};
let mut out = BTreeSet::new();
for dx in -1..=1 {
for dy in -n..=n {
let cx = gx.saturating_add(dx);
let cy = gy.saturating_add(dy).rem_euclid(lon_cells);
out.insert(ValueKey::Str(format!("{cx}|{cy}")));
}
}
out
}
const SCAN_ALL_SENTINEL: ValueKey = ValueKey::Bool(true);
fn ivf_cluster_key(cluster: usize) -> ValueKey {
ValueKey::Str(format!("\u{1}ivf:{cluster}"))
}
fn spec_is_scan_all_universe(spec: &CandidateSpec<'_>) -> bool {
match spec {
CandidateSpec::ScanAll { .. } => true,
CandidateSpec::Intersect(parts) => {
!parts.is_empty() && parts.iter().all(spec_is_scan_all_universe)
}
_ => false,
}
}
fn spec_is_bykey_external(spec: &CandidateSpec<'_>) -> bool {
match spec {
CandidateSpec::ByKey => true,
CandidateSpec::Intersect(parts) => {
!parts.is_empty() && parts.iter().all(spec_is_bykey_external)
}
_ => false,
}
}
impl SideIndex {
fn index_keys(spec: &CandidateSpec, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<ValueKey> {
match spec {
CandidateSpec::ByKey => BTreeSet::new(),
CandidateSpec::Scalar { field } => get(field)
.as_ref()
.and_then(ValueKey::from_value)
.into_iter()
.collect(),
CandidateSpec::Tokens { field } => get(field)
.as_ref()
.and_then(list_tokens)
.unwrap_or_default(),
CandidateSpec::ScalarOrElements { field } => match get(field) {
Some(Value::List(items)) => items
.iter()
.take(MAX_KEYMATCH_LIST)
.filter(|v| matches!(v, Value::Str(_)))
.filter_map(ValueKey::from_value)
.collect(),
Some(v) => ValueKey::from_value(&v).into_iter().collect(),
None => BTreeSet::new(),
},
CandidateSpec::NumericBucket { field, tolerance } => get(field)
.as_ref()
.and_then(as_finite_f64)
.and_then(|v| numeric_index_key(v, *tolerance))
.into_iter()
.collect(),
CandidateSpec::GeoGrid { field, km } => get(field)
.as_ref()
.and_then(as_latlon)
.and_then(|(lat, lon)| geo_index_key(lat, lon, *km))
.into_iter()
.collect(),
CandidateSpec::ScanAll { field } => get(field)
.as_ref()
.and_then(as_numeric_list)
.map(|_| SCAN_ALL_SENTINEL)
.into_iter()
.collect(),
CandidateSpec::VectorClusters { .. } => BTreeSet::new(),
CandidateSpec::Hnsw { .. } => BTreeSet::new(),
CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) => {
let mut out = BTreeSet::new();
for s in specs {
out.extend(Self::index_keys(s, get));
}
out
}
}
}
fn probe_keys(spec: &CandidateSpec, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<ValueKey> {
match spec {
CandidateSpec::ByKey
| CandidateSpec::Scalar { .. }
| CandidateSpec::Tokens { .. }
| CandidateSpec::ScalarOrElements { .. } => Self::index_keys(spec, get),
CandidateSpec::NumericBucket { field, tolerance } => get(field)
.as_ref()
.and_then(as_finite_f64)
.map(|v| numeric_probe_keys(v, *tolerance))
.unwrap_or_default(),
CandidateSpec::GeoGrid { field, km } => get(field)
.as_ref()
.and_then(as_latlon)
.map(|(lat, lon)| geo_probe_keys(lat, lon, *km))
.unwrap_or_default(),
CandidateSpec::ScanAll { field } => get(field)
.as_ref()
.and_then(as_numeric_list)
.map(|_| SCAN_ALL_SENTINEL)
.into_iter()
.collect(),
CandidateSpec::VectorClusters { .. } => BTreeSet::new(),
CandidateSpec::Hnsw { .. } => BTreeSet::new(),
CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) => {
let mut out = BTreeSet::new();
for s in specs {
out.extend(Self::probe_keys(s, get));
}
out
}
}
}
pub fn insert(&mut self, spec: &CandidateSpec, node: u32, get: &dyn Fn(&str) -> Option<Value>) {
if let CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) = spec {
for s in specs {
self.insert(s, node, get);
}
return;
}
if let CandidateSpec::Hnsw { field, .. } = spec {
if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
self.hnsw_tracked.insert(node);
if let Some(h) = &mut self.hnsw {
h.insert(node, &xs);
}
}
return;
}
if let CandidateSpec::VectorClusters { field, .. } = spec {
if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
self.ivf_raw.insert(node, xs.clone());
if !self.ivf_centroids.is_empty() {
if let Some(unit) = l2_normalize(&xs) {
let c = nearest_centroid(&self.ivf_centroids, &unit);
self.ivf_clusters.insert(node, c);
self.by_key
.entry(ivf_cluster_key(c))
.or_default()
.insert(node);
}
self.ivf_drift = self.ivf_drift.saturating_add(1);
}
}
return;
}
for k in Self::index_keys(spec, get) {
self.by_key.entry(k).or_default().insert(node);
}
if let CandidateSpec::ScanAll { field } = spec {
if let Some(xs) = get(field).as_ref().and_then(as_numeric_list) {
let mut n2 = 0.0f64;
for x in &xs {
n2 += x * x;
}
let norm = n2.sqrt();
self.vec_meta.insert(node, (xs.len() as u32, norm));
self.vec_checkpoints.insert(node, compute_ckpts(&xs));
self.vec_anchor.insert(node, xs[0]);
}
}
}
pub fn remove(&mut self, spec: &CandidateSpec, node: u32, get: &dyn Fn(&str) -> Option<Value>) {
if let CandidateSpec::Union(specs) | CandidateSpec::Intersect(specs) = spec {
for s in specs {
self.remove(s, node, get);
}
return;
}
if let CandidateSpec::Hnsw { field, .. } = spec {
if get(field).as_ref().and_then(as_numeric_list).is_some() {
self.hnsw_tracked.remove(&node);
if let Some(h) = &mut self.hnsw {
h.remove(node);
}
self.ivf_drift = self.ivf_drift.saturating_add(1);
}
return;
}
if let CandidateSpec::VectorClusters { .. } = spec {
if self.ivf_raw.remove(&node).is_some() {
self.ivf_drift = self.ivf_drift.saturating_add(1);
if let Some(c) = self.ivf_clusters.remove(&node) {
let key = ivf_cluster_key(c);
if let Some(s) = self.by_key.get_mut(&key) {
s.remove(&node);
if s.is_empty() {
self.by_key.remove(&key);
}
}
}
}
return;
}
for k in Self::index_keys(spec, get) {
if let Some(set) = self.by_key.get_mut(&k) {
set.remove(&node);
if set.is_empty() {
self.by_key.remove(&k);
}
}
}
if let CandidateSpec::ScanAll { field } = spec {
if get(field).as_ref().and_then(as_numeric_list).is_some() {
self.vec_meta.remove(&node);
self.vec_checkpoints.remove(&node);
self.vec_anchor.remove(&node);
}
}
}
pub fn vec_dim(&self, node: u32) -> Option<u32> {
self.vec_meta.get(&node).map(|(d, _)| *d)
}
pub fn vec_meta(&self, node: u32) -> Option<(u32, f64)> {
self.vec_meta.get(&node).copied()
}
pub fn vec_ckpts(&self, node: u32) -> Option<&[f64; 8]> {
self.vec_checkpoints.get(&node)
}
pub(crate) fn fresh_ckpts_for<'a>(
&'a self,
node: u32,
live: &[f64],
) -> Option<(f64, &'a [f64; 8])> {
let &(dim, norm) = self.vec_meta.get(&node)?;
if dim != live.len() as u32 {
return None;
}
let live_norm = {
let mut n2 = 0.0f64;
for x in live {
n2 += x * x;
}
n2.sqrt()
};
if norm != live_norm {
return None; }
let live_anchor = live[0];
let &cached_anchor = self.vec_anchor.get(&node)?;
if live_anchor != cached_anchor {
return None;
}
let ckpts = self.vec_checkpoints.get(&node)?;
Some((norm, ckpts))
}
pub fn candidates(
&self,
spec: &CandidateSpec,
get: &dyn Fn(&str) -> Option<Value>,
) -> BTreeSet<u32> {
if let CandidateSpec::Hnsw { field, k } = spec {
return self.hnsw_candidates(field, *k, get);
}
if let CandidateSpec::VectorClusters { field, .. } = spec {
return self.ivf_candidates(field, get);
}
if let CandidateSpec::Union(specs) = spec {
return specs.iter().flat_map(|s| self.candidates(s, get)).collect();
}
if let CandidateSpec::Intersect(specs) = spec {
return self.intersect_candidates(specs, get);
}
let mut out = BTreeSet::new();
for k in Self::probe_keys(spec, get) {
if let Some(set) = self.by_key.get(&k) {
out.extend(set.iter().copied());
}
}
if vector_dim_reject_enabled() {
if let CandidateSpec::ScanAll { field } = spec {
if let Some((dim, _)) = get(field).as_ref().and_then(vec_dim_norm) {
out.retain(|id| self.vec_meta.get(id).is_none_or(|(d, _)| *d == dim));
}
}
}
out
}
fn intersect_candidates(
&self,
specs: &[CandidateSpec<'_>],
get: &dyn Fn(&str) -> Option<Value>,
) -> BTreeSet<u32> {
let mut restrictive = Vec::new();
let mut scan_alls = Vec::new();
for s in specs {
if spec_is_scan_all_universe(s) {
scan_alls.push(s);
} else if spec_is_bykey_external(s) {
continue;
} else {
restrictive.push(s);
}
}
let to_intersect: &[&CandidateSpec<'_>] = if !restrictive.is_empty() {
&restrictive
} else if !scan_alls.is_empty() {
&scan_alls
} else {
return BTreeSet::new();
};
let mut iter = to_intersect.iter();
let Some(first) = iter.next() else {
return BTreeSet::new();
};
let mut acc = self.candidates(first, get);
if acc.is_empty() {
return acc;
}
for s in iter {
let other = self.candidates(s, get);
if other.is_empty() {
return BTreeSet::new();
}
acc = acc.intersection(&other).copied().collect();
if acc.is_empty() {
return acc;
}
}
acc
}
fn ivf_candidates(&self, field: &str, get: &dyn Fn(&str) -> Option<Value>) -> BTreeSet<u32> {
let Some(xs) = get(field).as_ref().and_then(as_numeric_list) else {
return BTreeSet::new();
};
if self.ivf_centroids.is_empty() {
return self.ivf_raw.keys().copied().collect();
}
if self.ivf_raw.len() <= self.ivf_centroids.len() {
return self.ivf_raw.keys().copied().collect();
}
let k = self.ivf_centroids.len();
let p = probe_count(k);
let Some(xs) = l2_normalize(&xs) else {
return BTreeSet::new();
};
let mut dists: Vec<(usize, f64)> = self
.ivf_centroids
.iter()
.enumerate()
.map(|(i, c)| (i, l2_sq(&xs, c)))
.collect();
dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut out = BTreeSet::new();
for (ci, _) in dists.iter().take(p) {
let key = ivf_cluster_key(*ci);
if let Some(nodes) = self.by_key.get(&key) {
out.extend(nodes.iter().copied());
}
}
out
}
pub fn fit_ivf_clusters(&mut self, rule_name: &str) {
if self.ivf_raw.is_empty() {
self.ivf_centroids.clear();
self.ivf_clusters.clear();
self.ivf_drift = 0;
return;
}
for c in self.ivf_clusters.values() {
self.by_key.remove(&ivf_cluster_key(*c));
}
self.ivf_clusters.clear();
let vecs: Vec<(u32, Vec<f64>)> = self
.ivf_raw
.iter()
.map(|(&id, xs)| (id, xs.clone()))
.collect();
let n = vecs.len();
let k = cluster_k(n);
let seed = fnv1a_u64(rule_name.as_bytes());
self.ivf_centroids = kmeans_fit(&vecs, k, seed);
for (node, xs) in &vecs {
let Some(unit) = l2_normalize(xs) else {
continue;
};
let c = nearest_centroid(&self.ivf_centroids, &unit);
self.ivf_clusters.insert(*node, c);
self.by_key
.entry(ivf_cluster_key(c))
.or_default()
.insert(*node);
}
self.ivf_drift = 0;
}
pub fn ivf_k(&self) -> usize {
self.ivf_centroids.len()
}
pub fn ivf_cluster_of(&self, node: u32) -> Option<usize> {
self.ivf_clusters.get(&node).copied()
}
pub fn export_ivf_state(&self) -> (Vec<Vec<f64>>, BTreeMap<u32, usize>, u64) {
(
self.ivf_centroids.clone(),
self.ivf_clusters.clone(),
self.ivf_drift,
)
}
pub fn load_ivf_state(
&mut self,
centroids: Vec<Vec<f64>>,
clusters: BTreeMap<u32, usize>,
drift: u64,
) {
for c in self.ivf_clusters.values() {
self.by_key.remove(&ivf_cluster_key(*c));
}
self.ivf_clusters.clear();
self.ivf_centroids = centroids;
self.ivf_drift = drift;
for (&node, &c) in &clusters {
if !self.ivf_raw.contains_key(&node) {
continue;
}
self.ivf_clusters.insert(node, c);
self.by_key
.entry(ivf_cluster_key(c))
.or_default()
.insert(node);
}
}
pub fn init_hnsw(&mut self, rule_name: &str) {
let seed = fnv1a_u64(rule_name.as_bytes());
self.hnsw = Some(HnswIndex::new(seed));
}
fn hnsw_candidates(
&self,
field: &str,
k: usize,
get: &dyn Fn(&str) -> Option<Value>,
) -> BTreeSet<u32> {
let Some(xs) = get(field).as_ref().and_then(as_numeric_list) else {
return BTreeSet::new();
};
if let Some(h) = &self.hnsw {
if !h.is_empty() {
return h.search(&xs, k).into_iter().map(|(id, _)| id).collect();
}
}
self.hnsw_tracked.clone()
}
pub fn export_hnsw_blob(&self) -> Vec<u8> {
self.hnsw
.as_ref()
.and_then(|h| bincode::serialize(h).ok())
.unwrap_or_default()
}
pub fn load_hnsw_blob(&mut self, blob: &[u8]) {
if blob.is_empty() {
return;
}
if let Ok(h) = bincode::deserialize::<HnswIndex>(blob) {
self.hnsw_tracked = h.node_ids();
self.hnsw = Some(h);
}
}
pub fn has_hnsw(&self) -> bool {
self.hnsw.as_ref().is_some_and(|h| !h.is_empty())
}
pub fn hnsw_ref(&self) -> Option<&HnswIndex> {
self.hnsw.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::def::Predicate;
use core_storage::Value;
use std::collections::{BTreeMap, HashMap};
fn getter(map: &HashMap<String, Value>) -> impl Fn(&str) -> Option<Value> + '_ {
move |f: &str| map.get(f).cloned()
}
#[test]
fn kmeans_centroids_are_unit_norm() {
let vecs = vec![(0, vec![3.0, 0.0, 0.0]), (1, vec![0.0, 4.0, 0.0])];
let cents = kmeans_fit(&vecs, 2, 1);
for c in cents {
let n = c.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!((n - 1.0).abs() < 1e-9, "{n}");
}
}
#[test]
fn scaled_vector_joins_same_ivf_cluster_as_unit() {
let spec = CandidateSpec::VectorClusters {
field: "emb",
min: 0.5,
};
let mut idx = SideIndex::default();
idx.load_ivf_state(
vec![vec![1.0, 0.0, 0.0], vec![2.5, 0.1, 0.0]],
BTreeMap::new(),
0,
);
idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0, 0.0])));
idx.insert(&spec, 2, &getter(&emb(&[3.0, 0.0, 0.0])));
assert_eq!(
idx.ivf_cluster_of(1),
idx.ivf_cluster_of(2),
"scale-equivalent vectors must share an IVF cluster; got {:?} vs {:?}",
idx.ivf_cluster_of(1),
idx.ivf_cluster_of(2)
);
assert_eq!(idx.ivf_cluster_of(1), Some(0));
}
#[test]
fn scalar_index_buckets_by_value() {
let pred = Predicate::FieldEqual {
field: "ind".into(),
};
let spec = candidate_spec(&pred);
let mut idx = SideIndex::default();
let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
idx.insert(&spec, 1, &getter(&a));
idx.insert(&spec, 2, &getter(&b));
idx.insert(&spec, 3, &getter(&a));
let c = idx.candidates(&spec, &getter(&a));
assert_eq!(c.into_iter().collect::<Vec<_>>(), vec![1, 3]);
idx.remove(&spec, 3, &getter(&a));
assert_eq!(idx.candidates(&spec, &getter(&a)).len(), 1);
let empty: HashMap<String, Value> = HashMap::new();
idx.insert(&spec, 9, &getter(&empty));
assert!(idx.candidates(&spec, &getter(&empty)).is_empty());
}
#[test]
fn token_index_unions_buckets() {
let mk =
|items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
let pred = Predicate::Overlap {
field: "tags".into(),
min: 0.5,
};
let spec = candidate_spec(&pred);
let mut idx = SideIndex::default();
let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
let c: HashMap<_, _> = [("tags".to_string(), mk(&["q"]))].into();
idx.insert(&spec, 1, &getter(&a));
idx.insert(&spec, 2, &getter(&b));
idx.insert(&spec, 3, &getter(&c));
let probe: HashMap<_, _> = [("tags".to_string(), mk(&["y"]))].into();
assert_eq!(
idx.candidates(&spec, &getter(&probe))
.into_iter()
.collect::<Vec<_>>(),
vec![1, 2]
);
idx.remove(&spec, 2, &getter(&b));
assert_eq!(
idx.candidates(&spec, &getter(&probe))
.into_iter()
.collect::<Vec<_>>(),
vec![1]
);
}
#[test]
fn all_intersects_parts_and_bykey_indexes_nothing() {
let all = Predicate::All(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::Overlap {
field: "tags".into(),
min: 0.5,
},
]);
match candidate_spec(&all) {
CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
other => panic!("{other:?}"),
}
let km = Predicate::KeyMatch { field: "fk".into() };
assert!(matches!(candidate_spec(&km), CandidateSpec::ByKey));
let mut idx = SideIndex::default();
let a: HashMap<_, _> = [("fk".to_string(), Value::Str("c1".into()))].into();
idx.insert(&candidate_spec(&km), 1, &getter(&a));
assert!(idx.candidates(&candidate_spec(&km), &getter(&a)).is_empty());
}
fn year(v: Value) -> HashMap<String, Value> {
[("year".to_string(), v)].into()
}
fn loc(lat: f64, lon: f64) -> HashMap<String, Value> {
[(
"loc".to_string(),
Value::List(vec![Value::Float(lat), Value::Float(lon)]),
)]
.into()
}
fn emb(vals: &[f64]) -> HashMap<String, Value> {
[(
"emb".to_string(),
Value::List(vals.iter().copied().map(Value::Float).collect()),
)]
.into()
}
fn bucket_int(spec: &CandidateSpec, map: &HashMap<String, Value>) -> Option<i64> {
match SideIndex::index_keys(spec, &getter(map)).into_iter().next() {
Some(ValueKey::Int(b)) => Some(b),
_ => None,
}
}
#[test]
fn numeric_bucket_adjacency_and_far_value() {
let pred = Predicate::NumericWithin {
field: "year".into(),
tolerance: 2.0,
};
let spec = candidate_spec(&pred);
assert!(matches!(
spec,
CandidateSpec::NumericBucket {
field: "year",
tolerance
} if tolerance == 2.0
));
let v10 = year(Value::Float(10.0));
let v119 = year(Value::Float(11.9));
let v99 = year(Value::Float(9.9));
let v141 = year(Value::Float(14.1));
let b10 = bucket_int(&spec, &v10).unwrap();
let b119 = bucket_int(&spec, &v119).unwrap();
let b99 = bucket_int(&spec, &v99).unwrap();
assert!((b10 - b119).abs() <= 1);
assert!((b10 - b99).abs() <= 1);
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&v10));
idx.insert(&spec, 2, &getter(&v119));
idx.insert(&spec, 3, &getter(&v141));
idx.insert(&spec, 4, &getter(&v99));
let hits = idx.candidates(&spec, &getter(&v10));
assert_eq!(hits.into_iter().collect::<Vec<_>>(), vec![1, 2, 4]);
}
#[test]
fn numeric_tol_zero_int_float_collide() {
let pred = Predicate::NumericWithin {
field: "year".into(),
tolerance: 0.0,
};
let spec = candidate_spec(&pred);
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&year(Value::Int(2))));
assert_eq!(
idx.candidates(&spec, &getter(&year(Value::Float(2.0))))
.into_iter()
.collect::<Vec<_>>(),
vec![1]
);
assert!(idx
.candidates(&spec, &getter(&year(Value::Float(2.1))))
.is_empty());
}
#[test]
fn numeric_tol_zero_signed_zero_collides() {
let pred = Predicate::NumericWithin {
field: "year".into(),
tolerance: 0.0,
};
let spec = candidate_spec(&pred);
let neg = year(Value::Float(-0.0));
let pos = year(Value::Float(0.0));
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&neg));
assert_eq!(
idx.candidates(&spec, &getter(&pos))
.into_iter()
.collect::<Vec<_>>(),
vec![1]
);
let mut idx2 = SideIndex::default();
idx2.insert(&spec, 2, &getter(&pos));
assert_eq!(
idx2.candidates(&spec, &getter(&neg))
.into_iter()
.collect::<Vec<_>>(),
vec![2]
);
}
#[test]
fn geo_grid_same_cell_cross_cell_and_far_city() {
let pred = Predicate::GeoRadius {
field: "loc".into(),
km: 400.0,
};
let spec = candidate_spec(&pred);
assert!(matches!(
spec,
CandidateSpec::GeoGrid {
field: "loc",
km
} if km == 400.0
));
let paris = loc(48.8566, 2.3522);
let london = loc(51.5074, -0.1278);
let nearby = loc(48.9, 2.4); let ny = loc(40.7128, -74.0060);
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&paris));
idx.insert(&spec, 2, &getter(&london));
idx.insert(&spec, 3, &getter(&nearby));
idx.insert(&spec, 4, &getter(&ny));
let from_paris = idx.candidates(&spec, &getter(&paris));
assert!(from_paris.contains(&1), "same-cell self");
assert!(from_paris.contains(&3), "same-cell neighbor");
assert!(from_paris.contains(&2), "cross-cell Paris↔London ~343.5 km");
assert!(!from_paris.contains(&4), "New York not in 400 km probe");
}
#[test]
fn geo_grid_high_latitude_probe_is_superset() {
let pred = Predicate::GeoRadius {
field: "loc".into(),
km: 340.0,
};
let spec = candidate_spec(&pred);
let reyk = loc(64.1466, -21.9426);
let lat = 64.0_f64;
let dlon = 300.0 / (111.0 * lat.to_radians().cos());
let east = loc(lat, -21.9426 + dlon);
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&reyk));
idx.insert(&spec, 2, &getter(&east));
let hits = idx.candidates(&spec, &getter(&reyk));
assert!(
hits.contains(&2),
"300 km east of Reykjavik must stay in the high-lat probe"
);
}
#[test]
fn geo_grid_antimeridian_wrap_and_evaluate_agree() {
let pred = Predicate::GeoRadius {
field: "loc".into(),
km: 400.0,
};
let spec = candidate_spec(&pred);
let east = loc(70.0, 179.9);
let west = loc(70.0, -179.9);
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&east));
assert!(
idx.candidates(&spec, &getter(&west)).contains(&1),
"±180 pair at lat 70 must land in the wrapped probe"
);
let sp = |f: &str| east.get(f).cloned();
let dp = |f: &str| west.get(f).cloned();
let score = crate::def::evaluate(
&pred,
&crate::def::NodeView {
key: "e",
props: &sp,
},
&crate::def::NodeView {
key: "w",
props: &dp,
},
);
assert!(
score.is_some(),
"haversine must match across the antimeridian"
);
let paris = loc(48.8566, 2.3522);
let ny = loc(40.7128, -74.0060);
let mut idx2 = SideIndex::default();
idx2.insert(&spec, 4, &getter(&ny));
assert!(
!idx2.candidates(&spec, &getter(&paris)).contains(&4),
"New York still not in the Paris probe after wrap"
);
}
#[test]
fn scan_all_returns_vector_nodes_skips_malformed() {
let pred = Predicate::VectorSimilar {
field: "emb".into(),
min: 0.5,
};
let spec = candidate_spec(&pred);
assert!(matches!(spec, CandidateSpec::ScanAll { field: "emb" }));
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0])));
idx.insert(&spec, 2, &getter(&emb(&[0.0, 1.0])));
idx.insert(&spec, 3, &getter(&emb(&[1.0, 2.0, 3.0])));
let empty: HashMap<_, _> = [("emb".to_string(), Value::List(vec![]))].into();
let text: HashMap<_, _> =
[("emb".to_string(), Value::List(vec![Value::Str("x".into())]))].into();
let missing: HashMap<String, Value> = HashMap::new();
idx.insert(&spec, 4, &getter(&empty));
idx.insert(&spec, 5, &getter(&text));
idx.insert(&spec, 6, &getter(&missing));
let hits = idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])));
assert_eq!(
hits.into_iter().collect::<Vec<_>>(),
vec![1, 2],
"dim-2 probe must drop the dim-3 member"
);
assert_eq!(
idx.candidates(&spec, &getter(&emb(&[1.0, 2.0, 3.0])))
.into_iter()
.collect::<Vec<_>>(),
vec![3]
);
with_vector_dim_reject(false, || {
assert_eq!(
idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])))
.into_iter()
.collect::<Vec<_>>(),
vec![1, 2, 3],
"unfiltered ScanAll still returns every vector node"
);
});
assert_eq!(idx.vec_dim(1), Some(2));
assert_eq!(idx.vec_dim(3), Some(3));
assert!(idx.vec_meta(1).is_some());
assert!(idx.vec_dim(4).is_none());
assert!(idx.candidates(&spec, &getter(&empty)).is_empty());
assert!(idx.candidates(&spec, &getter(&text)).is_empty());
assert!(idx.candidates(&spec, &getter(&missing)).is_empty());
idx.remove(&spec, 1, &getter(&emb(&[1.0, 0.0])));
assert!(idx.vec_dim(1).is_none());
}
#[test]
fn legacy_specs_probe_keys_equal_index_keys() {
let a: HashMap<_, _> = [
("ind".to_string(), Value::Str("arch".into())),
(
"tags".to_string(),
Value::List(vec![Value::Str("x".into()), Value::Str("y".into())]),
),
("fk".to_string(), Value::Str("c1".into())),
]
.into();
let get = getter(&a);
for pred in [
Predicate::KeyMatch { field: "fk".into() },
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::Overlap {
field: "tags".into(),
min: 0.5,
},
] {
let spec = candidate_spec(&pred);
assert_eq!(
SideIndex::index_keys(&spec, &get),
SideIndex::probe_keys(&spec, &get)
);
}
}
#[test]
fn all_vector_then_field_equal_does_not_scan_all() {
let p = Predicate::All(vec![
Predicate::VectorSimilar {
field: "e".into(),
min: 0.8,
},
Predicate::FieldEqual {
field: "industry".into(),
},
]);
match candidate_spec(&p) {
CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
other => panic!("{other:?}"),
}
let spec = candidate_spec(&p);
let mut idx = SideIndex::default();
let mk = |industry: &str, e: &[f64]| {
[
("industry".to_string(), Value::Str(industry.into())),
(
"e".to_string(),
Value::List(e.iter().copied().map(Value::Float).collect()),
),
]
.into()
};
let same: HashMap<_, _> = mk("tech", &[1.0, 0.0]);
let other_ind: HashMap<_, _> = mk("law", &[1.0, 0.0]);
let no_vec: HashMap<_, _> = [("industry".to_string(), Value::Str("tech".into()))].into();
idx.insert(&spec, 1, &getter(&same));
idx.insert(&spec, 2, &getter(&other_ind));
idx.insert(&spec, 3, &getter(&no_vec));
let hits = idx.candidates(&spec, &getter(&same));
assert!(hits.contains(&1), "matching industry must stay a candidate");
assert!(
!hits.contains(&2),
"different industry must not be scanned in via VectorSimilar"
);
assert!(
hits.contains(&3),
"ScanAll is universe: extra Scalar-only candidates are allowed"
);
let empty_ind: HashMap<_, _> = mk("finance", &[1.0, 0.0]);
assert!(
idx.candidates(&spec, &getter(&empty_ind)).is_empty(),
"empty Scalar child → empty intersect"
);
}
#[test]
fn all_approx_vector_then_field_equal_is_intersect() {
let p = Predicate::All(vec![
Predicate::VectorSimilar {
field: "e".into(),
min: 0.8,
},
Predicate::FieldEqual {
field: "industry".into(),
},
]);
match candidate_spec_approx(&p) {
CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
other => panic!("{other:?}"),
}
let spec = candidate_spec_approx(&p);
let mut idx = SideIndex::default();
idx.init_hnsw("test-rule");
let mk = |industry: &str, e: &[f64]| {
[
("industry".to_string(), Value::Str(industry.into())),
(
"e".to_string(),
Value::List(e.iter().copied().map(Value::Float).collect()),
),
]
.into()
};
let same: HashMap<_, _> = mk("tech", &[1.0, 0.0]);
let other_ind: HashMap<_, _> = mk("law", &[1.0, 0.0]);
idx.insert(&spec, 1, &getter(&same));
idx.insert(&spec, 2, &getter(&other_ind));
let hits = idx.candidates(&spec, &getter(&same));
assert!(hits.contains(&1), "matching industry must stay a candidate");
assert!(
!hits.contains(&2),
"FieldEqual must be probed on the approximate All path"
);
}
#[test]
fn all_of_scan_all_stays_scan_all() {
let p = Predicate::All(vec![
Predicate::VectorSimilar {
field: "emb".into(),
min: 0.5,
},
Predicate::VectorSimilar {
field: "emb".into(),
min: 0.9,
},
]);
match candidate_spec(&p) {
CandidateSpec::Intersect(v) => assert_eq!(v.len(), 2),
other => panic!("{other:?}"),
}
let spec = candidate_spec(&p);
let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&emb(&[1.0, 0.0])));
idx.insert(&spec, 2, &getter(&emb(&[0.0, 1.0])));
let hits = idx.candidates(&spec, &getter(&emb(&[1.0, 0.0])));
assert_eq!(hits.into_iter().collect::<Vec<_>>(), vec![1, 2]);
}
#[test]
fn any_stays_union() {
let p = Predicate::Any(vec![
Predicate::FieldEqual {
field: "industry".into(),
},
Predicate::Overlap {
field: "tags".into(),
min: 0.5,
},
]);
match candidate_spec(&p) {
CandidateSpec::Union(v) => assert_eq!(v.len(), 2),
other => panic!("{other:?}"),
}
}
#[test]
fn checkpoint_populated_and_consistent_with_norm() {
let pred = Predicate::VectorSimilar {
field: "emb".into(),
min: 0.8,
};
let spec = candidate_spec(&pred);
let xs = [3.0f64, 4.0]; let mut idx = SideIndex::default();
idx.insert(&spec, 1, &getter(&emb(&xs)));
let ckpts = idx
.vec_ckpts(1)
.expect("checkpoints must exist after insert");
let (_, norm) = idx.vec_meta(1).unwrap();
assert!(
(ckpts[0] - norm).abs() < 1e-12,
"ckpts[0] must equal the full L2 norm; got {} vs {}",
ckpts[0],
norm
);
assert!(
(norm - 5.0).abs() < 1e-12,
"norm of [3,4] must be 5.0, got {norm}"
);
idx.remove(&spec, 1, &getter(&emb(&xs)));
assert!(
idx.vec_ckpts(1).is_none(),
"checkpoints must be removed after remove()"
);
}
#[test]
fn fresh_ckpts_for_freshness_gate() {
let pred = Predicate::VectorSimilar {
field: "emb".into(),
min: 0.8,
};
let spec = candidate_spec(&pred);
let xs = [1.0f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let mut idx = SideIndex::default();
idx.insert(&spec, 7, &getter(&emb(&xs)));
let result = idx.fresh_ckpts_for(7, &xs);
assert!(
result.is_some(),
"fresh_ckpts_for must succeed with matching live vector"
);
let (norm, ckpts) = result.unwrap();
assert!((norm - 1.0).abs() < 1e-12);
assert!((ckpts[0] - 1.0).abs() < 1e-12);
let wrong = [2.0f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; assert!(
idx.fresh_ckpts_for(7, &wrong).is_none(),
"freshness gate must reject mismatched norm"
);
let short = [1.0f64, 0.0];
assert!(
idx.fresh_ckpts_for(7, &short).is_none(),
"freshness gate must reject mismatched dim"
);
assert!(idx.fresh_ckpts_for(99, &xs).is_none());
}
#[test]
fn checkpoint_suffix_norms_non_increasing() {
let pred = Predicate::VectorSimilar {
field: "emb".into(),
min: 0.5,
};
let spec = candidate_spec(&pred);
let xs: Vec<f64> = (1..=16).map(|i| i as f64).collect();
let mut idx = SideIndex::default();
idx.insert(&spec, 42, &getter(&emb(&xs)));
let ckpts = *idx.vec_ckpts(42).unwrap();
for c in 0..7 {
assert!(
ckpts[c] >= ckpts[c + 1] - 1e-12,
"suffix norm must be non-increasing: ckpts[{c}]={} < ckpts[{}]={}",
ckpts[c],
c + 1,
ckpts[c + 1]
);
}
let expected_last = (15.0f64 * 15.0 + 16.0 * 16.0).sqrt();
assert!(
(ckpts[7] - expected_last).abs() < 1e-9,
"ckpts[7] should be norm of last segment; got {} vs {}",
ckpts[7],
expected_last
);
}
}