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,2313 @@
1
+ #[cfg(feature = "redb")]
2
+ mod redb;
3
+ #[cfg(feature = "sqlite")]
4
+ mod sqlite;
5
+
6
+ #[cfg(feature = "postgres")]
7
+ mod postgres;
8
+
9
+ use crate::{
10
+ graph,
11
+ prelude::*,
12
+ store::{self, SelectNodeQuery, Store, TransactionBoxable, VectorIndexSpec, VectorMetric},
13
+ };
14
+
15
+ pub(crate) fn test_vector_operations<S: Store>(store: S)
16
+ {
17
+ let spec = store::VectorIndexSpec {
18
+ name: "embeddings".to_string(),
19
+ dimension: 3,
20
+ metric: store::VectorMetric::L2,
21
+ ..Default::default()
22
+ };
23
+
24
+ let mut tx = store.begin_write().unwrap();
25
+ store.create_vector_index(&mut tx, "default", spec).unwrap();
26
+ tx.close().unwrap();
27
+
28
+ let node1 = graph::Node::new(
29
+ Some("default".to_string()),
30
+ graph::Key::default(),
31
+ graph::labels!("Item"),
32
+ value::ValueMap::new(),
33
+ );
34
+ let node2 = graph::Node::new(
35
+ Some("default".to_string()),
36
+ graph::Key::default(),
37
+ graph::labels!("Item"),
38
+ value::ValueMap::new(),
39
+ );
40
+ let node3 = graph::Node::new(
41
+ Some("default".to_string()),
42
+ graph::Key::default(),
43
+ graph::labels!("Item"),
44
+ value::ValueMap::new(),
45
+ );
46
+
47
+ let mut tx = store.begin_write().unwrap();
48
+ store
49
+ .create_nodes(
50
+ &mut tx,
51
+ "default",
52
+ [node1.clone(), node2.clone(), node3.clone()].iter(),
53
+ )
54
+ .unwrap();
55
+
56
+ let vector1 = vec![1.0, 0.0, 0.0];
57
+ let vector2 = vec![0.9, 0.1, 0.0];
58
+ let vector3 = vec![0.0, 1.0, 0.0];
59
+
60
+ store
61
+ .upsert_vector(
62
+ &mut tx,
63
+ "default",
64
+ "embeddings",
65
+ node1.key(),
66
+ &crate::value::Tensor::from_f32_slice(&vector1),
67
+ )
68
+ .unwrap();
69
+ store
70
+ .upsert_vector(
71
+ &mut tx,
72
+ "default",
73
+ "embeddings",
74
+ node2.key(),
75
+ &crate::value::Tensor::from_f32_slice(&vector2),
76
+ )
77
+ .unwrap();
78
+ store
79
+ .upsert_vector(
80
+ &mut tx,
81
+ "default",
82
+ "embeddings",
83
+ node3.key(),
84
+ &crate::value::Tensor::from_f32_slice(&vector3),
85
+ )
86
+ .unwrap();
87
+
88
+ tx.close().unwrap();
89
+
90
+ let mut tx = store.begin_read().unwrap();
91
+ let query = vec![1.0, 0.0, 0.0];
92
+ let results = store
93
+ .vector_search(
94
+ &mut tx,
95
+ "default",
96
+ "embeddings",
97
+ &crate::value::Tensor::from_f32_slice(&query),
98
+ 2,
99
+ )
100
+ .unwrap();
101
+ tx.close().unwrap();
102
+
103
+ assert_eq!(
104
+ results.len(),
105
+ 2,
106
+ "Expected 2 results, got {}",
107
+ results.len()
108
+ );
109
+
110
+ assert_eq!(results[0].node_id, node1.key());
111
+ assert_eq!(results[1].node_id, node2.key());
112
+
113
+ let mut tx = store.begin_read().unwrap();
114
+ let query2 = vec![0.0, 1.0, 0.0];
115
+ let results2 = store
116
+ .vector_search(
117
+ &mut tx,
118
+ "default",
119
+ "embeddings",
120
+ &crate::value::Tensor::from_f32_slice(&query2),
121
+ 2,
122
+ )
123
+ .unwrap();
124
+ tx.close().unwrap();
125
+
126
+ assert_eq!(results2.len(), 2);
127
+ assert_eq!(results2[0].node_id, node3.key());
128
+ assert_eq!(results2[1].node_id, node2.key());
129
+ }
130
+
131
+ pub(crate) fn test_vector_search_returns_results<S: Store>(store: S)
132
+ {
133
+ let mut tx = store.begin_write().unwrap();
134
+ store
135
+ .create_vector_index(
136
+ &mut tx,
137
+ "default",
138
+ VectorIndexSpec {
139
+ name: "idx".into(),
140
+ dimension: 3,
141
+ metric: VectorMetric::Cosine,
142
+ ..Default::default()
143
+ },
144
+ )
145
+ .unwrap();
146
+
147
+ let node = graph::Node::new(
148
+ Some("default".into()),
149
+ graph::Key::default(),
150
+ graph::labels!("Item"),
151
+ value::ValueMap::new(),
152
+ );
153
+
154
+ store.create_nodes(&mut tx, "default", [&node]).unwrap();
155
+
156
+ store
157
+ .upsert_vector(
158
+ &mut tx,
159
+ "default",
160
+ "idx",
161
+ node.key(),
162
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
163
+ )
164
+ .unwrap();
165
+
166
+ tx.close().unwrap();
167
+
168
+ let mut tx = store.begin_read().unwrap();
169
+ let results = store
170
+ .vector_search(
171
+ &mut tx,
172
+ "default",
173
+ "idx",
174
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
175
+ 10,
176
+ )
177
+ .unwrap();
178
+
179
+ assert!(!results.is_empty(), "Vector search returned no results");
180
+ tx.close().unwrap();
181
+ }
182
+
183
+ pub(crate) fn test_vector_search_k_limit<S: Store>(store: S)
184
+ {
185
+ let mut tx = store.begin_write().unwrap();
186
+ store
187
+ .create_vector_index(
188
+ &mut tx,
189
+ "default",
190
+ VectorIndexSpec {
191
+ name: "idx".into(),
192
+ dimension: 3,
193
+ metric: VectorMetric::Cosine,
194
+ ..Default::default()
195
+ },
196
+ )
197
+ .unwrap();
198
+
199
+ for i in 0..5
200
+ {
201
+ let node = graph::Node::new(
202
+ Some("default".into()),
203
+ graph::Key::new(i as u128),
204
+ graph::labels!("Item"),
205
+ value::ValueMap::new(),
206
+ );
207
+
208
+ store.create_nodes(&mut tx, "default", [&node]).unwrap();
209
+
210
+ let vector = vec![
211
+ i as f32 * 0.1,
212
+ (i as f32 + 1.0) * 0.1,
213
+ (i as f32 + 2.0) * 0.1,
214
+ ];
215
+ store
216
+ .upsert_vector(
217
+ &mut tx,
218
+ "default",
219
+ "idx",
220
+ node.key(),
221
+ &crate::value::Tensor::from_f32_slice(&vector),
222
+ )
223
+ .unwrap();
224
+ }
225
+
226
+ tx.close().unwrap();
227
+
228
+ let mut tx = store.begin_read().unwrap();
229
+ let results = store
230
+ .vector_search(
231
+ &mut tx,
232
+ "default",
233
+ "idx",
234
+ &crate::value::Tensor::from_f32_slice(&[0.5, 0.6, 0.7]),
235
+ 3,
236
+ )
237
+ .unwrap();
238
+
239
+ assert_eq!(results.len(), 3, "Should return exactly k=3 results");
240
+ tx.close().unwrap();
241
+ }
242
+
243
+ pub(crate) fn test_vector_upsert_does_not_duplicate<S: Store>(store: S)
244
+ {
245
+ let mut tx = store.begin_write().unwrap();
246
+
247
+ store
248
+ .create_vector_index(
249
+ &mut tx,
250
+ "default",
251
+ VectorIndexSpec {
252
+ name: "idx".into(),
253
+ dimension: 3,
254
+ ..Default::default()
255
+ },
256
+ )
257
+ .unwrap();
258
+
259
+ let node = graph::Key::new(42);
260
+
261
+ store
262
+ .upsert_vector(
263
+ &mut tx,
264
+ "default",
265
+ "idx",
266
+ node,
267
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
268
+ )
269
+ .unwrap();
270
+
271
+ store
272
+ .upsert_vector(
273
+ &mut tx,
274
+ "default",
275
+ "idx",
276
+ node,
277
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
278
+ )
279
+ .unwrap();
280
+
281
+ tx.close().unwrap();
282
+
283
+ let mut tx = store.begin_read().unwrap();
284
+ let results = store
285
+ .vector_search(
286
+ &mut tx,
287
+ "default",
288
+ "idx",
289
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
290
+ 10,
291
+ )
292
+ .unwrap();
293
+
294
+ let matches: Vec<_> = results.into_iter().filter(|r| r.node_id == node).collect();
295
+
296
+ assert_eq!(
297
+ matches.len(),
298
+ 1,
299
+ "duplicate entries detected (rowid instability)"
300
+ );
301
+ tx.close().unwrap();
302
+ }
303
+
304
+ pub(crate) fn test_multiple_indexes_do_not_conflict<S: Store>(store: S)
305
+ {
306
+ let mut tx = store.begin_write().unwrap();
307
+
308
+ store
309
+ .create_vector_index(
310
+ &mut tx,
311
+ "default",
312
+ VectorIndexSpec {
313
+ name: "idx1".into(),
314
+ dimension: 2,
315
+ ..Default::default()
316
+ },
317
+ )
318
+ .unwrap();
319
+
320
+ store
321
+ .create_vector_index(
322
+ &mut tx,
323
+ "default",
324
+ VectorIndexSpec {
325
+ name: "idx2".into(),
326
+ dimension: 2,
327
+ ..Default::default()
328
+ },
329
+ )
330
+ .unwrap();
331
+
332
+ let node = graph::Key::new(1);
333
+
334
+ store
335
+ .upsert_vector(
336
+ &mut tx,
337
+ "default",
338
+ "idx1",
339
+ node,
340
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
341
+ )
342
+ .unwrap();
343
+
344
+ tx.close().unwrap();
345
+
346
+ let mut tx = store.begin_read().unwrap();
347
+ let r1 = store
348
+ .vector_search(
349
+ &mut tx,
350
+ "default",
351
+ "idx1",
352
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
353
+ 1,
354
+ )
355
+ .unwrap();
356
+ let r2 = store
357
+ .vector_search(
358
+ &mut tx,
359
+ "default",
360
+ "idx2",
361
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
362
+ 1,
363
+ )
364
+ .unwrap();
365
+
366
+ assert_eq!(r1.len(), 1);
367
+ assert_eq!(r2.len(), 0);
368
+ tx.close().unwrap();
369
+ }
370
+
371
+ pub(crate) fn test_dimension_mismatch_fails<S: Store>(store: S)
372
+ {
373
+ let mut tx = store.begin_write().unwrap();
374
+
375
+ store
376
+ .create_vector_index(
377
+ &mut tx,
378
+ "default",
379
+ VectorIndexSpec {
380
+ name: "idx".into(),
381
+ dimension: 3,
382
+ ..Default::default()
383
+ },
384
+ )
385
+ .unwrap();
386
+
387
+ let node = graph::Key::new(1);
388
+
389
+ let upsert_result = store.upsert_vector(
390
+ &mut tx,
391
+ "default",
392
+ "idx",
393
+ node,
394
+ &crate::value::Tensor::from_f32_slice(&[1.0, 2.0]),
395
+ );
396
+
397
+ tx.close().unwrap();
398
+
399
+ match upsert_result
400
+ {
401
+ Err(_) =>
402
+ {} // backend validated at upsert time
403
+ Ok(_) =>
404
+ {
405
+ // backend didn't validate; search should catch the mismatch
406
+ let mut tx = store.begin_read().unwrap();
407
+ let search_result = store.vector_search(
408
+ &mut tx,
409
+ "default",
410
+ "idx",
411
+ &crate::value::Tensor::from_f32_slice(&[1.0, 2.0]),
412
+ 1,
413
+ );
414
+ assert!(
415
+ search_result.is_err(),
416
+ "dimension mismatch should fail at search time"
417
+ );
418
+ }
419
+ }
420
+ }
421
+
422
+ pub(crate) fn test_upsert_on_missing_index_fails<S: Store>(store: S)
423
+ {
424
+ let mut tx = store.begin_write().unwrap();
425
+ store
426
+ .create_vector_index(
427
+ &mut tx,
428
+ "default",
429
+ VectorIndexSpec {
430
+ name: "idx".into(),
431
+ dimension: 3,
432
+ metric: VectorMetric::Cosine,
433
+ ..Default::default()
434
+ },
435
+ )
436
+ .unwrap();
437
+
438
+ let node = graph::Node::new(
439
+ Some("default".into()),
440
+ graph::Key::new(100),
441
+ graph::labels!("Item"),
442
+ value::ValueMap::new(),
443
+ );
444
+ store.create_nodes(&mut tx, "default", [&node]).unwrap();
445
+
446
+ let result = store.upsert_vector(
447
+ &mut tx,
448
+ "default",
449
+ "nonexistent_idx",
450
+ node.key(),
451
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
452
+ );
453
+
454
+ assert!(
455
+ result.is_err(),
456
+ "upsert on missing vector index should fail"
457
+ );
458
+ }
459
+
460
+ pub(crate) fn test_search_on_missing_index_fails<S: Store>(store: S)
461
+ {
462
+ let mut tx = store.begin_read().unwrap();
463
+ let result = store.vector_search(
464
+ &mut tx,
465
+ "default",
466
+ "never_created",
467
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
468
+ 5,
469
+ );
470
+ tx.close().unwrap();
471
+ assert!(
472
+ result.is_err(),
473
+ "search on a never-created index should fail"
474
+ );
475
+ }
476
+
477
+ pub(crate) fn test_drop_graph_cleans_vector_data<S: Store>(store: S)
478
+ {
479
+ let graph_name = "g";
480
+ let index_name = "idx";
481
+ let old_node = graph::Node::new(
482
+ Some(graph_name.into()),
483
+ graph::Key::new(6001),
484
+ graph::labels!("Item"),
485
+ value::ValueMap::new(),
486
+ );
487
+
488
+ let mut tx = store.begin_write().unwrap();
489
+ store.create_graph(&mut tx, graph_name, false).unwrap();
490
+ store
491
+ .create_vector_index(
492
+ &mut tx,
493
+ graph_name,
494
+ VectorIndexSpec {
495
+ name: index_name.into(),
496
+ dimension: 3,
497
+ metric: VectorMetric::Cosine,
498
+ ..Default::default()
499
+ },
500
+ )
501
+ .unwrap();
502
+ store
503
+ .create_nodes(&mut tx, graph_name, [&old_node])
504
+ .unwrap();
505
+ store
506
+ .upsert_vector(
507
+ &mut tx,
508
+ graph_name,
509
+ index_name,
510
+ old_node.key(),
511
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
512
+ )
513
+ .unwrap();
514
+ tx.close().unwrap();
515
+
516
+ let mut tx = store.begin_write().unwrap();
517
+ store.drop_graph(&mut tx, graph_name, false).unwrap();
518
+ store.create_graph(&mut tx, graph_name, false).unwrap();
519
+ tx.close().unwrap();
520
+
521
+ let mut tx = store.begin_read().unwrap();
522
+ let missing_index_result = store.vector_search(
523
+ &mut tx,
524
+ graph_name,
525
+ index_name,
526
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
527
+ 10,
528
+ );
529
+ tx.close().unwrap();
530
+ assert!(
531
+ missing_index_result.is_err(),
532
+ "search should fail after graph drop/recreate because index no longer exists"
533
+ );
534
+
535
+ let new_node = graph::Node::new(
536
+ Some(graph_name.into()),
537
+ graph::Key::new(6002),
538
+ graph::labels!("Item"),
539
+ value::ValueMap::new(),
540
+ );
541
+ let mut tx = store.begin_write().unwrap();
542
+ store
543
+ .create_vector_index(
544
+ &mut tx,
545
+ graph_name,
546
+ VectorIndexSpec {
547
+ name: index_name.into(),
548
+ dimension: 3,
549
+ metric: VectorMetric::Cosine,
550
+ ..Default::default()
551
+ },
552
+ )
553
+ .unwrap();
554
+ store
555
+ .create_nodes(&mut tx, graph_name, [&new_node])
556
+ .unwrap();
557
+ store
558
+ .upsert_vector(
559
+ &mut tx,
560
+ graph_name,
561
+ index_name,
562
+ new_node.key(),
563
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
564
+ )
565
+ .unwrap();
566
+ tx.close().unwrap();
567
+
568
+ let mut tx = store.begin_read().unwrap();
569
+ let results = store
570
+ .vector_search(
571
+ &mut tx,
572
+ graph_name,
573
+ index_name,
574
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
575
+ 10,
576
+ )
577
+ .unwrap();
578
+ tx.close().unwrap();
579
+
580
+ assert_eq!(
581
+ results.len(),
582
+ 1,
583
+ "recreated index should only contain new node"
584
+ );
585
+ assert_eq!(results[0].node_id, new_node.key());
586
+ }
587
+
588
+ pub(crate) fn test_vector_updated_on_node_property_update<S: Store>(store: S)
589
+ {
590
+ let node_a = crate::graph::Node::new(
591
+ Some("default".into()),
592
+ crate::graph::Key::new(1),
593
+ crate::graph::labels!("Item"),
594
+ crate::value::value_map!("embedding" => vec![1.0, 0.0, 0.0]),
595
+ );
596
+ let node_b = crate::graph::Node::new(
597
+ Some("default".into()),
598
+ crate::graph::Key::new(2),
599
+ crate::graph::labels!("Item"),
600
+ crate::value::value_map!("embedding" => vec![0.8, 0.2, 0.0]),
601
+ );
602
+
603
+ let mut tx = store.begin_write().unwrap();
604
+ store
605
+ .create_nodes(&mut tx, "default", [&node_a, &node_b])
606
+ .unwrap();
607
+ tx.close().unwrap();
608
+
609
+ let mut tx = store.begin_write().unwrap();
610
+ store
611
+ .create_vector_index(
612
+ &mut tx,
613
+ "default",
614
+ crate::store::VectorIndexSpec {
615
+ name: "idx".into(),
616
+ dimension: 3,
617
+ metric: crate::store::VectorMetric::Cosine,
618
+ label: Some("Item".into()),
619
+ property: Some("embedding".into()),
620
+ ..Default::default()
621
+ },
622
+ )
623
+ .unwrap();
624
+ tx.close().unwrap();
625
+
626
+ let mut tx = store.begin_read().unwrap();
627
+ let results = store
628
+ .vector_search(
629
+ &mut tx,
630
+ "default",
631
+ "idx",
632
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
633
+ 2,
634
+ )
635
+ .unwrap();
636
+ tx.close().unwrap();
637
+ assert_eq!(results[0].node_id, node_a.key());
638
+
639
+ let updated_node_a = crate::graph::Node::new(
640
+ Some("default".into()),
641
+ node_a.key(),
642
+ crate::graph::labels!("Item"),
643
+ crate::value::value_map!("embedding" => vec![0.0, 1.0, 0.0]),
644
+ );
645
+
646
+ let mut tx = store.begin_write().unwrap();
647
+ store
648
+ .update_node(&mut tx, "default", &updated_node_a)
649
+ .unwrap();
650
+ // Explicitly update the vector in the index after updating the node
651
+ store
652
+ .upsert_vector(
653
+ &mut tx,
654
+ "default",
655
+ "idx",
656
+ updated_node_a.key(),
657
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
658
+ )
659
+ .unwrap();
660
+ tx.close().unwrap();
661
+
662
+ let mut tx = store.begin_read().unwrap();
663
+ let old_results = store
664
+ .vector_search(
665
+ &mut tx,
666
+ "default",
667
+ "idx",
668
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
669
+ 2,
670
+ )
671
+ .unwrap();
672
+ let new_results = store
673
+ .vector_search(
674
+ &mut tx,
675
+ "default",
676
+ "idx",
677
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
678
+ 2,
679
+ )
680
+ .unwrap();
681
+ tx.close().unwrap();
682
+
683
+ assert_ne!(old_results[0].node_id, updated_node_a.key());
684
+ assert_eq!(new_results[0].node_id, updated_node_a.key());
685
+ }
686
+
687
+ pub(crate) fn test_vector_search_empty_corpus<S: Store>(store: S)
688
+ {
689
+ let mut tx = store.begin_write().unwrap();
690
+ store
691
+ .create_vector_index(
692
+ &mut tx,
693
+ "default",
694
+ VectorIndexSpec {
695
+ name: "empty_idx".into(),
696
+ dimension: 3,
697
+ metric: VectorMetric::Cosine,
698
+ ..Default::default()
699
+ },
700
+ )
701
+ .unwrap();
702
+ tx.close().unwrap();
703
+
704
+ let mut tx = store.begin_read().unwrap();
705
+ let result = store
706
+ .vector_search(
707
+ &mut tx,
708
+ "default",
709
+ "empty_idx",
710
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
711
+ 5,
712
+ )
713
+ .unwrap();
714
+ tx.close().unwrap();
715
+ assert!(
716
+ result.is_empty(),
717
+ "empty corpus search should return no results"
718
+ );
719
+ }
720
+
721
+ pub(crate) fn test_vector_search_k_larger_than_corpus<S: Store>(store: S)
722
+ {
723
+ let mut tx = store.begin_write().unwrap();
724
+ store
725
+ .create_vector_index(
726
+ &mut tx,
727
+ "default",
728
+ VectorIndexSpec {
729
+ name: "idx".into(),
730
+ dimension: 3,
731
+ metric: VectorMetric::Cosine,
732
+ ..Default::default()
733
+ },
734
+ )
735
+ .unwrap();
736
+
737
+ for i in 0..3
738
+ {
739
+ let node = graph::Node::new(
740
+ Some("default".into()),
741
+ graph::Key::new(7000 + i as u128),
742
+ graph::labels!("Item"),
743
+ value::ValueMap::new(),
744
+ );
745
+ store.create_nodes(&mut tx, "default", [&node]).unwrap();
746
+ store
747
+ .upsert_vector(
748
+ &mut tx,
749
+ "default",
750
+ "idx",
751
+ node.key(),
752
+ &crate::value::Tensor::from_f32_slice(&[1.0 - i as f32 * 0.1, i as f32 * 0.1, 0.0]),
753
+ )
754
+ .unwrap();
755
+ }
756
+ tx.close().unwrap();
757
+
758
+ let mut tx = store.begin_read().unwrap();
759
+ let results = store
760
+ .vector_search(
761
+ &mut tx,
762
+ "default",
763
+ "idx",
764
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
765
+ 100,
766
+ )
767
+ .unwrap();
768
+ tx.close().unwrap();
769
+
770
+ assert_eq!(
771
+ results.len(),
772
+ 3,
773
+ "k larger than corpus should return corpus size"
774
+ );
775
+ }
776
+
777
+ pub(crate) fn test_vector_zero_magnitude_handling<S: Store>(store: S)
778
+ {
779
+ let normal_node = graph::Node::new(
780
+ Some("default".into()),
781
+ graph::Key::new(8001),
782
+ graph::labels!("Item"),
783
+ value::ValueMap::new(),
784
+ );
785
+ let zero_node = graph::Node::new(
786
+ Some("default".into()),
787
+ graph::Key::new(8002),
788
+ graph::labels!("Item"),
789
+ value::ValueMap::new(),
790
+ );
791
+
792
+ let mut tx = store.begin_write().unwrap();
793
+ store
794
+ .create_vector_index(
795
+ &mut tx,
796
+ "default",
797
+ VectorIndexSpec {
798
+ name: "zero_idx".into(),
799
+ dimension: 3,
800
+ metric: VectorMetric::Cosine,
801
+ ..Default::default()
802
+ },
803
+ )
804
+ .unwrap();
805
+ store
806
+ .create_nodes(&mut tx, "default", [&normal_node, &zero_node])
807
+ .unwrap();
808
+ store
809
+ .upsert_vector(
810
+ &mut tx,
811
+ "default",
812
+ "zero_idx",
813
+ normal_node.key(),
814
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
815
+ )
816
+ .unwrap();
817
+ store
818
+ .upsert_vector(
819
+ &mut tx,
820
+ "default",
821
+ "zero_idx",
822
+ zero_node.key(),
823
+ &crate::value::Tensor::from_f32_slice(&[0.0, 0.0, 0.0]),
824
+ )
825
+ .unwrap();
826
+ tx.close().unwrap();
827
+
828
+ let mut tx = store.begin_read().unwrap();
829
+ let results = store
830
+ .vector_search(
831
+ &mut tx,
832
+ "default",
833
+ "zero_idx",
834
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
835
+ 2,
836
+ )
837
+ .unwrap();
838
+ tx.close().unwrap();
839
+
840
+ assert_eq!(
841
+ results.len(),
842
+ 2,
843
+ "zero-magnitude handling should not drop results"
844
+ );
845
+ assert_eq!(
846
+ results[0].node_id,
847
+ normal_node.key(),
848
+ "normal vector should rank before zero vector"
849
+ );
850
+ assert_eq!(results[1].node_id, zero_node.key());
851
+ }
852
+
853
+ pub(crate) fn test_vector_search_k_zero<S: Store>(store: S)
854
+ {
855
+ let mut tx = store.begin_write().unwrap();
856
+ store
857
+ .create_vector_index(
858
+ &mut tx,
859
+ "default",
860
+ VectorIndexSpec {
861
+ name: "k0_idx".into(),
862
+ dimension: 3,
863
+ metric: VectorMetric::Cosine,
864
+ ..Default::default()
865
+ },
866
+ )
867
+ .unwrap();
868
+ for i in 0..3
869
+ {
870
+ let node = graph::Node::new(
871
+ Some("default".into()),
872
+ graph::Key::new(9000 + i as u128),
873
+ graph::labels!("Item"),
874
+ value::ValueMap::new(),
875
+ );
876
+ store.create_nodes(&mut tx, "default", [&node]).unwrap();
877
+ store
878
+ .upsert_vector(
879
+ &mut tx,
880
+ "default",
881
+ "k0_idx",
882
+ node.key(),
883
+ &crate::value::Tensor::from_f32_slice(&[1.0, i as f32 * 0.1, 0.0]),
884
+ )
885
+ .unwrap();
886
+ }
887
+ tx.close().unwrap();
888
+
889
+ let mut tx = store.begin_read().unwrap();
890
+ let results = store
891
+ .vector_search(
892
+ &mut tx,
893
+ "default",
894
+ "k0_idx",
895
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
896
+ 0,
897
+ )
898
+ .unwrap();
899
+ tx.close().unwrap();
900
+ assert!(results.is_empty(), "k=0 should return empty results");
901
+ }
902
+
903
+ pub(crate) fn test_drop_vector_index_missing_fails<S: Store>(store: S)
904
+ {
905
+ let mut tx = store.begin_write().unwrap();
906
+ let result = store.drop_vector_index(&mut tx, "default", "missing_idx");
907
+ tx.close().unwrap();
908
+
909
+ assert!(result.is_err(), "dropping a missing index should fail");
910
+ assert!(
911
+ result.unwrap_err().to_string().contains("missing_idx"),
912
+ "error should mention missing index name"
913
+ );
914
+ }
915
+
916
+ pub(crate) fn test_drop_vector_index_removes_index_state<S: Store>(store: S)
917
+ {
918
+ let node = graph::Node::new(
919
+ Some("default".into()),
920
+ graph::Key::new(501),
921
+ graph::labels!("Item"),
922
+ value::ValueMap::new(),
923
+ );
924
+
925
+ let mut tx = store.begin_write().unwrap();
926
+ store
927
+ .create_vector_index(
928
+ &mut tx,
929
+ "default",
930
+ VectorIndexSpec {
931
+ name: "idx".into(),
932
+ dimension: 3,
933
+ metric: VectorMetric::Cosine,
934
+ ..Default::default()
935
+ },
936
+ )
937
+ .unwrap();
938
+ store.create_nodes(&mut tx, "default", [&node]).unwrap();
939
+ store
940
+ .upsert_vector(
941
+ &mut tx,
942
+ "default",
943
+ "idx",
944
+ node.key(),
945
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
946
+ )
947
+ .unwrap();
948
+ store.drop_vector_index(&mut tx, "default", "idx").unwrap();
949
+
950
+ let search_result = store.vector_search(
951
+ &mut tx,
952
+ "default",
953
+ "idx",
954
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
955
+ 1,
956
+ );
957
+ assert!(
958
+ search_result.is_err(),
959
+ "search on dropped index should return an error"
960
+ );
961
+
962
+ let upsert_result = store.upsert_vector(
963
+ &mut tx,
964
+ "default",
965
+ "idx",
966
+ node.key(),
967
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
968
+ );
969
+ assert!(
970
+ upsert_result.is_err(),
971
+ "upsert on dropped index should return an error"
972
+ );
973
+ tx.close().unwrap();
974
+ }
975
+
976
+ pub(crate) fn test_drop_graph_recreate_index_is_clean<S: Store>(store: S)
977
+ {
978
+ let graph_name = "drop_graph_recreate";
979
+ let index_name = "idx";
980
+ let node = graph::Node::new(
981
+ Some(graph_name.into()),
982
+ graph::Key::new(9001),
983
+ graph::labels!("Item"),
984
+ value::ValueMap::new(),
985
+ );
986
+
987
+ let mut tx = store.begin_write().unwrap();
988
+ store.create_graph(&mut tx, graph_name, false).unwrap();
989
+ store
990
+ .create_vector_index(
991
+ &mut tx,
992
+ graph_name,
993
+ VectorIndexSpec {
994
+ name: index_name.into(),
995
+ dimension: 3,
996
+ metric: VectorMetric::Cosine,
997
+ ..Default::default()
998
+ },
999
+ )
1000
+ .unwrap();
1001
+ store.create_nodes(&mut tx, graph_name, [&node]).unwrap();
1002
+ store
1003
+ .upsert_vector(
1004
+ &mut tx,
1005
+ graph_name,
1006
+ index_name,
1007
+ node.key(),
1008
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1009
+ )
1010
+ .unwrap();
1011
+ tx.close().unwrap();
1012
+
1013
+ let mut tx = store.begin_read().unwrap();
1014
+ let before = store
1015
+ .vector_search(
1016
+ &mut tx,
1017
+ graph_name,
1018
+ index_name,
1019
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1020
+ 5,
1021
+ )
1022
+ .unwrap();
1023
+ tx.close().unwrap();
1024
+ assert!(
1025
+ !before.is_empty(),
1026
+ "precondition: index should contain data"
1027
+ );
1028
+
1029
+ let mut tx = store.begin_write().unwrap();
1030
+ store.drop_graph(&mut tx, graph_name, false).unwrap();
1031
+ store.create_graph(&mut tx, graph_name, false).unwrap();
1032
+ store
1033
+ .create_vector_index(
1034
+ &mut tx,
1035
+ graph_name,
1036
+ VectorIndexSpec {
1037
+ name: index_name.into(),
1038
+ dimension: 3,
1039
+ metric: VectorMetric::Cosine,
1040
+ ..Default::default()
1041
+ },
1042
+ )
1043
+ .unwrap();
1044
+ tx.close().unwrap();
1045
+
1046
+ let mut tx = store.begin_read().unwrap();
1047
+ let after = store
1048
+ .vector_search(
1049
+ &mut tx,
1050
+ graph_name,
1051
+ index_name,
1052
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1053
+ 5,
1054
+ )
1055
+ .unwrap();
1056
+ tx.close().unwrap();
1057
+ assert!(
1058
+ after.is_empty(),
1059
+ "recreated graph/index should not contain stale vectors"
1060
+ );
1061
+ }
1062
+
1063
+ pub(crate) fn test_vector_search_ordering<S: Store>(store: S)
1064
+ {
1065
+ let mut tx = store.begin_write().unwrap();
1066
+
1067
+ store
1068
+ .create_vector_index(
1069
+ &mut tx,
1070
+ "default",
1071
+ VectorIndexSpec {
1072
+ name: "idx".into(),
1073
+ dimension: 2,
1074
+ ..Default::default()
1075
+ },
1076
+ )
1077
+ .unwrap();
1078
+
1079
+ let n1 = graph::Key::new(1);
1080
+ let n2 = graph::Key::new(2);
1081
+
1082
+ store
1083
+ .upsert_vector(
1084
+ &mut tx,
1085
+ "default",
1086
+ "idx",
1087
+ n1,
1088
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
1089
+ )
1090
+ .unwrap();
1091
+ store
1092
+ .upsert_vector(
1093
+ &mut tx,
1094
+ "default",
1095
+ "idx",
1096
+ n2,
1097
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0]),
1098
+ )
1099
+ .unwrap();
1100
+
1101
+ tx.close().unwrap();
1102
+
1103
+ let mut tx = store.begin_read().unwrap();
1104
+ let results = store
1105
+ .vector_search(
1106
+ &mut tx,
1107
+ "default",
1108
+ "idx",
1109
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
1110
+ 2,
1111
+ )
1112
+ .unwrap();
1113
+
1114
+ assert_eq!(results[0].node_id, n1);
1115
+ tx.close().unwrap();
1116
+ }
1117
+
1118
+ pub(crate) fn test_vector_metrics<S: Store>(store: S)
1119
+ {
1120
+ for (metric_name, metric) in [
1121
+ ("cosine", VectorMetric::Cosine),
1122
+ ("l2", VectorMetric::L2),
1123
+ ("dotproduct", VectorMetric::DotProduct),
1124
+ ]
1125
+ {
1126
+ let mut tx = store.begin_write().unwrap();
1127
+ store
1128
+ .create_vector_index(
1129
+ &mut tx,
1130
+ "default",
1131
+ VectorIndexSpec {
1132
+ name: format!("idx_{}", metric_name),
1133
+ dimension: 3,
1134
+ metric,
1135
+ ..Default::default()
1136
+ },
1137
+ )
1138
+ .unwrap();
1139
+
1140
+ let node_a = graph::Node::new(
1141
+ Some("default".into()),
1142
+ graph::Key::new(1),
1143
+ graph::labels!("Item"),
1144
+ value::value_map!("embedding" => vec![1.0, 0.0, 0.0]),
1145
+ );
1146
+ let node_b = graph::Node::new(
1147
+ Some("default".into()),
1148
+ graph::Key::new(2),
1149
+ graph::labels!("Item"),
1150
+ value::value_map!("embedding" => vec![0.0, 1.0, 0.0]),
1151
+ );
1152
+
1153
+ store
1154
+ .create_nodes(&mut tx, "default", [&node_a, &node_b])
1155
+ .unwrap();
1156
+ store
1157
+ .upsert_vector(
1158
+ &mut tx,
1159
+ "default",
1160
+ format!("idx_{}", metric_name),
1161
+ node_a.key(),
1162
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1163
+ )
1164
+ .unwrap();
1165
+ store
1166
+ .upsert_vector(
1167
+ &mut tx,
1168
+ "default",
1169
+ format!("idx_{}", metric_name),
1170
+ node_b.key(),
1171
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
1172
+ )
1173
+ .unwrap();
1174
+
1175
+ tx.close().unwrap();
1176
+
1177
+ let mut tx = store.begin_read().unwrap();
1178
+ let result = store.vector_search(
1179
+ &mut tx,
1180
+ "default",
1181
+ format!("idx_{}", metric_name),
1182
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1183
+ 2,
1184
+ );
1185
+
1186
+ let results = result.unwrap();
1187
+ assert_eq!(results.len(), 2, "Vector search should return both vectors");
1188
+ assert_eq!(results[0].node_id, node_a.key());
1189
+
1190
+ match metric
1191
+ {
1192
+ VectorMetric::Cosine | VectorMetric::L2 =>
1193
+ {
1194
+ assert!(
1195
+ (results[0].score - 0.0).abs() < 1e-4,
1196
+ "{} score should be near zero for an exact match",
1197
+ metric_name
1198
+ );
1199
+ }
1200
+ VectorMetric::DotProduct =>
1201
+ {
1202
+ assert!(
1203
+ results[0].score <= results[1].score,
1204
+ "dot-product results should be ordered by the smallest score"
1205
+ );
1206
+ }
1207
+ }
1208
+
1209
+ tx.close().unwrap();
1210
+ }
1211
+ }
1212
+
1213
+ pub(crate) fn test_multiple_vector_indexes_isolation<S: Store>(store: S)
1214
+ {
1215
+ let mut tx = store.begin_write().unwrap();
1216
+
1217
+ for name in ["idx1", "idx2"]
1218
+ {
1219
+ store
1220
+ .create_vector_index(
1221
+ &mut tx,
1222
+ "default",
1223
+ VectorIndexSpec {
1224
+ name: name.into(),
1225
+ dimension: 3,
1226
+ metric: VectorMetric::Cosine,
1227
+ ..Default::default()
1228
+ },
1229
+ )
1230
+ .unwrap();
1231
+ }
1232
+
1233
+ tx.close().unwrap();
1234
+ }
1235
+
1236
+ pub(crate) fn test_list_vector_indexes<S: Store>(store: S)
1237
+ {
1238
+ let mut tx = store.begin_write().unwrap();
1239
+ store.create_graph(&mut tx, "no_indices", false).unwrap();
1240
+
1241
+ store
1242
+ .create_vector_index(
1243
+ &mut tx,
1244
+ "default",
1245
+ VectorIndexSpec {
1246
+ name: "idx_a".to_string(),
1247
+ dimension: 3,
1248
+ metric: VectorMetric::Cosine,
1249
+ ..Default::default()
1250
+ },
1251
+ )
1252
+ .unwrap();
1253
+ store
1254
+ .create_vector_index(
1255
+ &mut tx,
1256
+ "default",
1257
+ VectorIndexSpec {
1258
+ name: "idx_b".to_string(),
1259
+ dimension: 4,
1260
+ metric: VectorMetric::L2,
1261
+ ..Default::default()
1262
+ },
1263
+ )
1264
+ .unwrap();
1265
+ tx.close().unwrap();
1266
+
1267
+ let mut tx = store.begin_read().unwrap();
1268
+ let listed = store.list_vector_indexes(&mut tx, "default").unwrap();
1269
+ let empty = store.list_vector_indexes(&mut tx, "no_indices").unwrap();
1270
+ tx.close().unwrap();
1271
+
1272
+ assert_eq!(listed.len(), 2, "expected exactly two vector indexes");
1273
+ assert!(empty.is_empty(), "graph with no indices should list empty");
1274
+
1275
+ let idx_a = listed.iter().find(|spec| spec.name == "idx_a").unwrap();
1276
+ assert_eq!(idx_a.metric, VectorMetric::Cosine);
1277
+ assert_eq!(idx_a.dimension, 3);
1278
+
1279
+ let idx_b = listed.iter().find(|spec| spec.name == "idx_b").unwrap();
1280
+ assert_eq!(idx_b.metric, VectorMetric::L2);
1281
+ assert_eq!(idx_b.dimension, 4);
1282
+ }
1283
+ pub(crate) fn test_vector_rollback<S: Store>(store: S)
1284
+ {
1285
+ let graph = "default";
1286
+ let index = "idx";
1287
+
1288
+ {
1289
+ let mut tx = store.begin_write().unwrap();
1290
+ store
1291
+ .create_vector_index(
1292
+ &mut tx,
1293
+ graph,
1294
+ VectorIndexSpec {
1295
+ name: index.into(),
1296
+ dimension: 2,
1297
+ ..Default::default()
1298
+ },
1299
+ )
1300
+ .unwrap();
1301
+
1302
+ store
1303
+ .upsert_vector(
1304
+ &mut tx,
1305
+ graph,
1306
+ index,
1307
+ graph::Key::new(7),
1308
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
1309
+ )
1310
+ .unwrap();
1311
+ }
1312
+
1313
+ let mut tx = store.begin_write().unwrap();
1314
+
1315
+ match store.vector_search(
1316
+ &mut tx,
1317
+ graph,
1318
+ index,
1319
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0]),
1320
+ 1,
1321
+ )
1322
+ {
1323
+ Ok(results) =>
1324
+ {
1325
+ // Index was persisted (in-memory backend) but vector was rolled back
1326
+ assert!(results.is_empty(), "rollback should leave no vectors");
1327
+ }
1328
+ Err(_) =>
1329
+ {
1330
+ // Index was fully rolled back (transactional backend) or reads are forbidden
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ pub(crate) fn test_vector_cleanup_after_node_deletion<S: Store>(store: S)
1336
+ {
1337
+ // Use explicit upsert_vector so the test works for all backends regardless of
1338
+ // whether they support retroactive population or apply_vector_indices_for_node.
1339
+ let node = graph::Node::new(
1340
+ Some("default".to_string()),
1341
+ graph::Key::default(),
1342
+ graph::labels!("Article"),
1343
+ value::ValueMap::new(),
1344
+ );
1345
+
1346
+ let mut tx = store.begin_write().unwrap();
1347
+ store
1348
+ .create_vector_index(
1349
+ &mut tx,
1350
+ "default",
1351
+ VectorIndexSpec {
1352
+ name: "cleanup_idx".to_string(),
1353
+ dimension: 3,
1354
+ metric: VectorMetric::Cosine,
1355
+ ..Default::default()
1356
+ },
1357
+ )
1358
+ .unwrap();
1359
+ store
1360
+ .create_nodes(&mut tx, "default", [node.clone()].iter())
1361
+ .unwrap();
1362
+ store
1363
+ .upsert_vector(
1364
+ &mut tx,
1365
+ "default",
1366
+ "cleanup_idx",
1367
+ node.key(),
1368
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1369
+ )
1370
+ .unwrap();
1371
+ tx.close().unwrap();
1372
+
1373
+ // Verify the node is found before deletion
1374
+ let mut tx = store.begin_read().unwrap();
1375
+ let results_before = store
1376
+ .vector_search(
1377
+ &mut tx,
1378
+ "default",
1379
+ "cleanup_idx",
1380
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1381
+ 1,
1382
+ )
1383
+ .unwrap();
1384
+ tx.close().unwrap();
1385
+ assert_eq!(results_before.len(), 1, "Expected 1 result before deletion");
1386
+ assert_eq!(results_before[0].node_id, node.key());
1387
+
1388
+ // Delete the node
1389
+ let mut tx = store.begin_write().unwrap();
1390
+ store
1391
+ .delete_nodes(
1392
+ &mut tx,
1393
+ "default",
1394
+ store::SelectNodeQuery::select_keys(vec![node.key()]),
1395
+ false,
1396
+ )
1397
+ .unwrap();
1398
+ tx.close().unwrap();
1399
+
1400
+ // After deletion, vector search must return no results (no stale entries)
1401
+ let mut tx = store.begin_read().unwrap();
1402
+ let results_after = store
1403
+ .vector_search(
1404
+ &mut tx,
1405
+ "default",
1406
+ "cleanup_idx",
1407
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1408
+ 1,
1409
+ )
1410
+ .unwrap();
1411
+ tx.close().unwrap();
1412
+ assert_eq!(
1413
+ results_after.len(),
1414
+ 0,
1415
+ "Expected 0 results after deletion — stale vector entry was not cleaned up"
1416
+ );
1417
+ }
1418
+
1419
+ pub(crate) fn test_vector_search_returns_correct_node<S: Store>(store: S)
1420
+ {
1421
+ let n1 = graph::Node::new(
1422
+ Some("default".to_string()),
1423
+ graph::Key::default(),
1424
+ graph::labels!("Article"),
1425
+ value::value_map!("embedding" => vec![1.0, 0.0, 0.0], "id" => 1),
1426
+ );
1427
+
1428
+ let n2 = graph::Node::new(
1429
+ Some("default".to_string()),
1430
+ graph::Key::default(),
1431
+ graph::labels!("Article"),
1432
+ value::value_map!("embedding" => vec![0.0, 1.0, 0.0], "id" => 2),
1433
+ );
1434
+
1435
+ let n3 = graph::Node::new(
1436
+ Some("default".to_string()),
1437
+ graph::Key::default(),
1438
+ graph::labels!("Article"),
1439
+ value::value_map!("embedding" => vec![1.0, 0.2, 0.0], "id" => 3),
1440
+ );
1441
+
1442
+ let mut tx = store.begin_write().unwrap();
1443
+ store
1444
+ .create_nodes(
1445
+ &mut tx,
1446
+ "default",
1447
+ [n1.clone(), n2.clone(), n3.clone()].iter(),
1448
+ )
1449
+ .unwrap();
1450
+
1451
+ store
1452
+ .create_vector_index(
1453
+ &mut tx,
1454
+ "default",
1455
+ VectorIndexSpec {
1456
+ name: "article_idx".to_string(),
1457
+ dimension: 3,
1458
+ metric: VectorMetric::Cosine,
1459
+ label: Some("Article".to_string()),
1460
+ property: Some("embedding".to_string()),
1461
+ ..Default::default()
1462
+ },
1463
+ )
1464
+ .unwrap();
1465
+ tx.close().unwrap();
1466
+
1467
+ let mut tx = store.begin_read().unwrap();
1468
+ let results = store
1469
+ .vector_search(
1470
+ &mut tx,
1471
+ "default",
1472
+ "article_idx",
1473
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1474
+ 2,
1475
+ )
1476
+ .unwrap();
1477
+ tx.close().unwrap();
1478
+
1479
+ assert_eq!(
1480
+ results.len(),
1481
+ 2,
1482
+ "Expected 2 results, got {}",
1483
+ results.len()
1484
+ );
1485
+
1486
+ assert_eq!(
1487
+ results[0].node_id,
1488
+ n1.key(),
1489
+ "First result should be n1 (closest to query vector)"
1490
+ );
1491
+ assert_eq!(
1492
+ results[1].node_id,
1493
+ n3.key(),
1494
+ "Second result should be n3 (next closest)"
1495
+ );
1496
+
1497
+ use std::borrow::BorrowMut;
1498
+ let mut tx = store.begin_read().unwrap();
1499
+ let selected_nodes = store
1500
+ .select_nodes(
1501
+ tx.borrow_mut(),
1502
+ "default",
1503
+ store::SelectNodeQuery::select_keys(vec![results[0].node_id, results[1].node_id]),
1504
+ )
1505
+ .unwrap();
1506
+ tx.close().unwrap();
1507
+
1508
+ let mut n1_found = false;
1509
+ let mut n3_found = false;
1510
+ for node in selected_nodes
1511
+ {
1512
+ let id_val = node.properties().get("id").cloned();
1513
+ if let Some(id) = id_val
1514
+ {
1515
+ if id == value::Value::from(1)
1516
+ {
1517
+ assert_eq!(
1518
+ node.key(),
1519
+ n1.key(),
1520
+ "Node with id=1 should have key matching n1"
1521
+ );
1522
+ n1_found = true;
1523
+ }
1524
+ else if id == value::Value::from(3)
1525
+ {
1526
+ assert_eq!(
1527
+ node.key(),
1528
+ n3.key(),
1529
+ "Node with id=3 should have key matching n3"
1530
+ );
1531
+ n3_found = true;
1532
+ }
1533
+ }
1534
+ }
1535
+ assert!(n1_found, "Node with id=1 should be in selected nodes");
1536
+ assert!(n3_found, "Node with id=3 should be in selected nodes");
1537
+ }
1538
+
1539
+ pub(crate) fn test_vector_index_on_non_default_graph<S: Store>(store: S)
1540
+ {
1541
+ let graph_name = "docs_graph";
1542
+ let index_name = "idx";
1543
+
1544
+ let mut tx = store.begin_write().unwrap();
1545
+ store.create_graph(&mut tx, graph_name, false).unwrap();
1546
+ store
1547
+ .create_vector_index(
1548
+ &mut tx,
1549
+ graph_name,
1550
+ VectorIndexSpec {
1551
+ name: index_name.into(),
1552
+ dimension: 3,
1553
+ metric: VectorMetric::Cosine,
1554
+ ..Default::default()
1555
+ },
1556
+ )
1557
+ .unwrap();
1558
+ let node = graph::Node::new(
1559
+ Some(graph_name.into()),
1560
+ graph::Key::new(1),
1561
+ graph::labels!("Doc"),
1562
+ value::ValueMap::new(),
1563
+ );
1564
+ store.create_nodes(&mut tx, graph_name, [&node]).unwrap();
1565
+ store
1566
+ .upsert_vector(
1567
+ &mut tx,
1568
+ graph_name,
1569
+ index_name,
1570
+ node.key(),
1571
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1572
+ )
1573
+ .unwrap();
1574
+ tx.close().unwrap();
1575
+
1576
+ // Positive case: search on the graph the index actually belongs to.
1577
+ let mut tx = store.begin_read().unwrap();
1578
+ let hits = store
1579
+ .vector_search(
1580
+ &mut tx,
1581
+ graph_name,
1582
+ index_name,
1583
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1584
+ 5,
1585
+ )
1586
+ .unwrap();
1587
+ tx.close().unwrap();
1588
+ assert_eq!(
1589
+ hits.len(),
1590
+ 1,
1591
+ "index must be usable on the graph it was created on"
1592
+ );
1593
+ assert_eq!(hits[0].node_id, node.key());
1594
+ }
1595
+
1596
+ pub(crate) fn test_vector_index_not_visible_from_a_different_graph<S: Store>(store: S)
1597
+ {
1598
+ let graph_name = "docs_graph";
1599
+ let other_graph = "other_graph";
1600
+ let index_name = "idx";
1601
+
1602
+ let mut tx = store.begin_write().unwrap();
1603
+ store.create_graph(&mut tx, graph_name, false).unwrap();
1604
+ store.create_graph(&mut tx, other_graph, false).unwrap();
1605
+ store
1606
+ .create_vector_index(
1607
+ &mut tx,
1608
+ graph_name,
1609
+ VectorIndexSpec {
1610
+ name: index_name.into(),
1611
+ dimension: 3,
1612
+ metric: VectorMetric::Cosine,
1613
+ ..Default::default()
1614
+ },
1615
+ )
1616
+ .unwrap();
1617
+ tx.close().unwrap();
1618
+
1619
+ // Negative case: the SAME index name, looked up against a DIFFERENT
1620
+ // graph than the one it was created on, must fail -- not silently
1621
+ // resolve to the graph it actually lives on, and not silently succeed
1622
+ // against an empty/default fallback.
1623
+ let mut tx = store.begin_read().unwrap();
1624
+ let result = store.vector_search(
1625
+ &mut tx,
1626
+ other_graph,
1627
+ index_name,
1628
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1629
+ 5,
1630
+ );
1631
+ tx.close().unwrap();
1632
+ assert!(
1633
+ result.is_err(),
1634
+ "an index created on '{}' must not be reachable from '{}'",
1635
+ graph_name,
1636
+ other_graph
1637
+ );
1638
+ }
1639
+
1640
+ pub(crate) fn test_same_index_name_on_different_graphs_does_not_conflict<S: Store>(store: S)
1641
+ {
1642
+ let graph_a = "graph_a";
1643
+ let graph_b = "graph_b";
1644
+ let index_name = "idx"; // deliberately identical on both graphs
1645
+
1646
+ let mut tx = store.begin_write().unwrap();
1647
+ store.create_graph(&mut tx, graph_a, false).unwrap();
1648
+ store.create_graph(&mut tx, graph_b, false).unwrap();
1649
+
1650
+ store
1651
+ .create_vector_index(
1652
+ &mut tx,
1653
+ graph_a,
1654
+ VectorIndexSpec {
1655
+ name: index_name.into(),
1656
+ dimension: 3,
1657
+ metric: VectorMetric::Cosine,
1658
+ ..Default::default()
1659
+ },
1660
+ )
1661
+ .unwrap();
1662
+ // Must not error just because an index of the same name already exists
1663
+ // on a DIFFERENT graph -- index identity is (graph, name), not name alone.
1664
+ store
1665
+ .create_vector_index(
1666
+ &mut tx,
1667
+ graph_b,
1668
+ VectorIndexSpec {
1669
+ name: index_name.into(),
1670
+ dimension: 3,
1671
+ metric: VectorMetric::Cosine,
1672
+ ..Default::default()
1673
+ },
1674
+ )
1675
+ .unwrap();
1676
+
1677
+ let node_a = graph::Node::new(
1678
+ Some(graph_a.into()),
1679
+ graph::Key::new(1),
1680
+ graph::labels!("Doc"),
1681
+ value::ValueMap::new(),
1682
+ );
1683
+ let node_b = graph::Node::new(
1684
+ Some(graph_b.into()),
1685
+ graph::Key::new(2),
1686
+ graph::labels!("Doc"),
1687
+ value::ValueMap::new(),
1688
+ );
1689
+ store.create_nodes(&mut tx, graph_a, [&node_a]).unwrap();
1690
+ store.create_nodes(&mut tx, graph_b, [&node_b]).unwrap();
1691
+ store
1692
+ .upsert_vector(
1693
+ &mut tx,
1694
+ graph_a,
1695
+ index_name,
1696
+ node_a.key(),
1697
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1698
+ )
1699
+ .unwrap();
1700
+ store
1701
+ .upsert_vector(
1702
+ &mut tx,
1703
+ graph_b,
1704
+ index_name,
1705
+ node_b.key(),
1706
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
1707
+ )
1708
+ .unwrap();
1709
+ tx.close().unwrap();
1710
+
1711
+ let mut tx = store.begin_read().unwrap();
1712
+ let hits_a = store
1713
+ .vector_search(
1714
+ &mut tx,
1715
+ graph_a,
1716
+ index_name,
1717
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1718
+ 5,
1719
+ )
1720
+ .unwrap();
1721
+ let hits_b = store
1722
+ .vector_search(
1723
+ &mut tx,
1724
+ graph_b,
1725
+ index_name,
1726
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
1727
+ 5,
1728
+ )
1729
+ .unwrap();
1730
+ tx.close().unwrap();
1731
+
1732
+ assert_eq!(hits_a.len(), 1);
1733
+ assert_eq!(hits_a[0].node_id, node_a.key());
1734
+ assert_eq!(hits_b.len(), 1);
1735
+ assert_eq!(hits_b[0].node_id, node_b.key());
1736
+ assert_ne!(
1737
+ hits_a[0].node_id, hits_b[0].node_id,
1738
+ "same-named indexes on different graphs must never cross-contaminate results"
1739
+ );
1740
+ }
1741
+
1742
+ /// The headline roundtrip: populate, close, reopen, search.
1743
+ pub(crate) fn test_vector_index_survives_reopen<S: Store>(factory: &dyn Fn() -> S)
1744
+ {
1745
+ let graph_name = "reopen_rt";
1746
+ let index_name = "idx";
1747
+
1748
+ {
1749
+ let store = factory();
1750
+ let mut tx = store.begin_write().unwrap();
1751
+ store.create_graph(&mut tx, graph_name, false).unwrap();
1752
+ store
1753
+ .create_vector_index(
1754
+ &mut tx,
1755
+ graph_name,
1756
+ VectorIndexSpec {
1757
+ name: index_name.into(),
1758
+ dimension: 3,
1759
+ metric: VectorMetric::Cosine,
1760
+ ..Default::default()
1761
+ },
1762
+ )
1763
+ .unwrap();
1764
+ for (id, v) in [
1765
+ (1u64, [1.0f32, 0.0, 0.0]),
1766
+ (2, [0.0, 1.0, 0.0]),
1767
+ (3, [0.0, 0.0, 1.0]),
1768
+ ]
1769
+ {
1770
+ let node = graph::Node::new(
1771
+ Some(graph_name.into()),
1772
+ graph::Key::new(id as u128),
1773
+ graph::labels!("Item"),
1774
+ value::ValueMap::new(),
1775
+ );
1776
+ store.create_nodes(&mut tx, graph_name, [&node]).unwrap();
1777
+ store
1778
+ .upsert_vector(
1779
+ &mut tx,
1780
+ graph_name,
1781
+ index_name,
1782
+ node.key(),
1783
+ &crate::value::Tensor::from_f32_slice(&v),
1784
+ )
1785
+ .unwrap();
1786
+ }
1787
+ tx.close().unwrap();
1788
+ } // store dropped: all in-memory state gone
1789
+
1790
+ let store = factory(); // reopen from the same backing storage
1791
+ let mut tx = store.begin_read().unwrap();
1792
+ let results = store
1793
+ .vector_search(
1794
+ &mut tx,
1795
+ graph_name,
1796
+ index_name,
1797
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1798
+ 1,
1799
+ )
1800
+ .unwrap();
1801
+ tx.close().unwrap();
1802
+
1803
+ assert_eq!(results.len(), 1, "reopened index must serve searches");
1804
+ assert_eq!(
1805
+ results[0].node_id,
1806
+ graph::Key::new(1),
1807
+ "reopened index must return the correct nearest neighbour, not merely any result"
1808
+ );
1809
+ }
1810
+
1811
+ /// Reopen must also restore write capability: inserting more vectors into a
1812
+ /// reopened index must work, and search must find both old and new vectors.
1813
+ /// Catches the capacity-not-restored class of bug (a reloaded index whose
1814
+ /// internal limits were sized to the persisted count instead of the original
1815
+ /// configuration) and any install-ordering issue that leaves the reloaded
1816
+ /// index read-only.
1817
+ pub(crate) fn test_insert_after_reopen<S: Store>(factory: &dyn Fn() -> S)
1818
+ {
1819
+ let graph_name = "reopen_insert";
1820
+ let index_name = "idx";
1821
+
1822
+ {
1823
+ let store = factory();
1824
+ let mut tx = store.begin_write().unwrap();
1825
+ store.create_graph(&mut tx, graph_name, false).unwrap();
1826
+ store
1827
+ .create_vector_index(
1828
+ &mut tx,
1829
+ graph_name,
1830
+ VectorIndexSpec {
1831
+ name: index_name.into(),
1832
+ dimension: 3,
1833
+ metric: VectorMetric::Cosine,
1834
+ ..Default::default()
1835
+ },
1836
+ )
1837
+ .unwrap();
1838
+ let node = graph::Node::new(
1839
+ Some(graph_name.into()),
1840
+ graph::Key::new(1u128),
1841
+ graph::labels!("Item"),
1842
+ value::ValueMap::new(),
1843
+ );
1844
+ store.create_nodes(&mut tx, graph_name, [&node]).unwrap();
1845
+ store
1846
+ .upsert_vector(
1847
+ &mut tx,
1848
+ graph_name,
1849
+ index_name,
1850
+ node.key(),
1851
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1852
+ )
1853
+ .unwrap();
1854
+ tx.close().unwrap();
1855
+ }
1856
+
1857
+ let store = factory();
1858
+ let mut tx = store.begin_write().unwrap();
1859
+ let node = graph::Node::new(
1860
+ Some(graph_name.into()),
1861
+ graph::Key::new(2u128),
1862
+ graph::labels!("Item"),
1863
+ value::ValueMap::new(),
1864
+ );
1865
+ store.create_nodes(&mut tx, graph_name, [&node]).unwrap();
1866
+ store
1867
+ .upsert_vector(
1868
+ &mut tx,
1869
+ graph_name,
1870
+ index_name,
1871
+ node.key(),
1872
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
1873
+ )
1874
+ .unwrap();
1875
+ tx.close().unwrap();
1876
+
1877
+ let mut tx = store.begin_read().unwrap();
1878
+ let old_hit = store
1879
+ .vector_search(
1880
+ &mut tx,
1881
+ graph_name,
1882
+ index_name,
1883
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
1884
+ 1,
1885
+ )
1886
+ .unwrap();
1887
+ let new_hit = store
1888
+ .vector_search(
1889
+ &mut tx,
1890
+ graph_name,
1891
+ index_name,
1892
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
1893
+ 1,
1894
+ )
1895
+ .unwrap();
1896
+ tx.close().unwrap();
1897
+
1898
+ assert_eq!(
1899
+ old_hit[0].node_id,
1900
+ graph::Key::new(1),
1901
+ "pre-reopen vector must remain searchable"
1902
+ );
1903
+ assert_eq!(
1904
+ new_hit[0].node_id,
1905
+ graph::Key::new(2),
1906
+ "post-reopen insert must be searchable"
1907
+ );
1908
+ }
1909
+
1910
+ /// A vector deleted (via node deletion) before close must stay gone after
1911
+ /// reopen; surviving vectors must remain searchable. Catches resurrection
1912
+ /// through stale persisted records.
1913
+ pub(crate) fn test_deleted_vector_stays_deleted_after_reopen<S: Store>(factory: &dyn Fn() -> S)
1914
+ {
1915
+ let graph_name = "reopen_deleted";
1916
+ let index_name = "idx";
1917
+
1918
+ {
1919
+ let store = factory();
1920
+ let mut tx = store.begin_write().unwrap();
1921
+ store.create_graph(&mut tx, graph_name, false).unwrap();
1922
+ store
1923
+ .create_vector_index(
1924
+ &mut tx,
1925
+ graph_name,
1926
+ VectorIndexSpec {
1927
+ name: index_name.into(),
1928
+ dimension: 3,
1929
+ metric: VectorMetric::Cosine,
1930
+ ..Default::default()
1931
+ },
1932
+ )
1933
+ .unwrap();
1934
+ for (id, v) in [
1935
+ (1u64, [1.0f32, 0.0, 0.0]),
1936
+ (2, [0.0, 1.0, 0.0]),
1937
+ (3, [0.0, 0.0, 1.0]),
1938
+ ]
1939
+ {
1940
+ let node = graph::Node::new(
1941
+ Some(graph_name.into()),
1942
+ graph::Key::new(id as u128),
1943
+ graph::labels!("Item"),
1944
+ value::ValueMap::new(),
1945
+ );
1946
+ store.create_nodes(&mut tx, graph_name, [&node]).unwrap();
1947
+ store
1948
+ .upsert_vector(
1949
+ &mut tx,
1950
+ graph_name,
1951
+ index_name,
1952
+ node.key(),
1953
+ &crate::value::Tensor::from_f32_slice(&v),
1954
+ )
1955
+ .unwrap();
1956
+ }
1957
+ tx.close().unwrap();
1958
+
1959
+ let mut tx = store.begin_write().unwrap();
1960
+ store
1961
+ .delete_nodes(
1962
+ &mut tx,
1963
+ graph_name,
1964
+ SelectNodeQuery::select_keys(vec![graph::Key::new(2)]),
1965
+ false,
1966
+ )
1967
+ .unwrap();
1968
+ tx.close().unwrap();
1969
+ }
1970
+
1971
+ let store = factory();
1972
+ let mut tx = store.begin_read().unwrap();
1973
+ let results = store
1974
+ .vector_search(
1975
+ &mut tx,
1976
+ graph_name,
1977
+ index_name,
1978
+ &crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
1979
+ 3,
1980
+ )
1981
+ .unwrap();
1982
+ tx.close().unwrap();
1983
+
1984
+ assert_eq!(
1985
+ results.len(),
1986
+ 2,
1987
+ "search should return 2 results (nodes 1 and 3), not 3"
1988
+ );
1989
+ let found_uuids: Vec<_> = results.iter().map(|r| r.node_id.uuid()).collect();
1990
+ assert!(
1991
+ found_uuids.contains(&1),
1992
+ "reopened index must still have node 1"
1993
+ );
1994
+ assert!(
1995
+ found_uuids.contains(&3),
1996
+ "reopened index must still have node 3"
1997
+ );
1998
+ assert!(
1999
+ !found_uuids.contains(&2),
2000
+ "deleted node 2 must not resurface after reopen"
2001
+ );
2002
+ }
2003
+
2004
+ /// An index created but never populated must reopen as an empty, functional
2005
+ /// index — search returns Ok(empty), not an error and not a missing-index error.
2006
+ pub(crate) fn test_empty_index_survives_reopen<S: Store>(factory: &dyn Fn() -> S)
2007
+ {
2008
+ let graph_name = "reopen_empty";
2009
+ let index_name = "idx";
2010
+
2011
+ {
2012
+ let store = factory();
2013
+ let mut tx = store.begin_write().unwrap();
2014
+ store.create_graph(&mut tx, graph_name, false).unwrap();
2015
+ store
2016
+ .create_vector_index(
2017
+ &mut tx,
2018
+ graph_name,
2019
+ VectorIndexSpec {
2020
+ name: index_name.into(),
2021
+ dimension: 3,
2022
+ metric: VectorMetric::Cosine,
2023
+ ..Default::default()
2024
+ },
2025
+ )
2026
+ .unwrap();
2027
+ tx.close().unwrap();
2028
+ }
2029
+
2030
+ let store = factory();
2031
+ let mut tx = store.begin_read().unwrap();
2032
+ let results = store
2033
+ .vector_search(
2034
+ &mut tx,
2035
+ graph_name,
2036
+ index_name,
2037
+ &crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
2038
+ 5,
2039
+ )
2040
+ .unwrap();
2041
+ tx.close().unwrap();
2042
+
2043
+ assert!(
2044
+ results.is_empty(),
2045
+ "empty index search should return empty results, not an error"
2046
+ );
2047
+ }
2048
+
2049
+ /// Generates one #[test] per generic single-open test. `$setup` must
2050
+ /// evaluate to `(guard, store)`; the guard keeps backing resources (tmp
2051
+ /// file, tmp database) alive for the test's duration. `$setup` is re-evaluated
2052
+ /// inside every generated test, so each test gets a fresh store.
2053
+ /// Extra attributes (e.g. #[serial]) are applied to every generated test.
2054
+ #[macro_export]
2055
+ macro_rules! vector_index_test_suite {
2056
+ ($(#[$attr:meta])* setup = $setup:expr) => {
2057
+ $(#[$attr])*
2058
+ #[test]
2059
+ fn test_vector_operations()
2060
+ {
2061
+ let (_guard, store) = ($setup)(0);
2062
+ super::super::test_vector_operations(store);
2063
+ }
2064
+
2065
+ $(#[$attr])*
2066
+ #[test]
2067
+ fn test_vector_search_returns_results()
2068
+ {
2069
+ let (_guard, store) = ($setup)(1);
2070
+ super::super::test_vector_search_returns_results(store);
2071
+ }
2072
+
2073
+ $(#[$attr])*
2074
+ #[test]
2075
+ fn test_vector_search_k_limit()
2076
+ {
2077
+ let (_guard, store) = ($setup)(2);
2078
+ super::super::test_vector_search_k_limit(store);
2079
+ }
2080
+
2081
+ $(#[$attr])*
2082
+ #[test]
2083
+ fn test_vector_upsert_does_not_duplicate()
2084
+ {
2085
+ let (_guard, store) = ($setup)(3);
2086
+ super::super::test_vector_upsert_does_not_duplicate(store);
2087
+ }
2088
+
2089
+ $(#[$attr])*
2090
+ #[test]
2091
+ fn test_multiple_indexes_do_not_conflict()
2092
+ {
2093
+ let (_guard, store) = ($setup)(4);
2094
+ super::super::test_multiple_indexes_do_not_conflict(store);
2095
+ }
2096
+
2097
+ $(#[$attr])*
2098
+ #[test]
2099
+ fn test_dimension_mismatch_fails()
2100
+ {
2101
+ let (_guard, store) = ($setup)(5);
2102
+ super::super::test_dimension_mismatch_fails(store);
2103
+ }
2104
+
2105
+ $(#[$attr])*
2106
+ #[test]
2107
+ fn test_upsert_on_missing_index_fails()
2108
+ {
2109
+ let (_guard, store) = ($setup)(6);
2110
+ super::super::test_upsert_on_missing_index_fails(store);
2111
+ }
2112
+
2113
+ $(#[$attr])*
2114
+ #[test]
2115
+ fn test_search_on_missing_index_fails()
2116
+ {
2117
+ let (_guard, store) = ($setup)(7);
2118
+ super::super::test_search_on_missing_index_fails(store);
2119
+ }
2120
+
2121
+ $(#[$attr])*
2122
+ #[test]
2123
+ fn test_drop_graph_cleans_vector_data()
2124
+ {
2125
+ let (_guard, store) = ($setup)(8);
2126
+ super::super::test_drop_graph_cleans_vector_data(store);
2127
+ }
2128
+
2129
+ $(#[$attr])*
2130
+ #[test]
2131
+ fn test_vector_updated_on_node_property_update()
2132
+ {
2133
+ let (_guard, store) = ($setup)(9);
2134
+ super::super::test_vector_updated_on_node_property_update(store);
2135
+ }
2136
+
2137
+ $(#[$attr])*
2138
+ #[test]
2139
+ fn test_vector_search_empty_corpus()
2140
+ {
2141
+ let (_guard, store) = ($setup)(10);
2142
+ super::super::test_vector_search_empty_corpus(store);
2143
+ }
2144
+
2145
+ $(#[$attr])*
2146
+ #[test]
2147
+ fn test_vector_search_k_larger_than_corpus()
2148
+ {
2149
+ let (_guard, store) = ($setup)(11);
2150
+ super::super::test_vector_search_k_larger_than_corpus(store);
2151
+ }
2152
+
2153
+ $(#[$attr])*
2154
+ #[test]
2155
+ fn test_vector_zero_magnitude_handling()
2156
+ {
2157
+ let (_guard, store) = ($setup)(12);
2158
+ super::super::test_vector_zero_magnitude_handling(store);
2159
+ }
2160
+
2161
+ $(#[$attr])*
2162
+ #[test]
2163
+ fn test_vector_search_k_zero()
2164
+ {
2165
+ let (_guard, store) = ($setup)(13);
2166
+ super::super::test_vector_search_k_zero(store);
2167
+ }
2168
+
2169
+ $(#[$attr])*
2170
+ #[test]
2171
+ fn test_drop_vector_index_missing_fails()
2172
+ {
2173
+ let (_guard, store) = ($setup)(14);
2174
+ super::super::test_drop_vector_index_missing_fails(store);
2175
+ }
2176
+
2177
+ $(#[$attr])*
2178
+ #[test]
2179
+ fn test_drop_vector_index_removes_index_state()
2180
+ {
2181
+ let (_guard, store) = ($setup)(15);
2182
+ super::super::test_drop_vector_index_removes_index_state(store);
2183
+ }
2184
+
2185
+ $(#[$attr])*
2186
+ #[test]
2187
+ fn test_drop_graph_recreate_index_is_clean()
2188
+ {
2189
+ let (_guard, store) = ($setup)(16);
2190
+ super::super::test_drop_graph_recreate_index_is_clean(store);
2191
+ }
2192
+
2193
+ $(#[$attr])*
2194
+ #[test]
2195
+ fn test_vector_search_ordering()
2196
+ {
2197
+ let (_guard, store) = ($setup)(17);
2198
+ super::super::test_vector_search_ordering(store);
2199
+ }
2200
+
2201
+ $(#[$attr])*
2202
+ #[test]
2203
+ fn test_vector_metrics()
2204
+ {
2205
+ let (_guard, store) = ($setup)(18);
2206
+ super::super::test_vector_metrics(store);
2207
+ }
2208
+
2209
+ $(#[$attr])*
2210
+ #[test]
2211
+ fn test_multiple_vector_indexes_isolation()
2212
+ {
2213
+ let (_guard, store) = ($setup)(19);
2214
+ super::super::test_multiple_vector_indexes_isolation(store);
2215
+ }
2216
+
2217
+ $(#[$attr])*
2218
+ #[test]
2219
+ fn test_list_vector_indexes()
2220
+ {
2221
+ let (_guard, store) = ($setup)(20);
2222
+ super::super::test_list_vector_indexes(store);
2223
+ }
2224
+
2225
+ $(#[$attr])*
2226
+ #[test]
2227
+ fn test_vector_rollback()
2228
+ {
2229
+ let (_guard, store) = ($setup)(21);
2230
+ super::super::test_vector_rollback(store);
2231
+ }
2232
+
2233
+ $(#[$attr])*
2234
+ #[test]
2235
+ fn test_vector_search_returns_correct_node()
2236
+ {
2237
+ let (_guard, store) = ($setup)(22);
2238
+ super::super::test_vector_search_returns_correct_node(store);
2239
+ }
2240
+
2241
+ $(#[$attr])*
2242
+ #[test]
2243
+ fn test_vector_cleanup_after_node_deletion()
2244
+ {
2245
+ let (_guard, store) = ($setup)(23);
2246
+ super::super::test_vector_cleanup_after_node_deletion(store);
2247
+ }
2248
+
2249
+ $(#[$attr])*
2250
+ #[test]
2251
+ fn test_vector_index_on_non_default_graph()
2252
+ {
2253
+ let (_guard, store) = ($setup)(24);
2254
+ super::super::test_vector_index_on_non_default_graph(store);
2255
+ }
2256
+
2257
+ $(#[$attr])*
2258
+ #[test]
2259
+ fn test_vector_index_not_visible_from_a_different_graph()
2260
+ {
2261
+ let (_guard, store) = ($setup)(25);
2262
+ super::super::test_vector_index_not_visible_from_a_different_graph(store);
2263
+ }
2264
+
2265
+ $(#[$attr])*
2266
+ #[test]
2267
+ fn test_same_index_name_on_different_graphs_does_not_conflict()
2268
+ {
2269
+ let (_guard, store) = ($setup)(26);
2270
+ super::super::test_same_index_name_on_different_graphs_does_not_conflict(store);
2271
+ }
2272
+ };
2273
+ }
2274
+
2275
+ /// Reopen suite: `$setup` evaluates to `(guard, factory)` where the factory
2276
+ /// constructs a store against the same backing storage on every call.
2277
+ /// Extra attributes (e.g. #[serial]) are applied to every generated test.
2278
+ #[macro_export]
2279
+ macro_rules! vector_index_reopen_suite {
2280
+ ($(#[$attr:meta])* setup = $setup:expr) => {
2281
+ $(#[$attr])*
2282
+ #[test]
2283
+ fn test_vector_index_survives_reopen()
2284
+ {
2285
+ let (_guard, factory) = ($setup)(0);
2286
+ super::super::test_vector_index_survives_reopen(&factory);
2287
+ }
2288
+
2289
+ $(#[$attr])*
2290
+ #[test]
2291
+ fn test_insert_after_reopen()
2292
+ {
2293
+ let (_guard, factory) = ($setup)(1);
2294
+ super::super::test_insert_after_reopen(&factory);
2295
+ }
2296
+
2297
+ $(#[$attr])*
2298
+ #[test]
2299
+ fn test_deleted_vector_stays_deleted_after_reopen()
2300
+ {
2301
+ let (_guard, factory) = ($setup)(2);
2302
+ super::super::test_deleted_vector_stays_deleted_after_reopen(&factory);
2303
+ }
2304
+
2305
+ $(#[$attr])*
2306
+ #[test]
2307
+ fn test_empty_index_survives_reopen()
2308
+ {
2309
+ let (_guard, factory) = ($setup)(3);
2310
+ super::super::test_empty_index_survives_reopen(&factory);
2311
+ }
2312
+ };
2313
+ }