use super::vector::{dot_product, neg_euclidean_distance, DistanceMetric};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::sync::RwLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum HnswMetric {
Cosine,
Dot,
Euclidean,
}
impl HnswMetric {
pub fn from_distance(metric: DistanceMetric) -> Option<Self> {
match metric {
DistanceMetric::Cosine => Some(HnswMetric::Cosine),
DistanceMetric::DotProduct => Some(HnswMetric::Dot),
DistanceMetric::Euclidean => Some(HnswMetric::Euclidean),
DistanceMetric::Poincare => None,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct HnswParams {
pub m: usize,
pub ef_construction: usize,
pub ef_search: usize,
}
impl Default for HnswParams {
fn default() -> Self {
HnswParams {
m: 16,
ef_construction: 200,
ef_search: 64,
}
}
}
struct SplitMix64(u64);
impl SplitMix64 {
#[inline]
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[inline]
fn unit(&mut self) -> f64 {
let v = (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64);
if v <= 0.0 {
f64::MIN_POSITIVE
} else {
v
}
}
}
#[derive(Clone, Copy)]
struct Cand {
id: u32,
dist: f32,
}
impl PartialEq for Cand {
fn eq(&self, other: &Self) -> bool {
self.dist == other.dist
}
}
impl Eq for Cand {}
impl PartialOrd for Cand {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Cand {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.dist
.partial_cmp(&other.dist)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
struct DistCtx<'a> {
data: &'a [f32],
norms: &'a [f32],
dim: usize,
metric: HnswMetric,
}
impl<'a> DistCtx<'a> {
#[inline]
fn vec(&self, id: u32) -> &[f32] {
let s = id as usize * self.dim;
&self.data[s..s + self.dim]
}
#[inline]
fn dist_ids(&self, a: u32, b: u32) -> f32 {
let va = self.vec(a);
let vb = self.vec(b);
match self.metric {
HnswMetric::Cosine => {
let denom = self.norms[a as usize] * self.norms[b as usize];
if denom > 0.0 {
1.0 - dot_product(va, vb) / denom
} else {
1.0
}
}
HnswMetric::Dot => -dot_product(va, vb),
HnswMetric::Euclidean => -neg_euclidean_distance(va, vb),
}
}
#[inline]
fn dist_query(&self, query: &[f32], query_norm: f32, b: u32) -> f32 {
let vb = self.vec(b);
match self.metric {
HnswMetric::Cosine => {
let denom = query_norm * self.norms[b as usize];
if denom > 0.0 {
1.0 - dot_product(query, vb) / denom
} else {
1.0
}
}
HnswMetric::Dot => -dot_product(query, vb),
HnswMetric::Euclidean => -neg_euclidean_distance(query, vb),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HnswIndex {
params: HnswParams,
metric: HnswMetric,
dim: usize,
len: usize,
node_levels: Vec<u8>,
links: Vec<Vec<Vec<u32>>>,
entry_point: Option<u32>,
max_level: usize,
seed: u64,
insert_counter: u64,
}
impl HnswIndex {
#[inline]
fn m_max(&self, layer: usize) -> usize {
if layer == 0 {
self.params.m * 2
} else {
self.params.m
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn metric(&self) -> HnswMetric {
self.metric
}
pub fn params(&self) -> HnswParams {
self.params
}
pub fn build(
data: &[f32],
norms: &[f32],
dim: usize,
metric: HnswMetric,
params: HnswParams,
seed: u64,
) -> Self {
let n = data.len().checked_div(dim).unwrap_or(0);
if n == 0 {
return HnswIndex {
params,
metric,
dim,
len: 0,
node_levels: Vec::new(),
links: Vec::new(),
entry_point: None,
max_level: 0,
seed,
insert_counter: 0,
};
}
let node_levels: Vec<u8> = (0..n as u64)
.map(|i| level_for(seed, i, params.m) as u8)
.collect();
let links: Vec<RwLock<Vec<Vec<u32>>>> = node_levels
.iter()
.map(|&lvl| RwLock::new(vec![Vec::new(); lvl as usize + 1]))
.collect();
let ep_state = RwLock::new((0u32, node_levels[0] as usize));
let ctx = DistCtx {
data,
norms,
dim,
metric,
};
(1..n as u32).into_par_iter().for_each(|slot| {
insert_concurrent(slot, &ctx, ¶ms, &node_levels, &links, &ep_state);
});
let links: Vec<Vec<Vec<u32>>> = links
.into_iter()
.map(|l| l.into_inner().unwrap_or_default())
.collect();
let (entry_point, max_level) = *ep_state.read().unwrap();
HnswIndex {
params,
metric,
dim,
len: n,
node_levels,
links,
entry_point: Some(entry_point),
max_level,
seed,
insert_counter: n as u64,
}
}
pub fn insert(&mut self, slot: u32, data: &[f32], norms: &[f32], dim: usize) {
debug_assert_eq!(dim, self.dim, "dimension mismatch on incremental insert");
let ctx = DistCtx {
data,
norms,
dim,
metric: self.metric,
};
self.insert_with_ctx(slot, &ctx);
}
fn random_level(&mut self) -> usize {
let lvl = level_for(self.seed, self.insert_counter, self.params.m);
self.insert_counter += 1;
lvl
}
fn insert_with_ctx(&mut self, slot: u32, ctx: &DistCtx) {
let level = self.random_level();
let need = slot as usize + 1;
if self.node_levels.len() < need {
self.node_levels.resize(need, 0);
self.links.resize(need, Vec::new());
}
self.node_levels[slot as usize] = level as u8;
self.links[slot as usize] = vec![Vec::new(); level + 1];
self.len += 1;
let entry = match self.entry_point {
Some(e) => e,
None => {
self.entry_point = Some(slot);
self.max_level = level;
return;
}
};
let df = |id: u32| ctx.dist_ids(slot, id);
let mut ep = vec![entry];
let top = self.max_level;
if top > level {
for lc in (level + 1..=top).rev() {
let w = self.search_layer(ctx, &ep, 1, lc, &df);
if let Some(best) = w.into_iter().min() {
ep = vec![best.id];
}
}
}
let start = top.min(level);
for lc in (0..=start).rev() {
let w = self.search_layer(ctx, &ep, self.params.ef_construction, lc, &df);
let m_max = self.m_max(lc);
let selected = select_neighbors(ctx, slot, &w, self.params.m);
self.links[slot as usize][lc] = selected.clone();
for &e in &selected {
self.links[e as usize][lc].push(slot);
if self.links[e as usize][lc].len() > m_max {
let cands: Vec<Cand> = self.links[e as usize][lc]
.iter()
.map(|&id| Cand {
id,
dist: ctx.dist_ids(e, id),
})
.collect();
let pruned = select_neighbors(ctx, e, &cands, m_max);
self.links[e as usize][lc] = pruned;
}
}
ep = w.iter().map(|c| c.id).collect();
if ep.is_empty() {
ep = vec![entry];
}
}
if level > self.max_level {
self.max_level = level;
self.entry_point = Some(slot);
}
}
fn search_layer(
&self,
_ctx: &DistCtx,
entry_points: &[u32],
ef: usize,
layer: usize,
df: &impl Fn(u32) -> f32,
) -> Vec<Cand> {
use std::cmp::Reverse;
use std::collections::BinaryHeap;
let mut visited = std::collections::HashSet::with_capacity(ef * 4);
let mut candidates: BinaryHeap<Reverse<Cand>> = BinaryHeap::new();
let mut w: BinaryHeap<Cand> = BinaryHeap::new();
for &e in entry_points {
if visited.insert(e) {
let c = Cand { id: e, dist: df(e) };
candidates.push(Reverse(c));
w.push(c);
}
}
while w.len() > ef {
w.pop();
}
while let Some(Reverse(c)) = candidates.pop() {
let farthest = w.peek().map(|f| f.dist).unwrap_or(f32::INFINITY);
if c.dist > farthest && w.len() >= ef {
break;
}
let neighbours = match self.links.get(c.id as usize).and_then(|l| l.get(layer)) {
Some(n) => n,
None => continue,
};
for &e in neighbours {
if visited.insert(e) {
let d = df(e);
let farthest = w.peek().map(|f| f.dist).unwrap_or(f32::INFINITY);
if d < farthest || w.len() < ef {
let cand = Cand { id: e, dist: d };
candidates.push(Reverse(cand));
w.push(cand);
if w.len() > ef {
w.pop();
}
}
}
}
}
w.into_vec()
}
pub fn search(
&self,
query: &[f32],
query_norm: f32,
k: usize,
ef: Option<usize>,
data: &[f32],
norms: &[f32],
) -> Vec<(u32, f32)> {
if self.len == 0 || k == 0 {
return Vec::new();
}
let ctx = DistCtx {
data,
norms,
dim: self.dim,
metric: self.metric,
};
let ef = ef.unwrap_or(self.params.ef_search).max(k);
let entry = match self.entry_point {
Some(e) => e,
None => return Vec::new(),
};
let df = |id: u32| ctx.dist_query(query, query_norm, id);
let mut ep = vec![entry];
for lc in (1..=self.max_level).rev() {
let w = self.search_layer(&ctx, &ep, 1, lc, &df);
if let Some(best) = w.into_iter().min() {
ep = vec![best.id];
}
}
let mut w = self.search_layer(&ctx, &ep, ef, 0, &df);
w.sort_unstable();
w.truncate(k);
w.into_iter().map(|c| (c.id, c.dist)).collect()
}
}
fn level_for(seed: u64, counter: u64, m: usize) -> usize {
let mut rng = SplitMix64(seed ^ counter.wrapping_mul(0x2545_F491_4F6C_DD1D));
let m_l = 1.0 / (m as f64).max(2.0).ln();
(-rng.unit().ln() * m_l).floor() as usize
}
fn select_neighbors(ctx: &DistCtx, base: u32, candidates: &[Cand], m: usize) -> Vec<u32> {
let mut sorted: Vec<Cand> = candidates
.iter()
.copied()
.filter(|c| c.id != base)
.collect();
sorted.sort_unstable();
let mut result: Vec<u32> = Vec::with_capacity(m);
let mut deferred: Vec<u32> = Vec::new();
for c in &sorted {
if result.len() >= m {
break;
}
let closer_to_base = result.iter().all(|&r| ctx.dist_ids(c.id, r) > c.dist);
if closer_to_base {
result.push(c.id);
} else {
deferred.push(c.id);
}
}
for id in deferred {
if result.len() >= m {
break;
}
result.push(id);
}
result
}
fn search_layer_locked(
links: &[RwLock<Vec<Vec<u32>>>],
entry_points: &[u32],
ef: usize,
layer: usize,
df: &impl Fn(u32) -> f32,
) -> Vec<Cand> {
use std::cmp::Reverse;
use std::collections::BinaryHeap;
let mut visited = std::collections::HashSet::with_capacity(ef * 4);
let mut candidates: BinaryHeap<Reverse<Cand>> = BinaryHeap::new();
let mut w: BinaryHeap<Cand> = BinaryHeap::new();
for &e in entry_points {
if visited.insert(e) {
let c = Cand { id: e, dist: df(e) };
candidates.push(Reverse(c));
w.push(c);
}
}
while w.len() > ef {
w.pop();
}
while let Some(Reverse(c)) = candidates.pop() {
let farthest = w.peek().map(|f| f.dist).unwrap_or(f32::INFINITY);
if c.dist > farthest && w.len() >= ef {
break;
}
let neighbours: Vec<u32> = match links.get(c.id as usize) {
Some(lock) => lock.read().unwrap().get(layer).cloned().unwrap_or_default(),
None => continue,
};
for e in neighbours {
if visited.insert(e) {
let d = df(e);
let farthest = w.peek().map(|f| f.dist).unwrap_or(f32::INFINITY);
if d < farthest || w.len() < ef {
let cand = Cand { id: e, dist: d };
candidates.push(Reverse(cand));
w.push(cand);
if w.len() > ef {
w.pop();
}
}
}
}
}
w.into_vec()
}
fn insert_concurrent(
slot: u32,
ctx: &DistCtx,
params: &HnswParams,
node_levels: &[u8],
links: &[RwLock<Vec<Vec<u32>>>],
ep_state: &RwLock<(u32, usize)>,
) {
let level = node_levels[slot as usize] as usize;
let df = |id: u32| ctx.dist_ids(slot, id);
let (entry, top) = *ep_state.read().unwrap();
let mut ep = vec![entry];
if top > level {
for lc in (level + 1..=top).rev() {
let w = search_layer_locked(links, &ep, 1, lc, &df);
if let Some(best) = w.into_iter().min() {
ep = vec![best.id];
}
}
}
let start = top.min(level);
for lc in (0..=start).rev() {
let w = search_layer_locked(links, &ep, params.ef_construction, lc, &df);
let m_max = if lc == 0 { params.m * 2 } else { params.m };
let selected = select_neighbors(ctx, slot, &w, params.m);
{
let mut g = links[slot as usize].write().unwrap();
if lc < g.len() {
g[lc] = selected.clone();
}
}
for &e in &selected {
let mut eg = links[e as usize].write().unwrap();
if lc >= eg.len() {
continue; }
eg[lc].push(slot);
if eg[lc].len() > m_max {
let cands: Vec<Cand> = eg[lc]
.iter()
.map(|&id| Cand {
id,
dist: ctx.dist_ids(e, id),
})
.collect();
eg[lc] = select_neighbors(ctx, e, &cands, m_max);
}
}
ep = w.iter().map(|c| c.id).collect();
if ep.is_empty() {
ep = vec![entry];
}
}
if level > top {
let mut g = ep_state.write().unwrap();
if level > g.1 {
*g = (slot, level);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_data(n: usize, dim: usize, seed: u64) -> (Vec<f32>, Vec<f32>) {
let mut rng = SplitMix64(seed);
let mut data = Vec::with_capacity(n * dim);
for _ in 0..n * dim {
let u = rng.unit() as f32;
let v = rng.unit() as f32;
data.push((u - 0.5) * 2.0 + (v - 0.5));
}
let mut norms = Vec::with_capacity(n);
for i in 0..n {
let s = i * dim;
let nn: f32 = data[s..s + dim].iter().map(|x| x * x).sum::<f32>().sqrt();
norms.push(nn);
}
(data, norms)
}
fn brute_topk(
data: &[f32],
norms: &[f32],
dim: usize,
metric: HnswMetric,
query: &[f32],
qnorm: f32,
k: usize,
) -> Vec<u32> {
let n = data.len() / dim;
let ctx = DistCtx {
data,
norms,
dim,
metric,
};
let mut all: Vec<Cand> = (0..n as u32)
.map(|id| Cand {
id,
dist: ctx.dist_query(query, qnorm, id),
})
.collect();
all.sort_unstable();
all.truncate(k);
all.into_iter().map(|c| c.id).collect()
}
fn recall_at_k(metric: HnswMetric, n: usize, dim: usize, k: usize) -> f64 {
let (data, norms) = make_data(n, dim, 0xABCD);
let index = HnswIndex::build(&data, &norms, dim, metric, HnswParams::default(), 42);
assert_eq!(index.len(), n);
let mut hits = 0usize;
let mut total = 0usize;
let n_queries = 50.min(n);
for q in 0..n_queries {
let qs = q * dim;
let query = &data[qs..qs + dim];
let qnorm = norms[q];
let truth = brute_topk(&data, &norms, dim, metric, query, qnorm, k);
let got: Vec<u32> = index
.search(query, qnorm, k, Some(100), &data, &norms)
.into_iter()
.map(|(id, _)| id)
.collect();
let truth_set: std::collections::HashSet<u32> = truth.into_iter().collect();
for g in got {
if truth_set.contains(&g) {
hits += 1;
}
}
total += k;
}
hits as f64 / total as f64
}
#[test]
fn test_recall_cosine() {
let r = recall_at_k(HnswMetric::Cosine, 2000, 32, 10);
assert!(r > 0.90, "cosine recall@10 too low: {}", r);
}
#[test]
fn test_recall_euclidean() {
let r = recall_at_k(HnswMetric::Euclidean, 2000, 32, 10);
assert!(r > 0.90, "euclidean recall@10 too low: {}", r);
}
#[test]
fn test_recall_dot() {
let r = recall_at_k(HnswMetric::Dot, 2000, 32, 10);
assert!(r > 0.85, "dot recall@10 too low: {}", r);
}
#[test]
fn test_empty_and_single() {
let index = HnswIndex::build(&[], &[], 4, HnswMetric::Cosine, HnswParams::default(), 1);
assert!(index.is_empty());
assert!(index
.search(&[1.0, 0.0, 0.0, 0.0], 1.0, 5, None, &[], &[])
.is_empty());
let data = vec![1.0, 0.0, 0.0, 0.0];
let norms = vec![1.0];
let index = HnswIndex::build(
&data,
&norms,
4,
HnswMetric::Cosine,
HnswParams::default(),
1,
);
assert_eq!(index.len(), 1);
let res = index.search(&[1.0, 0.0, 0.0, 0.0], 1.0, 5, None, &data, &norms);
assert_eq!(res.len(), 1);
assert_eq!(res[0].0, 0);
}
#[test]
fn test_k_larger_than_n() {
let (data, norms) = make_data(5, 8, 7);
let index = HnswIndex::build(
&data,
&norms,
8,
HnswMetric::Cosine,
HnswParams::default(),
3,
);
let qs = &data[0..8];
let res = index.search(qs, norms[0], 100, None, &data, &norms);
assert_eq!(res.len(), 5, "k>n should return all n");
}
#[test]
fn test_incremental_matches_build_recall() {
let (data, norms) = make_data(1500, 24, 0x1234);
let mut index = HnswIndex {
params: HnswParams::default(),
metric: HnswMetric::Cosine,
dim: 24,
len: 0,
node_levels: Vec::new(),
links: Vec::new(),
entry_point: None,
max_level: 0,
seed: 99,
insert_counter: 0,
};
for slot in 0..1500u32 {
index.insert(slot, &data, &norms, 24);
}
assert_eq!(index.len(), 1500);
let mut hits = 0;
for q in 0..40 {
let qs = q * 24;
let query = &data[qs..qs + 24];
let truth = brute_topk(&data, &norms, 24, HnswMetric::Cosine, query, norms[q], 10);
let got: std::collections::HashSet<u32> = index
.search(query, norms[q], 10, Some(100), &data, &norms)
.into_iter()
.map(|(id, _)| id)
.collect();
for t in truth {
if got.contains(&t) {
hits += 1;
}
}
}
let recall = hits as f64 / (40 * 10) as f64;
assert!(recall > 0.90, "incremental recall too low: {}", recall);
}
#[test]
fn test_deterministic_levels_concurrent_build() {
let (data, norms) = make_data(400, 16, 55);
let a = HnswIndex::build(
&data,
&norms,
16,
HnswMetric::Cosine,
HnswParams::default(),
7,
);
let b = HnswIndex::build(
&data,
&norms,
16,
HnswMetric::Cosine,
HnswParams::default(),
7,
);
assert_eq!(
a.node_levels, b.node_levels,
"seeded levels must be deterministic"
);
assert_eq!(a.len(), b.len());
assert_eq!(a.len(), 400);
for (slot, layers) in a.links.iter().enumerate() {
for (lc, nbrs) in layers.iter().enumerate() {
let m_max = if lc == 0 { a.params.m * 2 } else { a.params.m };
assert!(
nbrs.len() <= m_max,
"node {} layer {} over m_max: {} > {}",
slot,
lc,
nbrs.len(),
m_max
);
}
}
}
#[test]
fn test_metric_subset_mapping() {
assert_eq!(
HnswMetric::from_distance(DistanceMetric::Cosine),
Some(HnswMetric::Cosine)
);
assert_eq!(
HnswMetric::from_distance(DistanceMetric::DotProduct),
Some(HnswMetric::Dot)
);
assert_eq!(
HnswMetric::from_distance(DistanceMetric::Euclidean),
Some(HnswMetric::Euclidean)
);
assert_eq!(HnswMetric::from_distance(DistanceMetric::Poincare), None);
}
}