gqlite 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +12 -6
  3. data/ext/db-index/Cargo.toml +29 -0
  4. data/ext/db-index/src/hnsw/error.rs +66 -0
  5. data/ext/db-index/src/hnsw/graph_store.rs +272 -0
  6. data/ext/db-index/src/hnsw/id.rs +36 -0
  7. data/ext/db-index/src/hnsw/implementation.rs +1517 -0
  8. data/ext/db-index/src/hnsw/kernels.rs +1228 -0
  9. data/ext/db-index/src/hnsw/metric.rs +244 -0
  10. data/ext/db-index/src/hnsw/scalar.rs +72 -0
  11. data/ext/db-index/src/hnsw/simple_store.rs +140 -0
  12. data/ext/db-index/src/hnsw/store.rs +472 -0
  13. data/ext/db-index/src/hnsw/vector.rs +105 -0
  14. data/ext/db-index/src/hnsw/vectors.rs +568 -0
  15. data/ext/db-index/src/hnsw.rs +42 -0
  16. data/ext/db-index/src/lib.rs +3 -0
  17. data/ext/gqlitedb/Cargo.toml +19 -8
  18. data/ext/gqlitedb/benches/common/pokec.rs +62 -2
  19. data/ext/gqlitedb/benches/pokec_divan.rs +66 -2
  20. data/ext/gqlitedb/benches/pokec_iai.rs +60 -3
  21. data/ext/gqlitedb/release.toml +2 -2
  22. data/ext/gqlitedb/src/aggregators/arithmetic.rs +3 -1
  23. data/ext/gqlitedb/src/aggregators/containers.rs +1 -1
  24. data/ext/gqlitedb/src/aggregators/stats.rs +21 -12
  25. data/ext/gqlitedb/src/capi.rs +20 -24
  26. data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
  27. data/ext/gqlitedb/src/compiler/variables_manager.rs +104 -38
  28. data/ext/gqlitedb/src/compiler.rs +506 -227
  29. data/ext/gqlitedb/src/connection.rs +151 -11
  30. data/ext/gqlitedb/src/consts.rs +1 -2
  31. data/ext/gqlitedb/src/error.rs +123 -61
  32. data/ext/gqlitedb/src/functions/common.rs +64 -0
  33. data/ext/gqlitedb/src/functions/containers.rs +2 -46
  34. data/ext/gqlitedb/src/functions/edge.rs +1 -1
  35. data/ext/gqlitedb/src/functions/math.rs +87 -15
  36. data/ext/gqlitedb/src/functions/node.rs +1 -1
  37. data/ext/gqlitedb/src/functions/path.rs +48 -11
  38. data/ext/gqlitedb/src/functions/scalar.rs +47 -4
  39. data/ext/gqlitedb/src/functions/string.rs +123 -1
  40. data/ext/gqlitedb/src/functions/value.rs +1 -1
  41. data/ext/gqlitedb/src/functions.rs +165 -28
  42. data/ext/gqlitedb/src/graph.rs +1 -9
  43. data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
  44. data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
  45. data/ext/gqlitedb/src/lib.rs +5 -4
  46. data/ext/gqlitedb/src/parser/gql.pest +1 -1
  47. data/ext/gqlitedb/src/planner.rs +329 -0
  48. data/ext/gqlitedb/src/prelude.rs +3 -3
  49. data/ext/gqlitedb/src/store/pgrx.rs +1 -1
  50. data/ext/gqlitedb/src/store/postgres.rs +742 -16
  51. data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
  52. data/ext/gqlitedb/src/store/redb/index.rs +274 -0
  53. data/ext/gqlitedb/src/store/redb.rs +1268 -113
  54. data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
  55. data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
  56. data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
  57. data/ext/gqlitedb/src/store/sqlite.rs +576 -14
  58. data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
  59. data/ext/gqlitedb/src/store.rs +140 -29
  60. data/ext/gqlitedb/src/tests/compiler.rs +207 -10
  61. data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
  62. data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
  63. data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
  64. data/ext/gqlitedb/src/tests/connection.rs +61 -1
  65. data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
  66. data/ext/gqlitedb/src/tests/parser.rs +54 -23
  67. data/ext/gqlitedb/src/tests/planner.rs +511 -0
  68. data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
  69. data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
  70. data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
  71. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
  72. data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
  73. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
  74. data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
  75. data/ext/gqlitedb/src/tests/store.rs +78 -14
  76. data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
  77. data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
  78. data/ext/gqlitedb/src/tests.rs +15 -9
  79. data/ext/gqlitedb/src/utils.rs +6 -1
  80. data/ext/gqlitedb/src/value/compare.rs +61 -3
  81. data/ext/gqlitedb/src/value.rs +136 -7
  82. data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
  83. data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
  84. data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
  85. data/ext/gqliterb/src/lib.rs +53 -8
  86. data/ext/gqlparser/Cargo.toml +25 -0
  87. data/ext/gqlparser/README.MD +9 -0
  88. data/ext/gqlparser/benches/pokec_divan.rs +34 -0
  89. data/ext/gqlparser/src/common.rs +69 -0
  90. data/ext/gqlparser/src/gqls/ast.rs +69 -0
  91. data/ext/gqlparser/src/gqls/constraint.rs +79 -0
  92. data/ext/gqlparser/src/gqls/error.rs +22 -0
  93. data/ext/gqlparser/src/gqls/parser.rs +813 -0
  94. data/ext/gqlparser/src/gqls/prelude.rs +8 -0
  95. data/ext/gqlparser/src/gqls/properties.rs +115 -0
  96. data/ext/gqlparser/src/gqls/resolve.rs +207 -0
  97. data/ext/gqlparser/src/gqls.rs +265 -0
  98. data/ext/gqlparser/src/lib.rs +7 -0
  99. data/ext/gqlparser/src/oc/ast.rs +680 -0
  100. data/ext/gqlparser/src/oc/error.rs +172 -0
  101. data/ext/gqlparser/src/oc/lexer.rs +429 -0
  102. data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
  103. data/ext/gqlparser/src/oc/parser.rs +2005 -0
  104. data/ext/gqlparser/src/oc.rs +33 -0
  105. data/ext/gqlparser/src/prelude.rs +3 -0
  106. data/ext/graphcore/Cargo.toml +1 -0
  107. data/ext/graphcore/src/error.rs +26 -0
  108. data/ext/graphcore/src/graph.rs +177 -51
  109. data/ext/graphcore/src/lib.rs +4 -2
  110. data/ext/graphcore/src/open_cypher.rs +12 -0
  111. data/ext/graphcore/src/table.rs +50 -2
  112. data/ext/graphcore/src/timestamp.rs +127 -104
  113. data/ext/graphcore/src/value/tensor.rs +739 -0
  114. data/ext/graphcore/src/value/value_map.rs +1 -1
  115. data/ext/graphcore/src/value.rs +343 -19
  116. metadata +93 -28
  117. data/ext/gqlitedb/src/parser/ast.rs +0 -604
  118. data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
  119. data/ext/gqlitedb/src/parser.rs +0 -4
@@ -0,0 +1,1517 @@
1
+ use super::error::Error;
2
+ use super::error::Result;
3
+ use super::graph_store::GraphStoreRead;
4
+ use super::graph_store::GraphStoreWrite;
5
+ use super::id::NodeId;
6
+ use super::vector::VectorFamily;
7
+ use super::vector::VectorRef;
8
+ use super::vector::VectorView;
9
+ use crate::hnsw::metric::Metric;
10
+ use crate::hnsw::vectors::VectorStore;
11
+ use crate::hnsw::vectors::VectorStoreMut;
12
+ use ahash::HashSetExt;
13
+ use ordered_float::OrderedFloat;
14
+ use std::collections::BinaryHeap;
15
+ use tracing::warn;
16
+
17
+ fn splitmix64(mut x: u64) -> u64
18
+ {
19
+ x = x.wrapping_add(0x9E3779B97F4A7C15);
20
+ x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
21
+ x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB);
22
+ x ^ (x >> 31)
23
+ }
24
+
25
+ pub(crate) fn hash_level(seed: u64, node_id: u32, mult: f64) -> i32
26
+ {
27
+ let h = splitmix64(seed ^ ((node_id as u64) << 1 | 1));
28
+ let u = ((h >> 11) + 1) as f64 / (1u64 << 53) as f64;
29
+ (-u.ln() * mult) as i32
30
+ }
31
+
32
+ #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
33
+ pub struct HnswConfig
34
+ {
35
+ pub dim: usize,
36
+ pub max_nodes: usize,
37
+ pub m: usize,
38
+ pub ef_construction: usize,
39
+ pub ef_search: usize,
40
+ pub seed: u64,
41
+ }
42
+
43
+ impl HnswConfig
44
+ {
45
+ pub fn new(dim: usize, max_nodes: usize) -> Self
46
+ {
47
+ Self {
48
+ dim,
49
+ max_nodes,
50
+ m: 16,
51
+ ef_construction: 200,
52
+ ef_search: 50,
53
+ seed: 100,
54
+ }
55
+ }
56
+
57
+ pub fn m(mut self, m: usize) -> Self
58
+ {
59
+ self.m = m;
60
+ self
61
+ }
62
+
63
+ pub fn ef_construction(mut self, ef: usize) -> Self
64
+ {
65
+ self.ef_construction = ef;
66
+ self
67
+ }
68
+
69
+ pub fn ef_search(mut self, ef: usize) -> Self
70
+ {
71
+ self.ef_search = ef;
72
+ self
73
+ }
74
+
75
+ pub fn seed(mut self, seed: u64) -> Self
76
+ {
77
+ self.seed = seed;
78
+ self
79
+ }
80
+ }
81
+
82
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
83
+ pub enum SetOutcome
84
+ {
85
+ Inserted,
86
+ Updated,
87
+ Resurrected,
88
+ }
89
+
90
+ #[derive(Debug, Clone, PartialEq)]
91
+ pub struct SearchHit<K>
92
+ {
93
+ pub key: K,
94
+ pub distance: f32,
95
+ }
96
+
97
+ pub struct HnswIndex<M>
98
+ where
99
+ M: Metric,
100
+ {
101
+ metric: M,
102
+ cfg: HnswConfig,
103
+ max_m: usize,
104
+ max_m0: usize,
105
+ mult: f64,
106
+ ef_construction: usize,
107
+ }
108
+
109
+ impl<M> HnswIndex<M>
110
+ where
111
+ M: Metric,
112
+ {
113
+ pub fn new(metric: M, mut cfg: HnswConfig) -> Self
114
+ {
115
+ assert!(cfg.max_nodes <= u32::MAX as usize);
116
+ assert!(cfg.dim > 0, "dim must be > 0");
117
+ assert!(cfg.m >= 2, "m must be >= 2");
118
+
119
+ let m = if cfg.m <= 10_000
120
+ {
121
+ cfg.m
122
+ }
123
+ else
124
+ {
125
+ warn!("m={} exceeds 10000; capping to 10000", cfg.m);
126
+ 10_000
127
+ };
128
+ cfg.m = m;
129
+
130
+ let max_m = m;
131
+ let max_m0 = m * 2;
132
+ let ef_construction = cfg.ef_construction.max(m);
133
+ cfg.ef_construction = ef_construction;
134
+
135
+ let mult = 1.0 / (m as f64).ln();
136
+
137
+ Self {
138
+ metric,
139
+ cfg,
140
+ max_m,
141
+ max_m0,
142
+ mult,
143
+ ef_construction,
144
+ }
145
+ }
146
+
147
+ pub fn dim(&self) -> usize
148
+ {
149
+ self.cfg.dim
150
+ }
151
+
152
+ pub fn m(&self) -> usize
153
+ {
154
+ self.cfg.m
155
+ }
156
+
157
+ pub fn ef_construction(&self) -> usize
158
+ {
159
+ self.ef_construction
160
+ }
161
+
162
+ pub fn max_nodes(&self) -> usize
163
+ {
164
+ self.cfg.max_nodes
165
+ }
166
+
167
+ pub fn metric(&self) -> &M
168
+ {
169
+ &self.metric
170
+ }
171
+
172
+ pub fn config(&self) -> &HnswConfig
173
+ {
174
+ &self.cfg
175
+ }
176
+
177
+ fn level_for_key<K: std::hash::Hash>(&self, key: &K) -> i32
178
+ {
179
+ // Derive level from key hash, not node id, so the level is determined before
180
+ // allocation. Each node gets its own level assignment based on its key's hash.
181
+ let key_hash = ahash::RandomState::with_seeds(self.cfg.seed, 0, 0, 0).hash_one(key);
182
+ hash_level(self.cfg.seed, key_hash as u32, self.mult)
183
+ }
184
+
185
+ pub fn insert<K, G, V>(
186
+ &self,
187
+ graph: &mut G,
188
+ vectors: &mut V,
189
+ key: K,
190
+ vector: <M::Family as VectorFamily>::Ref<'_>,
191
+ ) -> Result<()>
192
+ where
193
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
194
+ G: GraphStoreWrite<K>,
195
+ V: VectorStoreMut<Family = M::Family>,
196
+ G::Error: Into<Error>,
197
+ {
198
+ if vector.len() != self.cfg.dim
199
+ {
200
+ return Err(Error::DimensionMismatch {
201
+ expected: self.cfg.dim,
202
+ actual: vector.len(),
203
+ });
204
+ }
205
+
206
+ if graph.node_of(&key).map_err(|e| e.into())?.is_some()
207
+ {
208
+ return Err(Error::KeyAlreadyExists);
209
+ }
210
+
211
+ let level = self.level_for_key(&key);
212
+ let node = graph.insert_node(key, level).map_err(|e| e.into())?;
213
+
214
+ // Rollback if vector write fails
215
+ if let Err(e) = vectors.set(node, vector)
216
+ {
217
+ let _ = graph.remove_node(node).map_err(|e2| e2.into());
218
+ return Err(e);
219
+ }
220
+
221
+ // Rollback if graph linking fails
222
+ if let Err(e) = self.add_point_at_level(graph, vectors, node, level)
223
+ {
224
+ let _ = graph.remove_node(node).map_err(|e2| e2.into());
225
+ return Err(e);
226
+ }
227
+
228
+ Ok(())
229
+ }
230
+
231
+ pub fn set<K, G, V>(
232
+ &self,
233
+ graph: &mut G,
234
+ vectors: &mut V,
235
+ key: K,
236
+ vector: <M::Family as VectorFamily>::Ref<'_>,
237
+ ) -> Result<SetOutcome>
238
+ where
239
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
240
+ G: GraphStoreWrite<K>,
241
+ V: VectorStoreMut<Family = M::Family>,
242
+ G::Error: Into<Error>,
243
+ {
244
+ if vector.len() != self.cfg.dim
245
+ {
246
+ return Err(Error::DimensionMismatch {
247
+ expected: self.cfg.dim,
248
+ actual: vector.len(),
249
+ });
250
+ }
251
+
252
+ if let Some(node) = graph.node_of(&key).map_err(|e| e.into())?
253
+ {
254
+ vectors.set(node, vector)?;
255
+ self.update_point(graph, vectors, node)?;
256
+ return Ok(SetOutcome::Updated);
257
+ }
258
+
259
+ let level = self.level_for_key(&key);
260
+ let node = graph.insert_node(key, level).map_err(|e| e.into())?;
261
+
262
+ // Rollback if vector write fails
263
+ if let Err(e) = vectors.set(node, vector)
264
+ {
265
+ let _ = graph.remove_node(node).map_err(|e2| e2.into());
266
+ return Err(e);
267
+ }
268
+
269
+ // Rollback if graph linking fails
270
+ if let Err(e) = self.add_point_at_level(graph, vectors, node, level)
271
+ {
272
+ let _ = graph.remove_node(node).map_err(|e2| e2.into());
273
+ return Err(e);
274
+ }
275
+
276
+ Ok(SetOutcome::Inserted)
277
+ }
278
+
279
+ pub fn delete<K, G>(&self, graph: &mut G, key: &K) -> Result<bool>
280
+ where
281
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
282
+ G: GraphStoreWrite<K>,
283
+ G::Error: Into<Error>,
284
+ {
285
+ let Some(node) = graph.node_of(key).map_err(|e| e.into())?
286
+ else
287
+ {
288
+ return Ok(false);
289
+ };
290
+
291
+ let entry_point_data = graph.entry_point().map_err(|e| e.into())?;
292
+
293
+ graph.remove_node(node).map_err(|e| e.into())?;
294
+
295
+ // If we deleted the entry point, find a new one
296
+ if let Some((ep, _)) = entry_point_data
297
+ {
298
+ if ep == node
299
+ {
300
+ let node_count = graph.node_count().map_err(|e| e.into())?;
301
+ let mut best_level = -1i32;
302
+ let mut best_node = None;
303
+
304
+ // Scan all nodes to find the highest-level survivor
305
+ for i in 0..node_count
306
+ {
307
+ let test_node = NodeId::new(i as u32);
308
+ if graph.contains(test_node).map_err(|e| e.into())?
309
+ {
310
+ let level = graph.level(test_node).map_err(|e| e.into())?;
311
+ if level > best_level
312
+ {
313
+ best_level = level;
314
+ best_node = Some(test_node);
315
+ }
316
+ }
317
+ }
318
+
319
+ let new_ep = best_node.map(|n| (n, best_level));
320
+ graph.set_entry_point(new_ep).map_err(|e| e.into())?;
321
+ }
322
+ }
323
+
324
+ Ok(true)
325
+ }
326
+
327
+ pub fn search<K, G, V>(
328
+ &self,
329
+ graph: &G,
330
+ vectors: &V,
331
+ query: <M::Family as VectorFamily>::Ref<'_>,
332
+ k: usize,
333
+ ef_search: Option<usize>,
334
+ filter: Option<&dyn Fn(&K) -> bool>,
335
+ ) -> Result<Vec<SearchHit<K>>>
336
+ where
337
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
338
+ G: GraphStoreRead<K>,
339
+ V: VectorStore<Family = M::Family>,
340
+ G::Error: Into<Error>,
341
+ {
342
+ if query.len() != self.cfg.dim
343
+ {
344
+ return Err(Error::DimensionMismatch {
345
+ expected: self.cfg.dim,
346
+ actual: query.len(),
347
+ });
348
+ }
349
+
350
+ let ef = std::cmp::max(k + 1, ef_search.unwrap_or(self.cfg.ef_search));
351
+
352
+ let (entry, max_level) = graph
353
+ .entry_point()
354
+ .map_err(|e| e.into())?
355
+ .ok_or(Error::EmptyIndex)?;
356
+ let node_count = graph.node_count().map_err(|e| e.into())?;
357
+ // TODO: visited pool / epoch tags — currently `node_count` is total ever inserted and never shrinks
358
+ let mut visited = vec![false; node_count];
359
+
360
+ let mut curr = entry;
361
+ let mut curdist = self.distance_query_to_node(graph, vectors, query, curr)?;
362
+
363
+ // Traverse upper layers to find better starting point
364
+ for level in (1..=max_level.max(0) as usize).rev()
365
+ {
366
+ let mut changed = true;
367
+ while changed
368
+ {
369
+ changed = false;
370
+ let mut neighbors = Vec::new();
371
+ graph
372
+ .neighbors(curr, level, &mut neighbors)
373
+ .map_err(|e| e.into())?;
374
+ for cand in neighbors
375
+ {
376
+ if !graph.contains(cand).map_err(|e| e.into())?
377
+ {
378
+ continue;
379
+ }
380
+ let d = self.distance_query_to_node(graph, vectors, query, cand)?;
381
+ if d < curdist
382
+ {
383
+ curdist = d;
384
+ curr = cand;
385
+ changed = true;
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ // Search base layer
392
+ let top_candidates =
393
+ self.search_base_layer(graph, vectors, curr, query, 0, ef, &mut visited, filter)?;
394
+
395
+ let mut result: Vec<(OrderedFloat<f32>, K)> = Vec::new();
396
+ for (dist, node) in top_candidates.into_sorted_vec()
397
+ {
398
+ if result.len() >= k
399
+ {
400
+ break;
401
+ }
402
+ if !graph.contains(node).map_err(|e| e.into())?
403
+ {
404
+ continue;
405
+ }
406
+ if let Some(key) = graph.key_of(node).map_err(|e| e.into())?
407
+ {
408
+ let allowed = filter.map(|f| f(&key)).unwrap_or(true);
409
+ if allowed
410
+ {
411
+ result.push((dist, key));
412
+ }
413
+ }
414
+ }
415
+
416
+ let mut hits = Vec::new();
417
+ for (dist, key) in result
418
+ {
419
+ hits.push(SearchHit {
420
+ key,
421
+ distance: dist.0,
422
+ });
423
+ }
424
+ Ok(hits)
425
+ }
426
+
427
+ #[allow(clippy::too_many_arguments)]
428
+ fn search_base_layer<K, G, V>(
429
+ &self,
430
+ graph: &G,
431
+ vectors: &V,
432
+ ep: NodeId,
433
+ query: <M::Family as VectorFamily>::Ref<'_>,
434
+ layer: usize,
435
+ ef: usize,
436
+ visited: &mut [bool],
437
+ filter: Option<&dyn Fn(&K) -> bool>,
438
+ ) -> Result<BinaryHeap<(OrderedFloat<f32>, NodeId)>>
439
+ where
440
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
441
+ G: GraphStoreRead<K>,
442
+ V: VectorStore<Family = M::Family>,
443
+ G::Error: Into<Error>,
444
+ {
445
+ let mut top_candidates: BinaryHeap<(OrderedFloat<f32>, NodeId)> = BinaryHeap::new();
446
+ let mut candidate_set: BinaryHeap<(OrderedFloat<f32>, NodeId)> = BinaryHeap::new();
447
+
448
+ let mut lower_bound;
449
+ let ep_key = graph.key_of(ep).map_err(|e| e.into())?;
450
+ let ep_allowed = if let Some(key) = ep_key
451
+ {
452
+ filter.map(|f| f(&key)).unwrap_or(true)
453
+ }
454
+ else
455
+ {
456
+ false
457
+ };
458
+
459
+ if graph.contains(ep).map_err(|e| e.into())? && ep_allowed
460
+ {
461
+ let dist = self.distance_query_to_node(graph, vectors, query, ep)?;
462
+ lower_bound = dist;
463
+ top_candidates.push((OrderedFloat(dist), ep));
464
+ candidate_set.push((OrderedFloat(-dist), ep));
465
+ }
466
+ else
467
+ {
468
+ lower_bound = f32::INFINITY;
469
+ candidate_set.push((OrderedFloat(-lower_bound), ep));
470
+ }
471
+
472
+ visited[ep.as_usize()] = true;
473
+
474
+ while let Some((neg_dist, cur)) = candidate_set.pop()
475
+ {
476
+ let cur_dist = -neg_dist.0;
477
+ if cur_dist > lower_bound && top_candidates.len() == ef
478
+ {
479
+ break;
480
+ }
481
+
482
+ let mut neighbors = Vec::new();
483
+ graph
484
+ .neighbors(cur, layer, &mut neighbors)
485
+ .map_err(|e| e.into())?;
486
+
487
+ for cand in neighbors
488
+ {
489
+ if visited[cand.as_usize()]
490
+ {
491
+ continue;
492
+ }
493
+ visited[cand.as_usize()] = true;
494
+
495
+ if !graph.contains(cand).map_err(|e| e.into())?
496
+ {
497
+ continue;
498
+ }
499
+
500
+ let dist = self.distance_query_to_node(graph, vectors, query, cand)?;
501
+ let should_consider = top_candidates.len() < ef || lower_bound > dist;
502
+ if !should_consider
503
+ {
504
+ continue;
505
+ }
506
+
507
+ candidate_set.push((OrderedFloat(-dist), cand));
508
+
509
+ let cand_key = graph.key_of(cand).map_err(|e| e.into())?;
510
+ let cand_allowed = if let Some(key) = cand_key
511
+ {
512
+ filter.map(|f| f(&key)).unwrap_or(true)
513
+ }
514
+ else
515
+ {
516
+ false
517
+ };
518
+
519
+ if cand_allowed
520
+ {
521
+ top_candidates.push((OrderedFloat(dist), cand));
522
+ }
523
+
524
+ while top_candidates.len() > ef
525
+ {
526
+ top_candidates.pop();
527
+ }
528
+ if let Some((worst, _)) = top_candidates.peek()
529
+ {
530
+ lower_bound = worst.0;
531
+ }
532
+ }
533
+ }
534
+
535
+ Ok(top_candidates)
536
+ }
537
+
538
+ fn distance_query_to_node<K, G, V>(
539
+ &self,
540
+ _graph: &G,
541
+ vectors: &V,
542
+ query: <M::Family as VectorFamily>::Ref<'_>,
543
+ node: NodeId,
544
+ ) -> Result<f32>
545
+ where
546
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
547
+ G: GraphStoreRead<K>,
548
+ V: VectorStore<Family = M::Family>,
549
+ G::Error: Into<Error>,
550
+ {
551
+ let node_vec = vectors
552
+ .vector(node)
553
+ .ok_or_else(|| Error::InvalidIndexFormat("vector not found".to_string()))?;
554
+ Ok(self.metric.distance(query, node_vec.view()))
555
+ }
556
+
557
+ fn distance_between_nodes<K, G, V>(
558
+ &self,
559
+ _graph: &G,
560
+ vectors: &V,
561
+ a: NodeId,
562
+ b: NodeId,
563
+ ) -> Result<f32>
564
+ where
565
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
566
+ G: GraphStoreRead<K>,
567
+ V: VectorStore<Family = M::Family>,
568
+ G::Error: Into<Error>,
569
+ {
570
+ let a_vec = vectors
571
+ .vector(a)
572
+ .ok_or_else(|| Error::InvalidIndexFormat("vector a not found".to_string()))?;
573
+ let b_vec = vectors
574
+ .vector(b)
575
+ .ok_or_else(|| Error::InvalidIndexFormat("vector b not found".to_string()))?;
576
+ Ok(self.metric.distance(a_vec.view(), b_vec.view()))
577
+ }
578
+
579
+ fn get_neighbors_by_heuristic2<K, G, V>(
580
+ &self,
581
+ graph: &G,
582
+ vectors: &V,
583
+ top_candidates: &mut BinaryHeap<(OrderedFloat<f32>, NodeId)>,
584
+ m: usize,
585
+ ) -> Result<()>
586
+ where
587
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
588
+ G: GraphStoreRead<K>,
589
+ V: VectorStore<Family = M::Family>,
590
+ G::Error: Into<Error>,
591
+ {
592
+ if top_candidates.len() < m
593
+ {
594
+ return Ok(());
595
+ }
596
+
597
+ let mut queue_closest: BinaryHeap<(OrderedFloat<f32>, NodeId)> = BinaryHeap::new();
598
+ while let Some((dist, id)) = top_candidates.pop()
599
+ {
600
+ queue_closest.push((OrderedFloat(-dist.0), id));
601
+ }
602
+
603
+ let mut return_list: Vec<(OrderedFloat<f32>, NodeId)> = Vec::with_capacity(m);
604
+ while let Some((neg_dist_to_query, cur_id)) = queue_closest.pop()
605
+ {
606
+ if return_list.len() >= m
607
+ {
608
+ break;
609
+ }
610
+ let dist_to_query = -neg_dist_to_query.0;
611
+
612
+ let mut good = true;
613
+ for &(_, selected_id) in &return_list
614
+ {
615
+ let cur_dist = self.distance_between_nodes(graph, vectors, selected_id, cur_id)?;
616
+ if cur_dist < dist_to_query
617
+ {
618
+ good = false;
619
+ break;
620
+ }
621
+ }
622
+
623
+ if good
624
+ {
625
+ return_list.push((neg_dist_to_query, cur_id));
626
+ }
627
+ }
628
+
629
+ for (neg_dist, id) in return_list
630
+ {
631
+ top_candidates.push((OrderedFloat(-neg_dist.0), id));
632
+ }
633
+ Ok(())
634
+ }
635
+
636
+ fn mutually_connect_new_element<K, G, V>(
637
+ &self,
638
+ graph: &mut G,
639
+ vectors: &V,
640
+ node: NodeId,
641
+ mut top_candidates: BinaryHeap<(OrderedFloat<f32>, NodeId)>,
642
+ level: usize,
643
+ _is_update: bool,
644
+ ) -> Result<NodeId>
645
+ where
646
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
647
+ G: GraphStoreWrite<K>,
648
+ V: VectorStore<Family = M::Family>,
649
+ G::Error: Into<Error>,
650
+ {
651
+ let cap = if level == 0 { self.max_m0 } else { self.max_m };
652
+ self.get_neighbors_by_heuristic2(graph, vectors, &mut top_candidates, self.cfg.m)?;
653
+ if top_candidates.len() > self.cfg.m
654
+ {
655
+ return Err(Error::InvalidIndexFormat(
656
+ "heuristic returned more than m candidates".to_string(),
657
+ ));
658
+ }
659
+
660
+ let mut selected: Vec<NodeId> = Vec::with_capacity(self.cfg.m);
661
+ while let Some((_dist, id)) = top_candidates.pop()
662
+ {
663
+ selected.push(id);
664
+ }
665
+
666
+ let next_entry = *selected
667
+ .last()
668
+ .ok_or_else(|| Error::InvalidIndexFormat("empty selected neighbor list".to_string()))?;
669
+
670
+ for &neighbor in &selected
671
+ {
672
+ let neighbor_level = graph.level(neighbor).map_err(|e| e.into())?;
673
+ if level > neighbor_level.max(0) as usize
674
+ {
675
+ return Err(Error::InvalidIndexFormat(
676
+ "trying to link on a non-existent level".to_string(),
677
+ ));
678
+ }
679
+ }
680
+
681
+ graph
682
+ .set_neighbors(node, level, &selected)
683
+ .map_err(|e| e.into())?;
684
+ self.connect_backlinks(graph, vectors, node, &selected, level)?;
685
+
686
+ if selected.len() > cap
687
+ {
688
+ return Err(Error::InvalidIndexFormat(
689
+ "too many selected neighbors".to_string(),
690
+ ));
691
+ }
692
+
693
+ Ok(next_entry)
694
+ }
695
+
696
+ fn connect_backlinks<K, G, V>(
697
+ &self,
698
+ graph: &mut G,
699
+ vectors: &V,
700
+ node: NodeId,
701
+ selected: &[NodeId],
702
+ level: usize,
703
+ ) -> Result<()>
704
+ where
705
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
706
+ G: GraphStoreWrite<K>,
707
+ V: VectorStore<Family = M::Family>,
708
+ G::Error: Into<Error>,
709
+ {
710
+ let mcurmax = if level > 0 { self.max_m } else { self.max_m0 };
711
+
712
+ for &neighbor in selected
713
+ {
714
+ if neighbor == node
715
+ {
716
+ return Err(Error::InvalidIndexFormat(
717
+ "trying to connect a node to itself".to_string(),
718
+ ));
719
+ }
720
+ let neighbor_level = graph.level(neighbor).map_err(|e| e.into())?;
721
+ if level > neighbor_level.max(0) as usize
722
+ {
723
+ return Err(Error::InvalidIndexFormat(
724
+ "trying to link on a non-existent level".to_string(),
725
+ ));
726
+ }
727
+
728
+ let mut existing = Vec::new();
729
+ graph
730
+ .neighbors(neighbor, level, &mut existing)
731
+ .map_err(|e| e.into())?;
732
+ let sz_other = existing.len();
733
+ let is_present = existing.contains(&node);
734
+
735
+ if sz_other > mcurmax
736
+ {
737
+ return Err(Error::InvalidIndexFormat(
738
+ "bad neighbor list size".to_string(),
739
+ ));
740
+ }
741
+ if is_present
742
+ {
743
+ continue;
744
+ }
745
+
746
+ if sz_other < mcurmax
747
+ {
748
+ existing.push(node);
749
+ graph
750
+ .set_neighbors(neighbor, level, &existing)
751
+ .map_err(|e| e.into())?;
752
+ continue;
753
+ }
754
+
755
+ let mut candidates: BinaryHeap<(OrderedFloat<f32>, NodeId)> = BinaryHeap::new();
756
+ let d_max = self.distance_between_nodes(graph, vectors, node, neighbor)?;
757
+ candidates.push((OrderedFloat(d_max), node));
758
+
759
+ for &existing_neighbor in &existing
760
+ {
761
+ let dist = self.distance_between_nodes(graph, vectors, existing_neighbor, neighbor)?;
762
+ candidates.push((OrderedFloat(dist), existing_neighbor));
763
+ }
764
+
765
+ self.get_neighbors_by_heuristic2(graph, vectors, &mut candidates, mcurmax)?;
766
+
767
+ let mut new_neighbors: Vec<NodeId> = Vec::with_capacity(candidates.len());
768
+ while let Some((_dist, id)) = candidates.pop()
769
+ {
770
+ new_neighbors.push(id);
771
+ }
772
+
773
+ graph
774
+ .set_neighbors(neighbor, level, &new_neighbors)
775
+ .map_err(|e| e.into())?;
776
+ }
777
+
778
+ Ok(())
779
+ }
780
+
781
+ fn update_point<K, G, V>(&self, graph: &mut G, vectors: &V, node: NodeId) -> Result<()>
782
+ where
783
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
784
+ G: GraphStoreWrite<K>,
785
+ V: VectorStore<Family = M::Family>,
786
+ G::Error: Into<Error>,
787
+ {
788
+ let entry_point_data = graph
789
+ .entry_point()
790
+ .map_err(|e| e.into())?
791
+ .ok_or(Error::EmptyIndex)?;
792
+ let (entry, max_level_copy) = entry_point_data;
793
+
794
+ let node_count = graph.node_count().map_err(|e| e.into())?;
795
+ if entry == node && node_count == 1
796
+ {
797
+ return Ok(());
798
+ }
799
+
800
+ let elem_level = graph.level(node).map_err(|e| e.into())?;
801
+ if elem_level < 0
802
+ {
803
+ return Err(Error::InvalidIndexFormat("node level < 0".to_string()));
804
+ }
805
+
806
+ // Use deterministic pseudo-random based on node id and config seed
807
+ let mut next_random_seed = self.cfg.seed ^ (node.as_u32() as u64);
808
+ let update_neighbor_probability = 1.0 / (self.cfg.m as f32);
809
+
810
+ for layer in 0..=elem_level as usize
811
+ {
812
+ let mut s_cand = ahash::HashSet::<NodeId>::new();
813
+ let mut s_neigh = ahash::HashSet::<NodeId>::new();
814
+
815
+ let mut list_one_hop = Vec::new();
816
+ graph
817
+ .neighbors(node, layer, &mut list_one_hop)
818
+ .map_err(|e| e.into())?;
819
+
820
+ if list_one_hop.is_empty()
821
+ {
822
+ continue;
823
+ }
824
+
825
+ s_cand.insert(node);
826
+
827
+ for el_one_hop in list_one_hop
828
+ {
829
+ s_cand.insert(el_one_hop);
830
+
831
+ // Deterministic random using splitmix64
832
+ next_random_seed = splitmix64(next_random_seed);
833
+ let random_val = ((next_random_seed >> 11) as f64 + 1.0) / (1u64 << 53) as f64;
834
+ if random_val as f32 > update_neighbor_probability
835
+ {
836
+ continue;
837
+ }
838
+
839
+ s_neigh.insert(el_one_hop);
840
+ let mut list_two_hop = Vec::new();
841
+ graph
842
+ .neighbors(el_one_hop, layer, &mut list_two_hop)
843
+ .map_err(|e| e.into())?;
844
+ for el_two_hop in list_two_hop
845
+ {
846
+ s_cand.insert(el_two_hop);
847
+ }
848
+ }
849
+
850
+ for neigh in s_neigh
851
+ {
852
+ let size = if s_cand.contains(&neigh)
853
+ {
854
+ s_cand.len().saturating_sub(1)
855
+ }
856
+ else
857
+ {
858
+ s_cand.len()
859
+ };
860
+ if size == 0
861
+ {
862
+ continue;
863
+ }
864
+
865
+ let elements_to_keep = self.ef_construction.min(size);
866
+ let mut candidates: BinaryHeap<(OrderedFloat<f32>, NodeId)> = BinaryHeap::new();
867
+
868
+ for cand in s_cand.iter().copied()
869
+ {
870
+ if cand == neigh
871
+ {
872
+ continue;
873
+ }
874
+ let dist = self.distance_between_nodes(graph, vectors, neigh, cand)?;
875
+ if candidates.len() < elements_to_keep
876
+ {
877
+ candidates.push((OrderedFloat(dist), cand));
878
+ }
879
+ else if let Some((worst, _)) = candidates.peek()
880
+ {
881
+ if dist < worst.0
882
+ {
883
+ candidates.pop();
884
+ candidates.push((OrderedFloat(dist), cand));
885
+ }
886
+ }
887
+ }
888
+
889
+ let cap = if layer == 0 { self.max_m0 } else { self.max_m };
890
+ self.get_neighbors_by_heuristic2(graph, vectors, &mut candidates, cap)?;
891
+
892
+ let mut new_neighbors: Vec<NodeId> = Vec::with_capacity(candidates.len());
893
+ while let Some((_dist, id)) = candidates.pop()
894
+ {
895
+ new_neighbors.push(id);
896
+ }
897
+ graph
898
+ .set_neighbors(neigh, layer, &new_neighbors)
899
+ .map_err(|e| e.into())?;
900
+ }
901
+ }
902
+
903
+ self.repair_connections_for_update(
904
+ graph,
905
+ vectors,
906
+ entry,
907
+ node,
908
+ elem_level as usize,
909
+ max_level_copy.max(0) as usize,
910
+ )?;
911
+
912
+ Ok(())
913
+ }
914
+
915
+ fn repair_connections_for_update<K, G, V>(
916
+ &self,
917
+ graph: &mut G,
918
+ vectors: &V,
919
+ entry: NodeId,
920
+ node: NodeId,
921
+ node_level: usize,
922
+ max_level: usize,
923
+ ) -> Result<()>
924
+ where
925
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
926
+ G: GraphStoreWrite<K>,
927
+ V: VectorStore<Family = M::Family>,
928
+ G::Error: Into<Error>,
929
+ {
930
+ let mut curr = entry;
931
+
932
+ // Descend to node level if it's below max level
933
+ if node_level < max_level
934
+ {
935
+ let node_vec = vectors.vector(node).ok_or(Error::MissingVector)?;
936
+ let node_vec = node_vec.view();
937
+ let mut curdist = self.distance_query_to_node(graph, vectors, node_vec, curr)?;
938
+ for level in (node_level + 1..=max_level).rev()
939
+ {
940
+ let mut changed = true;
941
+ while changed
942
+ {
943
+ changed = false;
944
+ let mut neighbors = Vec::new();
945
+ graph
946
+ .neighbors(curr, level, &mut neighbors)
947
+ .map_err(|e| e.into())?;
948
+ for cand in neighbors
949
+ {
950
+ let d = self.distance_between_nodes(graph, vectors, node, cand)?;
951
+ if d < curdist
952
+ {
953
+ curdist = d;
954
+ curr = cand;
955
+ changed = true;
956
+ }
957
+ }
958
+ }
959
+ }
960
+ }
961
+
962
+ if node_level > max_level
963
+ {
964
+ return Err(Error::InvalidIndexFormat(
965
+ "node level cannot exceed max level".to_string(),
966
+ ));
967
+ }
968
+
969
+ let node_vec = vectors.vector(node).ok_or(Error::MissingVector)?;
970
+ let node_vec = node_vec.view();
971
+ let node_count = graph.node_count().map_err(|e| e.into())?;
972
+ let mut visited = vec![false; node_count];
973
+
974
+ for level in (0..=node_level).rev()
975
+ {
976
+ visited.fill(false);
977
+ let mut top_candidates = self.search_base_layer(
978
+ graph,
979
+ vectors,
980
+ curr,
981
+ node_vec,
982
+ level,
983
+ self.ef_construction,
984
+ &mut visited,
985
+ None,
986
+ )?;
987
+
988
+ let mut filtered: BinaryHeap<(OrderedFloat<f32>, NodeId)> = BinaryHeap::new();
989
+ while let Some(cand) = top_candidates.pop()
990
+ {
991
+ if cand.1 != node
992
+ {
993
+ filtered.push(cand);
994
+ }
995
+ }
996
+
997
+ if !filtered.is_empty()
998
+ {
999
+ curr = self.mutually_connect_new_element(graph, vectors, node, filtered, level, true)?;
1000
+ }
1001
+ }
1002
+
1003
+ Ok(())
1004
+ }
1005
+
1006
+ fn add_point_at_level<K, G, V>(
1007
+ &self,
1008
+ graph: &mut G,
1009
+ vectors: &V,
1010
+ node: NodeId,
1011
+ cur_level: i32,
1012
+ ) -> Result<()>
1013
+ where
1014
+ K: Eq + std::hash::Hash + Clone + Send + Sync + 'static,
1015
+ G: GraphStoreWrite<K>,
1016
+ V: VectorStore<Family = M::Family>,
1017
+ G::Error: Into<Error>,
1018
+ {
1019
+ if cur_level < 0
1020
+ {
1021
+ return Err(Error::InvalidIndexFormat("level must be >= 0".to_string()));
1022
+ }
1023
+
1024
+ let entry_point_data = graph.entry_point().map_err(|e| e.into())?;
1025
+
1026
+ let (mut curr_obj, max_level_copy) = match entry_point_data
1027
+ {
1028
+ Some((entry, max_level)) => (Some(entry), max_level),
1029
+ None =>
1030
+ {
1031
+ graph
1032
+ .set_entry_point(Some((node, cur_level)))
1033
+ .map_err(|e| e.into())?;
1034
+ return Ok(());
1035
+ }
1036
+ };
1037
+
1038
+ let node_vec = vectors.vector(node).ok_or(Error::MissingVector)?;
1039
+ let node_vec = node_vec.view();
1040
+
1041
+ if max_level_copy >= 0 && cur_level < max_level_copy
1042
+ {
1043
+ let entry = curr_obj.unwrap();
1044
+ let mut curdist = self.distance_query_to_node(graph, vectors, node_vec, entry)?;
1045
+ let mut curr = entry;
1046
+ for level in ((cur_level + 1) as usize..=max_level_copy as usize).rev()
1047
+ {
1048
+ let mut changed = true;
1049
+ while changed
1050
+ {
1051
+ changed = false;
1052
+ let mut neighbors = Vec::new();
1053
+ graph
1054
+ .neighbors(curr, level, &mut neighbors)
1055
+ .map_err(|e| e.into())?;
1056
+ for cand in neighbors
1057
+ {
1058
+ let d = self.distance_query_to_node(graph, vectors, node_vec, cand)?;
1059
+ if d < curdist
1060
+ {
1061
+ curdist = d;
1062
+ curr = cand;
1063
+ changed = true;
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ curr_obj = Some(curr);
1069
+ }
1070
+
1071
+ let mut curr_obj = curr_obj.expect("entry checked above");
1072
+ let max_conn_level = usize::min(cur_level.max(0) as usize, max_level_copy.max(0) as usize);
1073
+ let node_count = graph.node_count().map_err(|e| e.into())?;
1074
+
1075
+ let mut selected_per_level: Vec<Vec<NodeId>> = vec![Vec::new(); max_conn_level + 1];
1076
+ let mut visited = vec![false; node_count];
1077
+
1078
+ for level in (0..=max_conn_level).rev()
1079
+ {
1080
+ visited.fill(false);
1081
+ let mut top_candidates = self.search_base_layer(
1082
+ graph,
1083
+ vectors,
1084
+ curr_obj,
1085
+ node_vec,
1086
+ level,
1087
+ self.ef_construction,
1088
+ &mut visited,
1089
+ None,
1090
+ )?;
1091
+
1092
+ // After bug fix for entry point reassignment (Bug 2), this should never
1093
+ // occur: the entry point is always a level-maximal survivor, and
1094
+ // search_base_layer searches from it. Empty candidates would indicate
1095
+ // an invariant violation.
1096
+ if top_candidates.is_empty()
1097
+ {
1098
+ return Err(Error::InvalidIndexFormat(
1099
+ "add_point_at_level: no candidates found at level (entry point invariant violated)"
1100
+ .to_string(),
1101
+ ));
1102
+ }
1103
+
1104
+ self.get_neighbors_by_heuristic2(graph, vectors, &mut top_candidates, self.cfg.m)?;
1105
+ if top_candidates.len() > self.cfg.m
1106
+ {
1107
+ return Err(Error::InvalidIndexFormat(
1108
+ "heuristic returned more than m candidates".to_string(),
1109
+ ));
1110
+ }
1111
+
1112
+ let mut selected: Vec<NodeId> = Vec::with_capacity(self.cfg.m);
1113
+ while let Some((_dist, id)) = top_candidates.pop()
1114
+ {
1115
+ selected.push(id);
1116
+ }
1117
+
1118
+ let next_entry = *selected
1119
+ .last()
1120
+ .ok_or_else(|| Error::InvalidIndexFormat("empty selected neighbor list".to_string()))?;
1121
+
1122
+ for &neighbor in &selected
1123
+ {
1124
+ let neighbor_level = graph.level(neighbor).map_err(|e| e.into())?;
1125
+ if level > neighbor_level.max(0) as usize
1126
+ {
1127
+ return Err(Error::InvalidIndexFormat(
1128
+ "trying to link on a non-existent level".to_string(),
1129
+ ));
1130
+ }
1131
+ }
1132
+
1133
+ graph
1134
+ .set_neighbors(node, level, &selected)
1135
+ .map_err(|e| e.into())?;
1136
+ selected_per_level[level] = selected;
1137
+ curr_obj = next_entry;
1138
+ }
1139
+
1140
+ for level in (0..=max_conn_level).rev()
1141
+ {
1142
+ self.connect_backlinks(graph, vectors, node, &selected_per_level[level], level)?;
1143
+ }
1144
+
1145
+ if cur_level > max_level_copy
1146
+ {
1147
+ graph
1148
+ .set_entry_point(Some((node, cur_level)))
1149
+ .map_err(|e| e.into())?;
1150
+ }
1151
+
1152
+ Ok(())
1153
+ }
1154
+ }
1155
+
1156
+ // HnswIndex holds only config; all mutable state lives in the graph/vector stores.
1157
+ // This function is never called, but will fail to compile if HnswIndex is not Sync.
1158
+ #[allow(dead_code)]
1159
+ fn assert_hnsw_index_is_sync()
1160
+ {
1161
+ fn is_sync<T: Sync>() {}
1162
+ is_sync::<HnswIndex<super::metric::L2>>();
1163
+ }
1164
+
1165
+ #[cfg(test)]
1166
+ mod tests
1167
+ {
1168
+ use super::*;
1169
+ use crate::hnsw::metric::L2;
1170
+ use crate::hnsw::simple_store::SimpleGraphStore;
1171
+ use crate::hnsw::vectors::InMemoryVectorStore;
1172
+
1173
+ #[test]
1174
+ fn simple_store_passes_conformance()
1175
+ {
1176
+ crate::hnsw::graph_store::conformance_all::<u128, _>(SimpleGraphStore::<u128>::new);
1177
+ }
1178
+
1179
+ #[test]
1180
+ fn hash_level_is_deterministic_and_seed_sensitive()
1181
+ {
1182
+ let mult = 1.0 / (16f64).ln();
1183
+ assert_eq!(hash_level(100, 7, mult), hash_level(100, 7, mult));
1184
+ let differs = (0..64u32).any(|id| hash_level(100, id, mult) != hash_level(101, id, mult));
1185
+ assert!(
1186
+ differs,
1187
+ "different seeds must produce different level sequences"
1188
+ );
1189
+ }
1190
+
1191
+ #[test]
1192
+ fn hash_level_distribution_matches_exponential()
1193
+ {
1194
+ // P(level >= 1) = 1/m. At n=20_000, m=16: expected 1250, sd ~34.
1195
+ let mult = 1.0 / (16f64).ln();
1196
+ let above = (0..20_000u32)
1197
+ .filter(|&id| hash_level(100, id, mult) >= 1)
1198
+ .count();
1199
+ assert!(
1200
+ (1050..1450).contains(&above),
1201
+ "P(level>=1) should be ~1/16 of 20k, got {above}"
1202
+ );
1203
+ }
1204
+
1205
+ #[test]
1206
+ fn set_and_search_roundtrip_exact_nearest()
1207
+ {
1208
+ let cfg = HnswConfig::new(4, 0);
1209
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1210
+ let mut g = SimpleGraphStore::<u128>::new();
1211
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 64);
1212
+
1213
+ for (key, v) in [
1214
+ (1u128, [10.0f32, 0.0, 0.0, 0.0]),
1215
+ (2, [0.0, 10.0, 0.0, 0.0]),
1216
+ (3, [0.0, 0.0, 10.0, 0.0]),
1217
+ (5, [0.1, 0.0, 0.0, 0.0]),
1218
+ ]
1219
+ {
1220
+ let _ = idx.set(&mut g, &mut vectors, key, &v).unwrap();
1221
+ }
1222
+
1223
+ let hits = idx
1224
+ .search(&g, &vectors, &[0.05, 0.0, 0.0, 0.0], 2, None, None)
1225
+ .unwrap();
1226
+ assert_eq!(hits[0].key, 5);
1227
+ assert_eq!(hits[1].key, 1);
1228
+ }
1229
+
1230
+ #[test]
1231
+ fn delete_removes_immediately_and_search_excludes()
1232
+ {
1233
+ let cfg = HnswConfig::new(4, 0);
1234
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1235
+ let mut g = SimpleGraphStore::<u128>::new();
1236
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 64);
1237
+
1238
+ for (key, v) in [
1239
+ (1u128, [1.0f32, 0., 0., 0.]),
1240
+ (2, [2.0, 0., 0., 0.]),
1241
+ (3, [3.0, 0., 0., 0.]),
1242
+ ]
1243
+ {
1244
+ idx.set(&mut g, &mut vectors, key, &v).unwrap();
1245
+ }
1246
+
1247
+ assert!(idx.delete(&mut g, &2u128).unwrap());
1248
+ assert!(
1249
+ !idx.delete(&mut g, &2u128).unwrap(),
1250
+ "second delete returns false"
1251
+ );
1252
+ assert_eq!(g.node_of(&2u128).unwrap(), None);
1253
+
1254
+ let hits = idx
1255
+ .search(&g, &vectors, &[2.0, 0., 0., 0.], 3, None, None)
1256
+ .unwrap();
1257
+ assert_eq!(hits.len(), 2);
1258
+ assert!(!hits.iter().any(|h| h.key == 2));
1259
+ }
1260
+
1261
+ #[test]
1262
+ fn deleting_entry_point_reassigns_and_search_survives()
1263
+ {
1264
+ let cfg = HnswConfig::new(4, 0);
1265
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1266
+ let mut g = SimpleGraphStore::<u128>::new();
1267
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 64);
1268
+
1269
+ for i in 0..5u128
1270
+ {
1271
+ idx
1272
+ .set(&mut g, &mut vectors, i, &[i as f32, 0., 0., 0.])
1273
+ .unwrap();
1274
+ }
1275
+
1276
+ let (ep, _) = g.entry_point().unwrap().unwrap();
1277
+ let ep_key = g.key_of(ep).unwrap().unwrap();
1278
+ assert!(idx.delete(&mut g, &ep_key).unwrap());
1279
+
1280
+ let (new_ep, _) = g
1281
+ .entry_point()
1282
+ .unwrap()
1283
+ .expect("entry point reassigned, not dangling");
1284
+ assert_ne!(new_ep, ep);
1285
+ assert!(g.contains(new_ep).unwrap());
1286
+
1287
+ let hits = idx
1288
+ .search(&g, &vectors, &[0.0, 0., 0., 0.], 4, None, None)
1289
+ .unwrap();
1290
+ assert_eq!(hits.len(), 4);
1291
+ }
1292
+
1293
+ #[test]
1294
+ fn traversal_through_a_stale_link_is_safe()
1295
+ {
1296
+ let cfg = HnswConfig::new(4, 0);
1297
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1298
+ let mut g = SimpleGraphStore::<u128>::new();
1299
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 64);
1300
+
1301
+ for i in 0..4u128
1302
+ {
1303
+ idx
1304
+ .set(&mut g, &mut vectors, i, &[i as f32, 0., 0., 0.])
1305
+ .unwrap();
1306
+ }
1307
+
1308
+ let victim = g.node_of(&1u128).unwrap().unwrap();
1309
+ idx.delete(&mut g, &1u128).unwrap();
1310
+
1311
+ // Force a stale link (delete leaves inbound links; make one explicit so
1312
+ // the test doesn't depend on graph shape):
1313
+ let holder = g.node_of(&0u128).unwrap().unwrap();
1314
+ let mut nbrs = Vec::new();
1315
+ g.neighbors(holder, 0, &mut nbrs).unwrap();
1316
+ nbrs.push(victim);
1317
+ g.set_neighbors(holder, 0, &nbrs).unwrap();
1318
+
1319
+ let hits = idx
1320
+ .search(&g, &vectors, &[1.0, 0., 0., 0.], 3, None, None)
1321
+ .unwrap();
1322
+ assert!(
1323
+ !hits.iter().any(|h| h.key == 1),
1324
+ "removed node never surfaces"
1325
+ );
1326
+ assert_eq!(hits.len(), 3);
1327
+ }
1328
+
1329
+ #[test]
1330
+ fn test_config()
1331
+ {
1332
+ let cfg = HnswConfig::new(10, 1000);
1333
+ assert_eq!(cfg.dim, 10);
1334
+ assert_eq!(cfg.max_nodes, 1000);
1335
+ }
1336
+
1337
+ #[test]
1338
+ fn test_hnsw_index_creation()
1339
+ {
1340
+ let index = HnswIndex::new(L2::<f32>::new(), HnswConfig::new(4, 100));
1341
+ assert_eq!(index.dim(), 4);
1342
+ assert_eq!(index.m(), 16);
1343
+ assert_eq!(index.max_nodes(), 100);
1344
+ }
1345
+
1346
+ /// Invariant: levels are derived per-key, so a large insert batch must
1347
+ /// produce a multi-level graph (P(level>=1) = 1/m per node; at n=1000, m=16,
1348
+ /// expected ~60 nodes above level 0, far beyond noise).
1349
+ #[test]
1350
+ fn insert_produces_varying_levels_across_many_nodes()
1351
+ {
1352
+ let cfg = HnswConfig::new(4, 0).m(16).seed(100);
1353
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1354
+ let mut g = SimpleGraphStore::<u128>::new();
1355
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 2048);
1356
+
1357
+ const N: usize = 1000;
1358
+ for i in 0..N as u128
1359
+ {
1360
+ let v = [i as f32, (i * 7) as f32, (i * 13) as f32, (i * 19) as f32];
1361
+ idx.insert(&mut g, &mut vectors, i, &v).unwrap();
1362
+ }
1363
+
1364
+ let levels: std::collections::HashSet<i32> = (0..N as u32)
1365
+ .map(|raw| g.level(NodeId::from_u32(raw)).unwrap())
1366
+ .collect();
1367
+
1368
+ assert!(
1369
+ levels.len() > 1,
1370
+ "expected multiple distinct levels across {N} real inserts, got only {levels:?} -- \
1371
+ every node received the identical level, meaning hash_level was called with the \
1372
+ same (seed, node_id) pair every time instead of each node's own id"
1373
+ );
1374
+
1375
+ let above_level_0 = (0..N as u32)
1376
+ .filter(|&raw| g.level(NodeId::from_u32(raw)).unwrap() > 0)
1377
+ .count();
1378
+ assert!(
1379
+ above_level_0 > 40,
1380
+ "expected roughly N/m = {} nodes above level 0 at m=16, n={N}, got {above_level_0} -- \
1381
+ far too few for a correctly seeded per-node level assignment",
1382
+ N / 16
1383
+ );
1384
+ }
1385
+
1386
+ /// Invariant: when any node is inserted, if it reaches a level higher than
1387
+ /// the current entry point, the entry point and max_level are updated.
1388
+ /// This ensures max_level advances monotonically as the graph grows.
1389
+ #[test]
1390
+ fn max_level_advances_as_more_nodes_are_inserted()
1391
+ {
1392
+ let cfg = HnswConfig::new(4, 0).m(16).seed(100);
1393
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1394
+ let mut g = SimpleGraphStore::<u128>::new();
1395
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 1024);
1396
+
1397
+ idx
1398
+ .insert(&mut g, &mut vectors, 0u128, &[0.0, 0.0, 0.0, 0.0])
1399
+ .unwrap();
1400
+ let (_, initial_max_level) = g.entry_point().unwrap().unwrap();
1401
+
1402
+ for i in 1..500u128
1403
+ {
1404
+ let v = [i as f32, (i * 3) as f32, (i * 5) as f32, (i * 11) as f32];
1405
+ idx.insert(&mut g, &mut vectors, i, &v).unwrap();
1406
+ }
1407
+
1408
+ let (_, final_max_level) = g.entry_point().unwrap().unwrap();
1409
+ assert!(
1410
+ final_max_level > initial_max_level,
1411
+ "max_level never advanced past the first-inserted node's own level ({initial_max_level}) \
1412
+ across 499 further inserts -- consistent with every node receiving that same level"
1413
+ );
1414
+ }
1415
+
1416
+ /// Invariant: when the entry point is deleted, the replacement must be a
1417
+ /// level-maximal survivor, so max_level never regresses below the true max.
1418
+ #[test]
1419
+ fn deleting_entry_point_reassigns_to_the_highest_level_survivor()
1420
+ {
1421
+ let cfg = HnswConfig::new(4, 0).m(16).seed(100);
1422
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1423
+ let mut g = SimpleGraphStore::<u128>::new();
1424
+ let mut vectors = InMemoryVectorStore::<f32>::new(4, 1024);
1425
+
1426
+ // Enough nodes to guarantee at least one non-entry-point node reaches
1427
+ // a level >= 1, so "highest surviving level" is a real, checkable claim
1428
+ // rather than everything being tied at level 0.
1429
+ const N: usize = 500;
1430
+ for i in 0..N as u128
1431
+ {
1432
+ let v = [i as f32, (i * 3) as f32, (i * 5) as f32, (i * 11) as f32];
1433
+ idx.insert(&mut g, &mut vectors, i, &v).unwrap();
1434
+ }
1435
+
1436
+ let (entry_before, max_level_before) = g.entry_point().unwrap().unwrap();
1437
+ let entry_key = g.key_of(entry_before).unwrap().unwrap();
1438
+ assert!(
1439
+ max_level_before > 0,
1440
+ "test precondition: need a real multi-level graph to distinguish \
1441
+ 'highest survivor' from 'any survivor' -- if this fails, bug 1 is \
1442
+ still present and this test can't isolate bug 2"
1443
+ );
1444
+
1445
+ // Independently compute the true highest-level node among everyone
1446
+ // EXCEPT the entry point, by direct inspection -- this is the ground
1447
+ // truth the delete path's reassignment must match.
1448
+ let mut expected_level = -1i32;
1449
+ let mut expected_node = None;
1450
+ for raw in 0..N as u32
1451
+ {
1452
+ let n = NodeId::from_u32(raw);
1453
+ if n == entry_before || !g.contains(n).unwrap()
1454
+ {
1455
+ continue;
1456
+ }
1457
+ let lvl = g.level(n).unwrap();
1458
+ if lvl > expected_level
1459
+ {
1460
+ expected_level = lvl;
1461
+ expected_node = Some(n);
1462
+ }
1463
+ }
1464
+ let _expected_node = expected_node.expect("graph has more than one node");
1465
+
1466
+ assert!(idx.delete(&mut g, &entry_key).unwrap());
1467
+
1468
+ let (new_entry, new_max_level) = g
1469
+ .entry_point()
1470
+ .unwrap()
1471
+ .expect("entry point must be reassigned, not left empty, when survivors exist");
1472
+
1473
+ // The new entry point must have the maximum level among survivors.
1474
+ assert_eq!(
1475
+ new_max_level, expected_level,
1476
+ "reassigned entry point's level ({new_max_level}) does not match the true \
1477
+ highest level among survivors ({expected_level})"
1478
+ );
1479
+ // Any node at the maximum level is valid, so just verify it exists and has the right level.
1480
+ assert!(g.contains(new_entry).unwrap());
1481
+ assert_eq!(g.level(new_entry).unwrap(), expected_level);
1482
+ }
1483
+
1484
+ /// Adversarial variant: construct a deterministic graph where the
1485
+ /// lowest-numbered surviving node has a LOWER level than a higher-numbered
1486
+ /// survivor. This tests that reassignment picks by level, not by node id.
1487
+ #[test]
1488
+ fn entry_point_reassignment_does_not_prefer_low_node_ids_over_high_levels()
1489
+ {
1490
+ let cfg = HnswConfig::new(4, 0).m(16).seed(100);
1491
+ let idx = HnswIndex::new(L2::<f32>::new(), cfg);
1492
+ let mut g = SimpleGraphStore::<u128>::new();
1493
+
1494
+ // Build a deterministic graph: node 0 (level 0), node 1 (level 2), entry (level 3).
1495
+ let n0 = g.insert_node(0u128, 0).unwrap();
1496
+ let n1 = g.insert_node(1u128, 2).unwrap();
1497
+ let ep = g.insert_node(2u128, 3).unwrap();
1498
+ g.set_entry_point(Some((ep, 3))).unwrap();
1499
+
1500
+ // Delete the entry point.
1501
+ assert!(idx.delete(&mut g, &2u128).unwrap());
1502
+
1503
+ // The new entry point must be node 1 (level 2), not node 0 (level 0).
1504
+ let (new_ep, new_level) = g
1505
+ .entry_point()
1506
+ .unwrap()
1507
+ .expect("entry point must be reassigned when survivors exist");
1508
+
1509
+ assert_eq!(new_level, 2, "entry point should have level 2");
1510
+ assert_eq!(
1511
+ new_ep, n1,
1512
+ "entry point should be the level-2 node, not the level-0 node"
1513
+ );
1514
+
1515
+ let _ = n0; // unused but intentional
1516
+ }
1517
+ }