mod build;
mod codec;
mod search;
pub mod select_neighbors;
mod visited;
use crate::materialize::PackedVectors;
pub const HNSW_PAYLOAD_VERSION: u32 = 2;
pub const DEFAULT_M: usize = 16;
pub const DEFAULT_EF_CONSTRUCTION: usize = 400;
#[derive(Clone, Debug)]
pub(super) struct Node {
pub level: u32,
pub neighbors: Vec<Vec<u32>>,
}
pub struct HnswIndex {
pub m: usize,
pub m_max0: usize,
pub ef_construction: usize,
pub entry_point: u32,
pub max_level: u32,
pub(super) nodes: Vec<Node>,
pub(super) store: PackedVectors,
pub(super) dim: usize,
pub(super) n: usize,
pub ef_search: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Candidate {
pub id: u32,
pub dist: f32,
}
impl Eq for Candidate {}
#[inline]
pub(super) fn cosine_dist(a: &[f32], b: &[f32]) -> f32 {
let mut acc = [0.0f32; 8];
let ca = a.chunks_exact(8);
let cb = b.chunks_exact(8);
let (ra, rb) = (ca.remainder(), cb.remainder());
for (x, y) in ca.zip(cb) {
for j in 0..8 {
acc[j] += x[j] * y[j];
}
}
let mut tail = 0.0f32;
for (x, y) in ra.iter().zip(rb.iter()) {
tail += x * y;
}
let dot =
((acc[0] + acc[1]) + (acc[2] + acc[3])) + ((acc[4] + acc[5]) + (acc[6] + acc[7])) + tail;
1.0 - dot
}
#[inline]
pub(super) fn dist_q(
store: &PackedVectors,
q: &[f32],
i: usize,
dim: usize,
scratch: &mut [f32],
) -> f32 {
cosine_dist(q, store.row(i, dim, scratch))
}
#[inline]
pub(super) fn dist_rr(
store: &PackedVectors,
a: usize,
b: usize,
dim: usize,
sa: &mut [f32],
sb: &mut [f32],
) -> f32 {
cosine_dist(store.row(a, dim, sa), store.row(b, dim, sb))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ann::build::LcgRng;
use std::collections::HashSet;
pub(super) fn random_vectors(n: usize, dim: usize, seed: u64) -> Vec<f32> {
let mut rng = LcgRng::new(seed);
let mut v = Vec::with_capacity(n * dim);
for _ in 0..(n * dim) {
v.push((rng.next_f64() as f32) - 0.5);
}
for i in 0..n {
let row = &mut v[i * dim..(i + 1) * dim];
let norm: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in row.iter_mut() {
*x /= norm;
}
}
}
v
}
#[test]
fn small_index_recall_against_exact() {
let n = 200;
let dim = 32;
let vecs = random_vectors(n, dim, 0xDEAD_BEEF);
let idx = HnswIndex::build(vecs.clone(), n, dim, 8, 50, 42);
let q = random_vectors(1, dim, 0xCAFEBABE);
let mut exact: Vec<(usize, f32)> = (0..n)
.map(|i| {
let row = &vecs[i * dim..(i + 1) * dim];
let mut s = 0.0f32;
for j in 0..dim {
s += q[j] * row[j];
}
(i, s)
})
.collect();
exact.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let exact_top: HashSet<usize> = exact.iter().take(10).map(|p| p.0).collect();
let approx = idx.search(&q, 50);
let approx_set: HashSet<usize> = approx.into_iter().take(10).collect();
let overlap = exact_top.intersection(&approx_set).count();
assert!(
overlap >= 7,
"recall@10 too low: {} of 10 (expected >= 7)",
overlap
);
}
#[test]
fn serialize_roundtrip() {
let n = 50;
let dim = 16;
let vecs = random_vectors(n, dim, 7);
let idx = HnswIndex::build(vecs.clone(), n, dim, 8, 30, 42);
let bytes = idx.to_bytes();
let mut decoded = HnswIndex::from_bytes(&bytes, n, dim).unwrap();
decoded.attach_vectors(vecs);
let q: Vec<f32> = vec![1.0 / (dim as f32).sqrt(); dim];
let a = idx.search(&q, 20);
let b = decoded.search(&q, 20);
let a_set: HashSet<usize> = a.into_iter().collect();
let b_set: HashSet<usize> = b.into_iter().collect();
assert_eq!(a_set, b_set, "candidate sets must match after roundtrip");
}
#[test]
fn deterministic_build_same_seed() {
let n = 30;
let dim = 8;
let vecs = random_vectors(n, dim, 0xABCD);
let a = HnswIndex::build(vecs.clone(), n, dim, 4, 20, 123);
let b = HnswIndex::build(vecs, n, dim, 4, 20, 123);
assert_eq!(a.to_bytes(), b.to_bytes(), "same seed => same graph");
}
}