1use std::cell::RefCell;
7#[cfg(not(target_arch = "wasm32"))]
8use std::sync::Arc;
9
10use nodedb_types::hnsw::HnswParams;
11use nodedb_types::vector_dtype::VectorStorageDtype;
12
13use crate::hnsw::arena::BeamSearchArena;
14
15use super::super::ARENA_INITIAL_CAPACITY;
16use super::super::types::{Node, Xorshift64};
17
18pub struct HnswIndex {
24 pub(crate) params: HnswParams,
25 pub(crate) dim: usize,
26 pub(crate) nodes: Vec<Node>,
27 pub(crate) entry_point: Option<u32>,
28 pub(crate) max_layer: usize,
29 pub(crate) rng: Xorshift64,
30 pub(crate) flat_neighbors: Option<crate::hnsw::flat_neighbors::FlatNeighborStore>,
34 #[cfg(not(target_arch = "wasm32"))]
40 pub(crate) backing: Option<Arc<dyn crate::segment_backing::VectorSegmentBacking>>,
41 pub(crate) arena: RefCell<BeamSearchArena>,
49}
50
51impl HnswIndex {
52 pub fn new(dim: usize, params: HnswParams) -> Self {
54 Self::seeded(dim, params, 42)
55 }
56
57 pub fn with_seed(dim: usize, params: HnswParams, seed: u64) -> Self {
59 Self::seeded(dim, params, seed)
60 }
61
62 fn seeded(dim: usize, params: HnswParams, seed: u64) -> Self {
63 let initial_capacity = params.ef_construction.max(ARENA_INITIAL_CAPACITY);
64 Self {
65 dim,
66 nodes: Vec::new(),
67 entry_point: None,
68 max_layer: 0,
69 rng: Xorshift64::new(seed),
70 flat_neighbors: None,
71 arena: RefCell::new(BeamSearchArena::new(initial_capacity)),
72 params,
73 #[cfg(not(target_arch = "wasm32"))]
74 backing: None,
75 }
76 }
77
78 pub fn metric(&self) -> crate::distance::DistanceMetric {
82 self.params.metric
83 }
84
85 pub fn params(&self) -> &HnswParams {
86 &self.params
87 }
88
89 pub fn dim(&self) -> usize {
90 self.dim
91 }
92
93 pub fn dtype(&self) -> VectorStorageDtype {
95 self.params.dtype
96 }
97
98 pub fn entry_point(&self) -> Option<u32> {
99 self.entry_point
100 }
101
102 pub fn max_layer(&self) -> usize {
103 self.max_layer
104 }
105
106 pub fn rng_state(&self) -> u64 {
108 self.rng.0
109 }
110
111 pub fn memory_usage_bytes(&self) -> usize {
113 let vector_bytes = self.nodes.len() * self.params.dtype.bytes_for_dim(self.dim);
114 let neighbor_bytes: usize = self
115 .nodes
116 .iter()
117 .map(|n| {
118 n.neighbors
119 .iter()
120 .map(|layer| layer.len() * 4)
121 .sum::<usize>()
122 })
123 .sum();
124 let node_overhead = self.nodes.len() * std::mem::size_of::<Node>();
125 vector_bytes + neighbor_bytes + node_overhead
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 #[cfg(not(target_arch = "wasm32"))]
133 use std::sync::Arc;
134
135 use super::{HnswIndex, HnswParams};
136 use crate::distance::DistanceMetric;
137 use crate::error::VectorError;
138 use nodedb_types::vector_dtype::VectorStorageDtype;
139
140 fn make_params(dtype: VectorStorageDtype) -> HnswParams {
141 HnswParams {
142 m: 4,
143 m0: 8,
144 ef_construction: 32,
145 metric: DistanceMetric::L2,
146 dtype,
147 }
148 }
149
150 #[test]
151 fn create_empty_index() {
152 let idx = HnswIndex::new(3, HnswParams::default());
153 assert_eq!(idx.len(), 0);
154 assert!(idx.is_empty());
155 assert!(idx.entry_point().is_none());
156 }
157
158 #[test]
159 fn params_default() {
160 let p = HnswParams::default();
161 assert_eq!(p.m, 16);
162 assert_eq!(p.m0, 32);
163 assert_eq!(p.ef_construction, 200);
164 assert_eq!(p.metric, DistanceMetric::Cosine);
165 assert_eq!(p.dtype, VectorStorageDtype::F32);
166 }
167
168 #[test]
169 fn candidate_ordering() {
170 let a = crate::hnsw::graph::types::Candidate { dist: 0.1, id: 1 };
171 let b = crate::hnsw::graph::types::Candidate { dist: 0.5, id: 2 };
172 assert!(a < b);
173 }
174
175 #[test]
176 fn f32_default_unchanged() {
177 let mut idx = HnswIndex::with_seed(3, make_params(VectorStorageDtype::F32), 1);
178 assert_eq!(idx.dtype(), VectorStorageDtype::F32);
179 for i in 0..10u32 {
180 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
181 }
182 let v = idx.get_vector(3).unwrap();
184 assert_eq!(v[0], 3.0_f32);
185 assert_eq!(idx.get_vector_bytes(3).unwrap().len(), 12); }
188
189 #[test]
190 fn f16_insert_search_smoke() {
191 let mut idx = HnswIndex::with_seed(3, make_params(VectorStorageDtype::F16), 42);
192 assert_eq!(idx.dtype(), VectorStorageDtype::F16);
193 for i in 0..10u32 {
194 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
195 }
196 let results = idx.search(&[5.0, 0.0, 0.0], 3, 32).unwrap();
197 assert_eq!(results.len(), 3);
198 for w in results.windows(2) {
200 assert!(
201 w[0].distance <= w[1].distance,
202 "results not sorted: {:?}",
203 results
204 );
205 }
206 }
207
208 #[test]
209 fn bf16_insert_search_smoke() {
210 let mut idx = HnswIndex::with_seed(3, make_params(VectorStorageDtype::BF16), 42);
211 assert_eq!(idx.dtype(), VectorStorageDtype::BF16);
212 for i in 0..10u32 {
213 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
214 }
215 let results = idx.search(&[5.0, 0.0, 0.0], 3, 32).unwrap();
216 assert_eq!(results.len(), 3);
217 for w in results.windows(2) {
218 assert!(
219 w[0].distance <= w[1].distance,
220 "results not sorted: {:?}",
221 results
222 );
223 }
224 }
225
226 #[test]
227 fn get_vector_returns_none_on_non_f32_dtype() {
228 let mut idx = HnswIndex::with_seed(3, make_params(VectorStorageDtype::F16), 1);
229 idx.insert(vec![1.0, 2.0, 3.0]).unwrap();
230 assert!(idx.get_vector_bytes(0).is_some());
233 #[cfg(not(debug_assertions))]
234 assert!(idx.get_vector(0).is_none());
235 }
236
237 #[test]
240 fn materialize_vector_decodes_every_dtype() {
241 for dtype in [
242 VectorStorageDtype::F32,
243 VectorStorageDtype::F16,
244 VectorStorageDtype::BF16,
245 ] {
246 let mut idx = HnswIndex::with_seed(3, make_params(dtype), 1);
247 idx.insert(vec![1.0, 2.0, 4.0]).unwrap();
248 let v = idx
249 .materialize_vector(0)
250 .unwrap_or_else(|e| panic!("dtype={dtype:?} must materialize, got {e}"));
251 assert_eq!(v.len(), 3, "dtype={dtype:?}");
252 assert_eq!(v, vec![1.0, 2.0, 4.0], "dtype={dtype:?}");
254 }
255 }
256
257 #[cfg(not(target_arch = "wasm32"))]
263 #[test]
264 fn get_vector_or_backing_serves_narrow_dtypes() {
265 for dtype in [VectorStorageDtype::F16, VectorStorageDtype::BF16] {
266 let mut idx = HnswIndex::with_seed(3, make_params(dtype), 1);
267 idx.insert(vec![1.0, 2.0, 4.0]).unwrap();
268 let v = idx
269 .get_vector_or_backing(0)
270 .unwrap_or_else(|| panic!("dtype={dtype:?} must serve a rerank vector"));
271 assert_eq!(&*v, &[1.0, 2.0, 4.0][..], "dtype={dtype:?}");
272 }
273 }
274
275 #[test]
280 fn extractors_reject_an_index_with_no_vector_source() {
281 let mut idx = HnswIndex::with_seed(3, make_params(VectorStorageDtype::F32), 1);
282 idx.insert(vec![1.0, 2.0, 3.0]).unwrap();
283 let graph_only = idx.graph_checkpoint_to_bytes().unwrap();
284 let restored = HnswIndex::from_checkpoint(&graph_only)
286 .unwrap()
287 .expect("graph checkpoint must be recognized");
288 assert_eq!(restored.len(), 1, "graph topology must survive");
289
290 assert!(
291 matches!(
292 restored.materialize_vector(0),
293 Err(VectorError::VectorUnavailable { id: 0 })
294 ),
295 "materialize_vector must report the missing vector source"
296 );
297 assert!(
298 restored.export_vectors().is_err(),
299 "export_vectors must not yield empty placeholder vectors"
300 );
301 assert!(
302 restored.extract_vectors_and_surrogates().is_err(),
303 "extract_vectors_and_surrogates must not yield empty placeholder vectors"
304 );
305 assert!(
306 restored.checkpoint_to_bytes().is_err(),
307 "a full checkpoint cannot be written from an index with no vector data"
308 );
309 }
310
311 #[cfg(not(target_arch = "wasm32"))]
318 #[test]
319 fn with_backing_refuses_a_backing_that_cannot_serve_the_index() {
320 use crate::segment_backing::VectorSegmentBacking;
321
322 struct LyingBacking {
325 len: usize,
326 dim: usize,
327 served: Vec<Vec<f32>>,
328 }
329 impl VectorSegmentBacking for LyingBacking {
330 fn len(&self) -> usize {
331 self.len
332 }
333 fn dim(&self) -> usize {
334 self.dim
335 }
336 fn get_vector(&self, id: u32) -> Option<&[f32]> {
337 self.served.get(id as usize).map(Vec::as_slice)
338 }
339 fn get_surrogate(&self, _id: u32) -> Option<u64> {
340 None
341 }
342 }
343
344 let mut src = HnswIndex::with_seed(3, make_params(VectorStorageDtype::F32), 1);
345 src.insert(vec![1.0, 2.0, 3.0]).unwrap();
346 src.insert(vec![4.0, 5.0, 6.0]).unwrap();
347 let graph_only = src.graph_checkpoint_to_bytes().unwrap();
348 let restore = || {
349 HnswIndex::from_checkpoint(&graph_only)
350 .unwrap()
351 .expect("graph checkpoint must be recognized")
352 };
353
354 let mut idx = restore();
356 assert!(
357 idx.with_backing(Arc::new(LyingBacking {
358 len: 2,
359 dim: 3,
360 served: Vec::new(),
361 }))
362 .is_err(),
363 "a backing that serves no vectors must be refused"
364 );
365
366 let mut idx = restore();
368 assert!(
369 idx.with_backing(Arc::new(LyingBacking {
370 len: 1,
371 dim: 3,
372 served: vec![vec![1.0, 2.0, 3.0]],
373 }))
374 .is_err(),
375 "a backing shorter than the node count must be refused"
376 );
377
378 let mut idx = restore();
380 assert!(
381 matches!(
382 idx.with_backing(Arc::new(LyingBacking {
383 len: 2,
384 dim: 4,
385 served: vec![vec![0.0; 4], vec![0.0; 4]],
386 })),
387 Err(VectorError::StoredDimensionMismatch { .. })
388 ),
389 "a backing with the wrong dim must be refused"
390 );
391
392 let mut idx = restore();
395 idx.with_backing(Arc::new(LyingBacking {
396 len: 2,
397 dim: 3,
398 served: vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]],
399 }))
400 .expect("a complete backing must attach");
401 assert_eq!(idx.materialize_vector(0).unwrap(), vec![1.0, 2.0, 3.0]);
402 assert_eq!(idx.materialize_vector(1).unwrap(), vec![4.0, 5.0, 6.0]);
403 }
404
405 #[test]
408 fn search_before_with_backing_returns_no_vectorless_hits() {
409 use crate::test_support::{graph_only_restore, seeded_index, seeded_vectors};
410
411 let vectors = seeded_vectors(50, 3, 0x9E37_79B9_7F4A_7C15);
412 let src = seeded_index(&vectors, DistanceMetric::L2, VectorStorageDtype::F32);
413 let idx = graph_only_restore(&src);
415 assert_eq!(idx.len(), 50, "graph topology must survive");
416
417 let filter: roaring::RoaringBitmap = (0..50u32).collect();
418 let unfiltered = idx.search(&vectors[0], 5, 64).unwrap();
419 let filtered = idx.search_filtered(&vectors[0], 5, 64, &filter).unwrap();
420 assert!(unfiltered.is_empty(), "got {unfiltered:?}");
421 assert!(filtered.is_empty(), "got {filtered:?}");
422 }
423
424 #[test]
425 fn get_vector_bytes_works_for_all_dtypes() {
426 for (dtype, expected_byte_len) in [
427 (VectorStorageDtype::F32, 12usize), (VectorStorageDtype::F16, 6usize), (VectorStorageDtype::BF16, 6usize), ] {
431 let mut idx = HnswIndex::with_seed(3, make_params(dtype), 1);
432 idx.insert(vec![1.0, 2.0, 3.0]).unwrap();
433 let bytes = idx.get_vector_bytes(0).expect("must be Some for valid id");
434 assert_eq!(
435 bytes.len(),
436 expected_byte_len,
437 "wrong byte len for dtype={dtype:?}"
438 );
439 }
440 }
441}