use std::cmp::Ordering;
pub trait VectorIndex {
fn build(points: &[(usize, Vec<f32>)], seed: u64) -> Self
where
Self: Sized;
fn search(&self, query: &[f32], k: usize) -> Vec<(usize, f32)>;
}
pub fn cosine(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
(dot / (norm_a * norm_b)).clamp(-1.0, 1.0)
}
}
pub struct BruteForceIndex {
points: Vec<(usize, Vec<f32>)>,
}
impl VectorIndex for BruteForceIndex {
fn build(points: &[(usize, Vec<f32>)], _seed: u64) -> Self {
Self {
points: points.to_vec(),
}
}
fn search(&self, query: &[f32], k: usize) -> Vec<(usize, f32)> {
let mut scored: Vec<(usize, f32)> = self
.points
.iter()
.map(|(id_index, vec)| (*id_index, cosine(query, vec)))
.collect();
scored.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
scored.truncate(k);
scored
}
}
#[cfg(feature = "ann")]
pub struct InstantDistanceIndex {
hnsw: instant_distance::Hnsw<VecPoint>,
reverse_map: std::collections::HashMap<instant_distance::PointId, usize>,
id_mapping: Vec<usize>,
vectors: Vec<Vec<f32>>,
}
#[cfg(feature = "ann")]
#[derive(Clone)]
struct VecPoint(Vec<f32>);
#[cfg(feature = "ann")]
impl instant_distance::Point for VecPoint {
fn distance(&self, other: &Self) -> f32 {
(1.0 - cosine(&self.0, &other.0)).max(0.0)
}
}
#[cfg(feature = "ann")]
impl VectorIndex for InstantDistanceIndex {
fn build(points: &[(usize, Vec<f32>)], seed: u64) -> Self {
use std::collections::HashMap;
let id_mapping: Vec<usize> = points.iter().map(|(i, _)| *i).collect();
let vectors: Vec<Vec<f32>> = points.iter().map(|(_, v)| v.clone()).collect();
let vec_points: Vec<VecPoint> = vectors.iter().map(|v| VecPoint(v.clone())).collect();
let (hnsw, ids) = instant_distance::Builder::default()
.seed(seed)
.build_hnsw(vec_points);
let mut reverse_map = HashMap::with_capacity(ids.len());
for (original_idx, pid) in ids.iter().enumerate() {
reverse_map.insert(*pid, original_idx);
}
Self {
hnsw,
reverse_map,
id_mapping,
vectors,
}
}
fn search(&self, query: &[f32], k: usize) -> Vec<(usize, f32)> {
let qp = VecPoint(query.to_vec());
let mut search = instant_distance::Search::default();
let mut results: Vec<(usize, f32)> = self
.hnsw
.search(&qp, &mut search)
.take(k)
.filter_map(|item| {
let &orig_idx = self.reverse_map.get(&item.pid)?;
let id_index = self.id_mapping[orig_idx];
let sim = cosine(query, &self.vectors[orig_idx]);
Some((id_index, sim))
})
.collect();
results.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
results
}
}
#[cfg(test)]
mod tests {
use super::*;
pub(super) fn bow(text: &str, dim: usize) -> Vec<f32> {
let mut v = vec![0f32; dim];
for w in text.to_lowercase().split_whitespace() {
let h = w
.bytes()
.fold(0usize, |a, b| a.wrapping_mul(31).wrapping_add(b as usize))
% dim;
v[h] += 1.0;
}
let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 0.0 {
for x in &mut v {
*x /= n;
}
}
v
}
#[test]
fn test_cosine_identical_unit_vectors() {
let v = vec![1.0f32, 0.0, 0.0];
let sim = cosine(&v, &v);
assert!(
(sim - 1.0).abs() < 1e-6,
"identical unit vectors → cosine = 1"
);
}
#[test]
fn test_cosine_orthogonal_vectors() {
let a = vec![1.0f32, 0.0];
let b = vec![0.0f32, 1.0];
assert!((cosine(&a, &b)).abs() < 1e-6, "orthogonal → cosine = 0");
}
#[test]
fn test_cosine_zero_vector_returns_zero() {
let z = vec![0.0f32, 0.0, 0.0];
let v = vec![1.0f32, 0.0, 0.0];
assert_eq!(cosine(&z, &v), 0.0);
assert_eq!(cosine(&v, &z), 0.0);
}
#[test]
fn test_cosine_dim_mismatch_returns_zero() {
let a = vec![1.0f32, 0.0];
let b = vec![1.0f32, 0.0, 0.0];
assert_eq!(cosine(&a, &b), 0.0);
}
#[test]
fn test_brute_force_nearest_is_correct() {
let dim = 64usize;
let points: Vec<(usize, Vec<f32>)> = vec![
(0, bow("budget allocation policy", dim)),
(1, bow("network latency tuning", dim)),
(2, bow("thread pool size", dim)),
];
let query = bow("what is the budget", dim);
let index = BruteForceIndex::build(&points, 42);
let hits = index.search(&query, 1);
assert_eq!(hits.len(), 1);
assert_eq!(
hits[0].0, 0,
"point 0 should be the nearest to the budget query"
);
assert!(hits[0].1 > 0.0, "cosine should be positive");
}
#[test]
fn test_brute_force_tie_break_by_id_index_asc() {
let v = vec![0.5f32, 0.5];
let points: Vec<(usize, Vec<f32>)> = vec![(10, v.clone()), (5, v.clone())];
let index = BruteForceIndex::build(&points, 42);
let hits = index.search(&v, 2);
assert_eq!(hits[0].0, 5, "lower id_index wins the tie");
assert_eq!(hits[1].0, 10);
}
#[test]
fn test_brute_force_returns_at_most_k() {
let points: Vec<(usize, Vec<f32>)> =
(0..10).map(|i| (i, bow(&format!("item {i}"), 8))).collect();
let index = BruteForceIndex::build(&points, 42);
let hits = index.search(&bow("item 3", 8), 3);
assert!(hits.len() <= 3);
}
#[cfg(feature = "ann")]
#[test]
fn test_ann_identical_rebuilds_are_byte_stable() {
let dim = 32usize;
let seed = 42u64;
let points: Vec<(usize, Vec<f32>)> = (0..64)
.map(|i| {
(
i,
bow(&format!("point number {i} with label {}", i % 8), dim),
)
})
.collect();
let query = bow("point label 3 feature", dim);
let index1 = InstantDistanceIndex::build(&points, seed);
let index2 = InstantDistanceIndex::build(&points, seed);
let top5_1 = index1.search(&query, 5);
let top5_2 = index2.search(&query, 5);
assert!(
!top5_1.is_empty(),
"ANN should return at least one result for a non-empty index"
);
assert_eq!(
top5_1, top5_2,
"ANN builds with same seed and data must be byte-stable (CP2-A)"
);
}
#[cfg(feature = "ann")]
#[test]
fn test_bruteforce_and_ann_agree_on_top1() {
let dim = 64usize; let seed = 42u64;
let mut pts: Vec<(usize, Vec<f32>)> = (1..50)
.map(|i| {
(
i,
bow(&format!("network latency server rack node {i}", i = i), dim),
)
})
.collect();
pts.insert(
0,
(0, bow("budget context token allocation policy limit", dim)),
);
let query = bow("budget context token allocation policy limit", dim);
let bf = BruteForceIndex::build(&pts, seed);
let ann = InstantDistanceIndex::build(&pts, seed);
let bf_top3 = bf.search(&query, 3);
let ann_top1 = ann.search(&query, 1);
assert!(
!bf_top3.is_empty(),
"brute-force should find nearest points"
);
assert!(!ann_top1.is_empty(), "ANN should find a nearest point");
let bf_top3_ids: Vec<usize> = bf_top3.iter().map(|(id, _)| *id).collect();
assert!(
bf_top3_ids.contains(&ann_top1[0].0),
"ANN top-1 (id={}) must be within brute-force top-3 {:?} (oracle check)",
ann_top1[0].0,
bf_top3_ids
);
}
}