1use crate::dtype::cast_from_f32;
6use crate::error::VectorError;
7use crate::hnsw::graph::{Candidate, HnswIndex, Node, NodeStorage};
8use crate::hnsw::search::search_layer;
9use nodedb_types::vector_dtype::VectorStorageDtype;
10
11impl HnswIndex {
12 pub fn insert(&mut self, vector: Vec<f32>) -> Result<(), VectorError> {
20 self.ensure_mutable_neighbors();
22
23 if vector.len() != self.dim {
24 return Err(VectorError::DimensionMismatch {
25 expected: self.dim,
26 got: vector.len(),
27 });
28 }
29
30 let new_id = self.nodes.len() as u32;
31 let new_layer = self.random_layer();
32
33 let storage = match self.params.dtype {
34 VectorStorageDtype::F32 => NodeStorage::F32(vector.clone()),
35 dtype => NodeStorage::Bytes {
36 dtype,
37 bytes: cast_from_f32(&vector, dtype),
38 },
39 };
40 let node = Node {
41 storage,
42 neighbors: (0..=new_layer).map(|_| Vec::new()).collect(),
43 deleted: false,
44 };
45 self.nodes.push(node);
46
47 let Some(ep) = self.entry_point else {
48 self.entry_point = Some(new_id);
49 self.max_layer = new_layer;
50 return Ok(());
51 };
52
53 let probe = self.probe(&vector);
55
56 let mut current_ep = ep;
57
58 if self.max_layer > new_layer {
60 for layer in (new_layer + 1..=self.max_layer).rev() {
61 let results = search_layer(self, &probe, current_ep, 1, layer, None, 0);
62 if let Some(nearest) = results.first() {
63 current_ep = nearest.id;
64 }
65 }
66 }
67
68 let insert_top = new_layer.min(self.max_layer);
70 let mut reached_live = false;
71 for layer in (0..=insert_top).rev() {
72 let ef = self.params.ef_construction;
73 let candidates = search_layer(self, &probe, current_ep, ef, layer, None, 0);
74 if layer == 0 {
75 reached_live = !candidates.is_empty();
76 }
77
78 let m = self.max_neighbors(layer);
79 let selected = select_neighbors_heuristic(self, &candidates, m);
80
81 self.nodes[new_id as usize].neighbors[layer] = selected.iter().map(|c| c.id).collect();
82
83 for neighbor in &selected {
84 let nid = neighbor.id as usize;
85 self.nodes[nid].neighbors[layer].push(new_id);
86
87 if self.nodes[nid].neighbors[layer].len() > m {
88 self.prune_neighbors(neighbor.id, layer, m);
89 }
90 }
91
92 if let Some(nearest) = candidates.first() {
93 current_ep = nearest.id;
94 }
95 }
96
97 if !reached_live || new_layer > self.max_layer {
102 self.entry_point = Some(new_id);
103 self.max_layer = new_layer;
104 }
105
106 Ok(())
107 }
108
109 fn prune_neighbors(&mut self, node_id: u32, layer: usize, m: usize) {
111 let metric = self.params.metric;
112 let node_idx = node_id as usize;
113 let selected: Vec<u32> = {
114 let probe = self.node_probe(node_id);
115 let mut candidates: Vec<Candidate> = self.nodes[node_idx].neighbors[layer]
116 .iter()
117 .map(|&nid| Candidate {
118 id: nid,
119 dist: probe
120 .as_ref()
121 .and_then(|p| self.dist_to_node(p, nid, metric))
122 .unwrap_or(f32::INFINITY),
123 })
124 .collect();
125 candidates.sort_unstable_by(|a, b| a.dist.total_cmp(&b.dist));
126 select_neighbors_heuristic(self, &candidates, m)
127 .iter()
128 .map(|c| c.id)
129 .collect()
130 };
131 self.nodes[node_idx].neighbors[layer] = selected;
132 }
133}
134
135pub(crate) fn select_neighbors_heuristic(
137 index: &HnswIndex,
138 candidates: &[Candidate],
139 m: usize,
140) -> Vec<Candidate> {
141 let metric = index.params.metric;
142 let mut selected: Vec<Candidate> = Vec::with_capacity(m);
143
144 for candidate in candidates {
145 if selected.len() >= m {
146 break;
147 }
148
149 let candidate_probe = index.node_probe(candidate.id);
151 let is_diverse = selected.iter().all(|s| {
152 let dist_to_selected = candidate_probe
153 .as_ref()
154 .and_then(|p| index.dist_to_node(p, s.id, metric))
155 .unwrap_or(f32::MAX);
156 candidate.dist <= dist_to_selected
157 });
158
159 if is_diverse {
160 selected.push(*candidate);
161 }
162 }
163
164 if selected.len() < m {
166 let selected_ids: std::collections::HashSet<u32> = selected.iter().map(|c| c.id).collect();
167 for candidate in candidates {
168 if selected.len() >= m {
169 break;
170 }
171 if !selected_ids.contains(&candidate.id) {
172 selected.push(*candidate);
173 }
174 }
175 }
176
177 selected
178}
179
180#[cfg(test)]
181mod tests {
182 use nodedb_types::vector_dtype::VectorStorageDtype;
183
184 use crate::distance::DistanceMetric;
185 use crate::hnsw::{HnswIndex, HnswParams};
186
187 fn make_index() -> HnswIndex {
188 HnswIndex::with_seed(
189 3,
190 HnswParams {
191 m: 4,
192 m0: 8,
193 ef_construction: 32,
194 metric: DistanceMetric::L2,
195 dtype: nodedb_types::vector_dtype::VectorStorageDtype::F32,
196 },
197 12345,
198 )
199 }
200
201 #[test]
202 fn insert_single() {
203 let mut idx = make_index();
204 idx.insert(vec![1.0, 0.0, 0.0]).unwrap();
205 assert_eq!(idx.len(), 1);
206 assert_eq!(idx.entry_point(), Some(0));
207 }
208
209 #[test]
210 fn insert_many_maintains_invariants() {
211 let mut idx = make_index();
212 for i in 0..100 {
213 let v = vec![(i as f32) * 0.1, (i as f32) * 0.2, (i as f32) * 0.3];
214 idx.insert(v).unwrap();
215 }
216 assert_eq!(idx.len(), 100);
217 assert!(idx.entry_point().is_some());
218
219 for node in &idx.nodes {
220 assert!(node.neighbors[0].len() <= idx.params.m0);
221 }
222 for node in &idx.nodes {
223 for (layer, neighbors) in node.neighbors.iter().enumerate().skip(1) {
224 assert!(
225 neighbors.len() <= idx.params.m,
226 "layer {layer} neighbor count {} exceeds m={}",
227 neighbors.len(),
228 idx.params.m
229 );
230 }
231 }
232 }
233
234 #[test]
235 fn all_nodes_reachable_from_entry() {
236 let mut idx = make_index();
237 for i in 0..20 {
238 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
239 }
240
241 for target in 0..20u32 {
242 let query = idx.get_vector(target).unwrap().to_vec();
243 let results = idx.search(&query, 1, 32).unwrap();
244 assert_eq!(results[0].id, target, "node {target} not reachable");
245 }
246 }
247
248 #[test]
251 fn narrow_dtype_inserts_reach_every_node() {
252 use crate::test_support::{seeded_index, seeded_vectors};
253
254 let vectors = seeded_vectors(120, 8, 0x2545_F491_4F6C_DD1D);
255 for dtype in [VectorStorageDtype::F16, VectorStorageDtype::BF16] {
256 for metric in [DistanceMetric::L2, DistanceMetric::Cosine] {
257 let idx = seeded_index(&vectors, metric, dtype);
258 for (id, v) in vectors.iter().enumerate() {
259 let hits = idx.search(v, 1, 128).unwrap();
261 assert_eq!(
262 hits.first().map(|h| h.id),
263 Some(id as u32),
264 "dtype={dtype:?} metric={metric:?}: exact query misses node {id}"
265 );
266 }
267 for node in &idx.nodes {
268 assert!(node.neighbors[0].len() <= idx.params.m0);
269 }
270 }
271 }
272 }
273
274 #[cfg(not(target_arch = "wasm32"))]
281 #[test]
282 fn inserts_after_graph_only_restore_match_a_full_restore() {
283 use crate::test_support::{graph_only_restore_with_backing, seeded_index, seeded_vectors};
284
285 const DIM: usize = 8;
286 let base = seeded_vectors(200, DIM, 0x9E37_79B9_7F4A_7C15);
287 let extra = seeded_vectors(5, DIM, 0xD1B5_4A32_D192_ED03);
288
289 let src = seeded_index(&base, DistanceMetric::L2, VectorStorageDtype::F32);
290 let mut full = HnswIndex::from_checkpoint(&src.checkpoint_to_bytes().unwrap())
291 .unwrap()
292 .expect("full checkpoint must be recognized");
293 let mut graph_only = graph_only_restore_with_backing(&src, &base);
294
295 for v in &extra {
296 full.insert(v.clone()).unwrap();
297 graph_only.insert(v.clone()).unwrap();
298 }
299
300 let neighbors = graph_only.export_neighbors();
301 assert_eq!(
302 full.export_neighbors(),
303 neighbors,
304 "a graph-only restore must build the same graph as a full restore"
305 );
306
307 for (offset, v) in extra.iter().enumerate() {
308 let new_id = (base.len() + offset) as u32;
309 let hits = graph_only.search(v, 1, 64).unwrap();
310 assert_eq!(
311 hits.first().map(|h| h.id),
312 Some(new_id),
313 "exact query for inserted node {new_id} returns it, got {hits:?}"
314 );
315 assert!(
316 hits[0].distance < 1e-6,
317 "exact query for node {new_id} scores ~0, got {}",
318 hits[0].distance
319 );
320 assert!(
321 neighbors
322 .iter()
323 .any(|layers| layers.first().is_some_and(|l0| l0.contains(&new_id))),
324 "node {new_id} has no incoming layer-0 edge"
325 );
326 }
327
328 let m = graph_only.params().m;
329 let m0 = graph_only.params().m0;
330 for (id, layers) in neighbors.iter().enumerate() {
331 for (layer, list) in layers.iter().enumerate() {
332 let cap = if layer == 0 { m0 } else { m };
333 assert!(
334 list.len() <= cap,
335 "node {id} layer {layer} has {} neighbors, cap {cap}",
336 list.len()
337 );
338 }
339 }
340 }
341
342 #[test]
343 fn compact_removes_tombstones() {
344 let mut idx = make_index();
345 for i in 0..20u32 {
346 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
347 }
348
349 for i in (0..20u32).step_by(2) {
350 assert!(idx.delete(i));
351 }
352 assert_eq!(idx.compact(), 10);
353 assert_eq!(idx.len(), 10);
354
355 for target_old_id in (1..20u32).step_by(2) {
356 let query = vec![target_old_id as f32, 0.0, 0.0];
357 let results = idx.search(&query, 1, 32).unwrap();
358 assert!(!results.is_empty());
359 let found_vec = idx.get_vector(results[0].id).unwrap();
360 assert_eq!(found_vec[0], target_old_id as f32);
361 }
362 }
363
364 #[test]
368 fn insert_after_every_node_is_deleted_stays_searchable() {
369 for seed in 1..=32u64 {
370 let mut idx = HnswIndex::with_seed(
371 3,
372 HnswParams {
373 m: 4,
374 m0: 8,
375 ef_construction: 32,
376 metric: DistanceMetric::L2,
377 dtype: VectorStorageDtype::F32,
378 },
379 seed,
380 );
381 idx.insert(vec![1.0, 0.0, 0.0]).unwrap();
382 assert!(idx.delete(0));
383 let fresh = [[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.5, 0.5, 0.0]];
384 for v in fresh {
385 idx.insert(v.to_vec()).unwrap();
386 }
387 let ep = idx.entry_point().unwrap();
388 assert!(!idx.is_deleted(ep), "seed {seed}: entry point is deleted");
389 for (i, v) in fresh.iter().enumerate() {
390 let hits = idx.search(v, 1, 32).unwrap();
391 assert_eq!(
392 hits.first().map(|h| h.id),
393 Some(i as u32 + 1),
394 "seed {seed}: node {} unreachable",
395 i + 1
396 );
397 }
398 }
399 }
400}