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.
- checksums.yaml +4 -4
- data/ext/Cargo.toml +12 -6
- data/ext/db-index/Cargo.toml +29 -0
- data/ext/db-index/src/hnsw/error.rs +66 -0
- data/ext/db-index/src/hnsw/graph_store.rs +272 -0
- data/ext/db-index/src/hnsw/id.rs +36 -0
- data/ext/db-index/src/hnsw/implementation.rs +1517 -0
- data/ext/db-index/src/hnsw/kernels.rs +1228 -0
- data/ext/db-index/src/hnsw/metric.rs +244 -0
- data/ext/db-index/src/hnsw/scalar.rs +72 -0
- data/ext/db-index/src/hnsw/simple_store.rs +140 -0
- data/ext/db-index/src/hnsw/store.rs +472 -0
- data/ext/db-index/src/hnsw/vector.rs +105 -0
- data/ext/db-index/src/hnsw/vectors.rs +568 -0
- data/ext/db-index/src/hnsw.rs +42 -0
- data/ext/db-index/src/lib.rs +3 -0
- data/ext/gqlitedb/Cargo.toml +19 -8
- data/ext/gqlitedb/benches/common/pokec.rs +62 -2
- data/ext/gqlitedb/benches/pokec_divan.rs +66 -2
- data/ext/gqlitedb/benches/pokec_iai.rs +60 -3
- data/ext/gqlitedb/release.toml +2 -2
- data/ext/gqlitedb/src/aggregators/arithmetic.rs +3 -1
- data/ext/gqlitedb/src/aggregators/containers.rs +1 -1
- data/ext/gqlitedb/src/aggregators/stats.rs +21 -12
- data/ext/gqlitedb/src/capi.rs +20 -24
- data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
- data/ext/gqlitedb/src/compiler/variables_manager.rs +104 -38
- data/ext/gqlitedb/src/compiler.rs +506 -227
- data/ext/gqlitedb/src/connection.rs +151 -11
- data/ext/gqlitedb/src/consts.rs +1 -2
- data/ext/gqlitedb/src/error.rs +123 -61
- data/ext/gqlitedb/src/functions/common.rs +64 -0
- data/ext/gqlitedb/src/functions/containers.rs +2 -46
- data/ext/gqlitedb/src/functions/edge.rs +1 -1
- data/ext/gqlitedb/src/functions/math.rs +87 -15
- data/ext/gqlitedb/src/functions/node.rs +1 -1
- data/ext/gqlitedb/src/functions/path.rs +48 -11
- data/ext/gqlitedb/src/functions/scalar.rs +47 -4
- data/ext/gqlitedb/src/functions/string.rs +123 -1
- data/ext/gqlitedb/src/functions/value.rs +1 -1
- data/ext/gqlitedb/src/functions.rs +165 -28
- data/ext/gqlitedb/src/graph.rs +1 -9
- data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
- data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
- data/ext/gqlitedb/src/lib.rs +5 -4
- data/ext/gqlitedb/src/parser/gql.pest +1 -1
- data/ext/gqlitedb/src/planner.rs +329 -0
- data/ext/gqlitedb/src/prelude.rs +3 -3
- data/ext/gqlitedb/src/store/pgrx.rs +1 -1
- data/ext/gqlitedb/src/store/postgres.rs +742 -16
- data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
- data/ext/gqlitedb/src/store/redb/index.rs +274 -0
- data/ext/gqlitedb/src/store/redb.rs +1268 -113
- data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
- data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
- data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
- data/ext/gqlitedb/src/store/sqlite.rs +576 -14
- data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
- data/ext/gqlitedb/src/store.rs +140 -29
- data/ext/gqlitedb/src/tests/compiler.rs +207 -10
- data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
- data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
- data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
- data/ext/gqlitedb/src/tests/connection.rs +61 -1
- data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
- data/ext/gqlitedb/src/tests/parser.rs +54 -23
- data/ext/gqlitedb/src/tests/planner.rs +511 -0
- data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
- data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
- data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
- data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
- data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
- data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
- data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
- data/ext/gqlitedb/src/tests/store.rs +78 -14
- data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
- data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
- data/ext/gqlitedb/src/tests.rs +15 -9
- data/ext/gqlitedb/src/utils.rs +6 -1
- data/ext/gqlitedb/src/value/compare.rs +61 -3
- data/ext/gqlitedb/src/value.rs +136 -7
- data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
- data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
- data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
- data/ext/gqliterb/src/lib.rs +53 -8
- data/ext/gqlparser/Cargo.toml +25 -0
- data/ext/gqlparser/README.MD +9 -0
- data/ext/gqlparser/benches/pokec_divan.rs +34 -0
- data/ext/gqlparser/src/common.rs +69 -0
- data/ext/gqlparser/src/gqls/ast.rs +69 -0
- data/ext/gqlparser/src/gqls/constraint.rs +79 -0
- data/ext/gqlparser/src/gqls/error.rs +22 -0
- data/ext/gqlparser/src/gqls/parser.rs +813 -0
- data/ext/gqlparser/src/gqls/prelude.rs +8 -0
- data/ext/gqlparser/src/gqls/properties.rs +115 -0
- data/ext/gqlparser/src/gqls/resolve.rs +207 -0
- data/ext/gqlparser/src/gqls.rs +265 -0
- data/ext/gqlparser/src/lib.rs +7 -0
- data/ext/gqlparser/src/oc/ast.rs +680 -0
- data/ext/gqlparser/src/oc/error.rs +172 -0
- data/ext/gqlparser/src/oc/lexer.rs +429 -0
- data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
- data/ext/gqlparser/src/oc/parser.rs +2005 -0
- data/ext/gqlparser/src/oc.rs +33 -0
- data/ext/gqlparser/src/prelude.rs +3 -0
- data/ext/graphcore/Cargo.toml +1 -0
- data/ext/graphcore/src/error.rs +26 -0
- data/ext/graphcore/src/graph.rs +177 -51
- data/ext/graphcore/src/lib.rs +4 -2
- data/ext/graphcore/src/open_cypher.rs +12 -0
- data/ext/graphcore/src/table.rs +50 -2
- data/ext/graphcore/src/timestamp.rs +127 -104
- data/ext/graphcore/src/value/tensor.rs +739 -0
- data/ext/graphcore/src/value/value_map.rs +1 -1
- data/ext/graphcore/src/value.rs +343 -19
- metadata +93 -28
- data/ext/gqlitedb/src/parser/ast.rs +0 -604
- data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
- 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
|
+
}
|