use crate::dtype::cast_from_f32;
use crate::error::VectorError;
use crate::hnsw::graph::{Candidate, HnswIndex, Node, NodeStorage};
use crate::hnsw::search::search_layer;
use nodedb_types::vector_dtype::VectorStorageDtype;
impl HnswIndex {
pub fn insert(&mut self, vector: Vec<f32>) -> Result<(), VectorError> {
self.ensure_mutable_neighbors();
if vector.len() != self.dim {
return Err(VectorError::DimensionMismatch {
expected: self.dim,
got: vector.len(),
});
}
let new_id = self.nodes.len() as u32;
let new_layer = self.random_layer();
let storage = match self.params.dtype {
VectorStorageDtype::F32 => NodeStorage::F32(vector.clone()),
dtype => NodeStorage::Bytes {
dtype,
bytes: cast_from_f32(&vector, dtype),
},
};
let node = Node {
storage,
neighbors: (0..=new_layer).map(|_| Vec::new()).collect(),
deleted: false,
};
self.nodes.push(node);
let Some(ep) = self.entry_point else {
self.entry_point = Some(new_id);
self.max_layer = new_layer;
return Ok(());
};
let probe = self.probe(&vector);
let mut current_ep = ep;
if self.max_layer > new_layer {
for layer in (new_layer + 1..=self.max_layer).rev() {
let results = search_layer(self, &probe, current_ep, 1, layer, None, 0);
if let Some(nearest) = results.first() {
current_ep = nearest.id;
}
}
}
let insert_top = new_layer.min(self.max_layer);
let mut reached_live = false;
for layer in (0..=insert_top).rev() {
let ef = self.params.ef_construction;
let candidates = search_layer(self, &probe, current_ep, ef, layer, None, 0);
if layer == 0 {
reached_live = !candidates.is_empty();
}
let m = self.max_neighbors(layer);
let selected = select_neighbors_heuristic(self, &candidates, m);
self.nodes[new_id as usize].neighbors[layer] = selected.iter().map(|c| c.id).collect();
for neighbor in &selected {
let nid = neighbor.id as usize;
self.nodes[nid].neighbors[layer].push(new_id);
if self.nodes[nid].neighbors[layer].len() > m {
self.prune_neighbors(neighbor.id, layer, m);
}
}
if let Some(nearest) = candidates.first() {
current_ep = nearest.id;
}
}
if !reached_live || new_layer > self.max_layer {
self.entry_point = Some(new_id);
self.max_layer = new_layer;
}
Ok(())
}
fn prune_neighbors(&mut self, node_id: u32, layer: usize, m: usize) {
let metric = self.params.metric;
let node_idx = node_id as usize;
let selected: Vec<u32> = {
let probe = self.node_probe(node_id);
let mut candidates: Vec<Candidate> = self.nodes[node_idx].neighbors[layer]
.iter()
.map(|&nid| Candidate {
id: nid,
dist: probe
.as_ref()
.and_then(|p| self.dist_to_node(p, nid, metric))
.unwrap_or(f32::INFINITY),
})
.collect();
candidates.sort_unstable_by(|a, b| a.dist.total_cmp(&b.dist));
select_neighbors_heuristic(self, &candidates, m)
.iter()
.map(|c| c.id)
.collect()
};
self.nodes[node_idx].neighbors[layer] = selected;
}
}
pub(crate) fn select_neighbors_heuristic(
index: &HnswIndex,
candidates: &[Candidate],
m: usize,
) -> Vec<Candidate> {
let metric = index.params.metric;
let mut selected: Vec<Candidate> = Vec::with_capacity(m);
for candidate in candidates {
if selected.len() >= m {
break;
}
let candidate_probe = index.node_probe(candidate.id);
let is_diverse = selected.iter().all(|s| {
let dist_to_selected = candidate_probe
.as_ref()
.and_then(|p| index.dist_to_node(p, s.id, metric))
.unwrap_or(f32::MAX);
candidate.dist <= dist_to_selected
});
if is_diverse {
selected.push(*candidate);
}
}
if selected.len() < m {
let selected_ids: std::collections::HashSet<u32> = selected.iter().map(|c| c.id).collect();
for candidate in candidates {
if selected.len() >= m {
break;
}
if !selected_ids.contains(&candidate.id) {
selected.push(*candidate);
}
}
}
selected
}
#[cfg(test)]
mod tests {
use nodedb_types::vector_dtype::VectorStorageDtype;
use crate::distance::DistanceMetric;
use crate::hnsw::{HnswIndex, HnswParams};
fn make_index() -> HnswIndex {
HnswIndex::with_seed(
3,
HnswParams {
m: 4,
m0: 8,
ef_construction: 32,
metric: DistanceMetric::L2,
dtype: nodedb_types::vector_dtype::VectorStorageDtype::F32,
},
12345,
)
}
#[test]
fn insert_single() {
let mut idx = make_index();
idx.insert(vec![1.0, 0.0, 0.0]).unwrap();
assert_eq!(idx.len(), 1);
assert_eq!(idx.entry_point(), Some(0));
}
#[test]
fn insert_many_maintains_invariants() {
let mut idx = make_index();
for i in 0..100 {
let v = vec![(i as f32) * 0.1, (i as f32) * 0.2, (i as f32) * 0.3];
idx.insert(v).unwrap();
}
assert_eq!(idx.len(), 100);
assert!(idx.entry_point().is_some());
for node in &idx.nodes {
assert!(node.neighbors[0].len() <= idx.params.m0);
}
for node in &idx.nodes {
for (layer, neighbors) in node.neighbors.iter().enumerate().skip(1) {
assert!(
neighbors.len() <= idx.params.m,
"layer {layer} neighbor count {} exceeds m={}",
neighbors.len(),
idx.params.m
);
}
}
}
#[test]
fn all_nodes_reachable_from_entry() {
let mut idx = make_index();
for i in 0..20 {
idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
}
for target in 0..20u32 {
let query = idx.get_vector(target).unwrap().to_vec();
let results = idx.search(&query, 1, 32).unwrap();
assert_eq!(results[0].id, target, "node {target} not reachable");
}
}
#[test]
fn narrow_dtype_inserts_reach_every_node() {
use crate::test_support::{seeded_index, seeded_vectors};
let vectors = seeded_vectors(120, 8, 0x2545_F491_4F6C_DD1D);
for dtype in [VectorStorageDtype::F16, VectorStorageDtype::BF16] {
for metric in [DistanceMetric::L2, DistanceMetric::Cosine] {
let idx = seeded_index(&vectors, metric, dtype);
for (id, v) in vectors.iter().enumerate() {
let hits = idx.search(v, 1, 128).unwrap();
assert_eq!(
hits.first().map(|h| h.id),
Some(id as u32),
"dtype={dtype:?} metric={metric:?}: exact query misses node {id}"
);
}
for node in &idx.nodes {
assert!(node.neighbors[0].len() <= idx.params.m0);
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn inserts_after_graph_only_restore_match_a_full_restore() {
use crate::test_support::{graph_only_restore_with_backing, seeded_index, seeded_vectors};
const DIM: usize = 8;
let base = seeded_vectors(200, DIM, 0x9E37_79B9_7F4A_7C15);
let extra = seeded_vectors(5, DIM, 0xD1B5_4A32_D192_ED03);
let src = seeded_index(&base, DistanceMetric::L2, VectorStorageDtype::F32);
let mut full = HnswIndex::from_checkpoint(&src.checkpoint_to_bytes().unwrap())
.unwrap()
.expect("full checkpoint must be recognized");
let mut graph_only = graph_only_restore_with_backing(&src, &base);
for v in &extra {
full.insert(v.clone()).unwrap();
graph_only.insert(v.clone()).unwrap();
}
let neighbors = graph_only.export_neighbors();
assert_eq!(
full.export_neighbors(),
neighbors,
"a graph-only restore must build the same graph as a full restore"
);
for (offset, v) in extra.iter().enumerate() {
let new_id = (base.len() + offset) as u32;
let hits = graph_only.search(v, 1, 64).unwrap();
assert_eq!(
hits.first().map(|h| h.id),
Some(new_id),
"exact query for inserted node {new_id} returns it, got {hits:?}"
);
assert!(
hits[0].distance < 1e-6,
"exact query for node {new_id} scores ~0, got {}",
hits[0].distance
);
assert!(
neighbors
.iter()
.any(|layers| layers.first().is_some_and(|l0| l0.contains(&new_id))),
"node {new_id} has no incoming layer-0 edge"
);
}
let m = graph_only.params().m;
let m0 = graph_only.params().m0;
for (id, layers) in neighbors.iter().enumerate() {
for (layer, list) in layers.iter().enumerate() {
let cap = if layer == 0 { m0 } else { m };
assert!(
list.len() <= cap,
"node {id} layer {layer} has {} neighbors, cap {cap}",
list.len()
);
}
}
}
#[test]
fn compact_removes_tombstones() {
let mut idx = make_index();
for i in 0..20u32 {
idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
}
for i in (0..20u32).step_by(2) {
assert!(idx.delete(i));
}
assert_eq!(idx.compact(), 10);
assert_eq!(idx.len(), 10);
for target_old_id in (1..20u32).step_by(2) {
let query = vec![target_old_id as f32, 0.0, 0.0];
let results = idx.search(&query, 1, 32).unwrap();
assert!(!results.is_empty());
let found_vec = idx.get_vector(results[0].id).unwrap();
assert_eq!(found_vec[0], target_old_id as f32);
}
}
#[test]
fn insert_after_every_node_is_deleted_stays_searchable() {
for seed in 1..=32u64 {
let mut idx = HnswIndex::with_seed(
3,
HnswParams {
m: 4,
m0: 8,
ef_construction: 32,
metric: DistanceMetric::L2,
dtype: VectorStorageDtype::F32,
},
seed,
);
idx.insert(vec![1.0, 0.0, 0.0]).unwrap();
assert!(idx.delete(0));
let fresh = [[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.5, 0.5, 0.0]];
for v in fresh {
idx.insert(v.to_vec()).unwrap();
}
let ep = idx.entry_point().unwrap();
assert!(!idx.is_deleted(ep), "seed {seed}: entry point is deleted");
for (i, v) in fresh.iter().enumerate() {
let hits = idx.search(v, 1, 32).unwrap();
assert_eq!(
hits.first().map(|h| h.id),
Some(i as u32 + 1),
"seed {seed}: node {} unreachable",
i + 1
);
}
}
}
}