#[cfg(feature = "embedding-router")]
use super::EmbeddingRouterError;
use super::normalise;
#[derive(Debug, Clone)]
pub(crate) struct IndexHit {
pub key: String,
pub distance: f32,
}
pub(crate) trait Index: Send + Sync {
fn search(&self, query: &[f32], k: usize) -> Vec<IndexHit>;
fn len(&self) -> usize;
}
#[allow(dead_code)]
pub(crate) struct BruteForceIndex {
entries: Vec<(String, Vec<f32>)>,
}
#[allow(dead_code)]
impl BruteForceIndex {
pub(crate) fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub(crate) fn insert(&mut self, key: String, vector: Vec<f32>) {
let unit = normalise(vector);
self.entries.push((key, unit));
}
}
impl Index for BruteForceIndex {
fn search(&self, query: &[f32], k: usize) -> Vec<IndexHit> {
if self.entries.is_empty() || k == 0 {
return Vec::new();
}
let mut scored: Vec<(String, f32)> = self
.entries
.iter()
.map(|(key, v)| (key.clone(), l2_squared(query, v)))
.collect();
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
scored.truncate(k);
scored
.into_iter()
.map(|(key, distance)| IndexHit { key, distance })
.collect()
}
fn len(&self) -> usize {
self.entries.len()
}
}
#[cfg(feature = "embedding-router")]
pub(crate) struct DiskAnnEmbeddingIndex {
inner: ruvector_diskann::DiskAnnIndex,
len: usize,
}
#[cfg(feature = "embedding-router")]
impl DiskAnnEmbeddingIndex {
pub(crate) fn build(
dim: usize,
entries: Vec<(String, Vec<f32>)>,
) -> Result<Self, EmbeddingRouterError> {
let config = ruvector_diskann::DiskAnnConfig {
dim,
max_degree: 32,
build_beam: 64,
search_beam: 64,
alpha: 1.2,
pq_subspaces: 0,
pq_iterations: 0,
storage_path: None,
};
let mut index = ruvector_diskann::DiskAnnIndex::new(config);
let len = entries.len();
for (key, vector) in entries {
let unit = normalise(vector);
index
.insert(key, unit)
.map_err(|e| EmbeddingRouterError::IndexError(e.to_string()))?;
}
index
.build()
.map_err(|e| EmbeddingRouterError::IndexError(e.to_string()))?;
Ok(Self { inner: index, len })
}
}
#[cfg(feature = "embedding-router")]
impl Index for DiskAnnEmbeddingIndex {
fn search(&self, query: &[f32], k: usize) -> Vec<IndexHit> {
let unit = normalise(query.to_vec());
match self.inner.search(&unit, k) {
Ok(hits) => hits
.into_iter()
.map(|h| IndexHit {
key: h.id,
distance: h.distance,
})
.collect(),
Err(_) => Vec::new(),
}
}
fn len(&self) -> usize {
self.len
}
}
#[allow(dead_code)]
fn l2_squared(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| {
let d = x - y;
d * d
})
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn brute_force_index_basic() {
let mut idx = BruteForceIndex::new();
idx.insert("a".into(), vec![1.0, 0.0]);
idx.insert("b".into(), vec![0.0, 1.0]);
let unit = normalise(vec![1.0, 0.0]);
let hits = idx.search(&unit, 1);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].key, "a");
assert!(hits[0].distance < 1e-5);
}
#[test]
fn brute_force_index_empty_returns_empty() {
let idx = BruteForceIndex::new();
let hits = idx.search(&[1.0, 0.0], 5);
assert!(hits.is_empty());
}
#[test]
fn l2_squared_basic() {
assert!((l2_squared(&[1.0, 0.0], &[0.0, 0.0]) - 1.0).abs() < 1e-6);
assert!((l2_squared(&[1.0, 1.0], &[2.0, 2.0]) - 2.0).abs() < 1e-6);
}
#[test]
fn brute_force_k_zero_returns_empty() {
let mut idx = BruteForceIndex::new();
idx.insert("a".into(), vec![1.0, 0.0]);
assert!(idx.search(&[1.0, 0.0], 0).is_empty());
}
#[test]
fn brute_force_top_k_orders_by_ascending_distance() {
let mut idx = BruteForceIndex::new();
idx.insert("plus_x".into(), vec![1.0, 0.0]);
idx.insert("plus_y".into(), vec![0.0, 1.0]);
idx.insert("minus_x".into(), vec![-1.0, 0.0]);
let q = normalise(vec![1.0, 0.1]);
let hits = idx.search(&q, 3);
assert_eq!(hits.len(), 3);
assert_eq!(hits[0].key, "plus_x");
assert_eq!(hits[1].key, "plus_y");
assert_eq!(hits[2].key, "minus_x");
assert!(hits[0].distance <= hits[1].distance);
assert!(hits[1].distance <= hits[2].distance);
}
#[test]
fn brute_force_k_larger_than_len_returns_all() {
let mut idx = BruteForceIndex::new();
idx.insert("a".into(), vec![1.0, 0.0]);
idx.insert("b".into(), vec![0.0, 1.0]);
let hits = idx.search(&normalise(vec![1.0, 0.0]), 50);
assert_eq!(hits.len(), 2);
assert_eq!(idx.len(), 2);
}
#[test]
fn brute_force_insert_normalises_non_unit_vectors() {
let mut idx = BruteForceIndex::new();
idx.insert("scaled".into(), vec![3.0, 4.0]);
let unit = normalise(vec![3.0, 4.0]);
let hits = idx.search(&unit, 1);
assert_eq!(hits.len(), 1);
assert!(
hits[0].distance < 1e-5,
"self-match after normalise must be ~0, got {}",
hits[0].distance
);
}
#[test]
fn brute_force_cosine_from_l2_squared_is_one_for_identical() {
let mut idx = BruteForceIndex::new();
idx.insert("s".into(), vec![1.0, 0.0, 0.0]);
let hits = idx.search(&normalise(vec![1.0, 0.0, 0.0]), 1);
let cos = 1.0 - hits[0].distance / 2.0;
assert!((cos - 1.0).abs() < 1e-5, "cos={cos}");
}
#[test]
fn brute_force_orthogonal_cosine_near_zero() {
let mut idx = BruteForceIndex::new();
idx.insert("x".into(), vec![1.0, 0.0]);
let hits = idx.search(&normalise(vec![0.0, 1.0]), 1);
let cos = 1.0 - hits[0].distance / 2.0;
assert!(cos.abs() < 1e-4, "orthogonal cos should be ~0, got {cos}");
}
}