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
|
@@ -1,22 +1,105 @@
|
|
|
1
1
|
use std::cell::RefCell;
|
|
2
2
|
|
|
3
3
|
use ccutils::pool::{self, Pool};
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
4
5
|
|
|
5
6
|
use crate::{
|
|
6
7
|
prelude::*,
|
|
7
8
|
store::{
|
|
9
|
+
TransactionBoxable, VectorMetric,
|
|
8
10
|
sqlbase::{SqlBindingValue, SqlResultValue},
|
|
9
|
-
TransactionBoxable,
|
|
10
11
|
},
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
ccutils::assert_impl_all!(Store: Sync, Send);
|
|
14
15
|
|
|
15
16
|
use askama::Template;
|
|
16
|
-
use postgres::{types::ToSql
|
|
17
|
+
use postgres::{NoTls, types::ToSql};
|
|
17
18
|
|
|
18
19
|
const SCHEMA: &str = "public";
|
|
19
20
|
|
|
21
|
+
/// Generate a vector index table name from graph and index names
|
|
22
|
+
fn vector_index_table_name(graph_name: &str, index_name: &str) -> String
|
|
23
|
+
{
|
|
24
|
+
format!("{}_{}", graph_name, index_name)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Convert hex-formatted u128 string back to UUID string format
|
|
28
|
+
fn format_hex_to_uuid(hex_str: &str) -> String
|
|
29
|
+
{
|
|
30
|
+
// Convert hex string back to u128, then to UUID
|
|
31
|
+
if let Ok(val) = u128::from_str_radix(hex_str, 16)
|
|
32
|
+
{
|
|
33
|
+
uuid::Uuid::from_u128(val).to_string()
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
{
|
|
37
|
+
// If parsing fails, return the original string (will cause SQL error)
|
|
38
|
+
hex_str.to_string()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn cosine_distance(query: &[f32], stored: &[f32]) -> f32
|
|
43
|
+
{
|
|
44
|
+
if query.len() != stored.len()
|
|
45
|
+
{
|
|
46
|
+
return f32::INFINITY;
|
|
47
|
+
}
|
|
48
|
+
let query_norm = query.iter().map(|v| v * v).sum::<f32>().sqrt();
|
|
49
|
+
let stored_norm = stored.iter().map(|v| v * v).sum::<f32>().sqrt();
|
|
50
|
+
if query_norm == 0.0 || stored_norm == 0.0
|
|
51
|
+
{
|
|
52
|
+
return f32::INFINITY;
|
|
53
|
+
}
|
|
54
|
+
let dot = query
|
|
55
|
+
.iter()
|
|
56
|
+
.zip(stored.iter())
|
|
57
|
+
.map(|(a, b)| a * b)
|
|
58
|
+
.sum::<f32>();
|
|
59
|
+
let cosine_similarity = (dot / (query_norm * stored_norm)).clamp(-1.0, 1.0);
|
|
60
|
+
1.0 - cosine_similarity
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Map VectorMetric to pgvector operators
|
|
64
|
+
fn metric_to_pgvector_operator(metric: Option<&super::VectorMetric>) -> &'static str
|
|
65
|
+
{
|
|
66
|
+
match metric
|
|
67
|
+
{
|
|
68
|
+
Some(super::VectorMetric::Cosine) | None => "<=>",
|
|
69
|
+
Some(super::VectorMetric::L2) => "<->",
|
|
70
|
+
Some(super::VectorMetric::DotProduct) => "<#>",
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Serialize a vector to pgvector binary format for use with postgres crate
|
|
75
|
+
/// pgvector expects vectors as arrays that can be passed as text
|
|
76
|
+
fn serialize_vector_for_pgvector(vector: &[f32]) -> String
|
|
77
|
+
{
|
|
78
|
+
format!(
|
|
79
|
+
"[{}]",
|
|
80
|
+
vector
|
|
81
|
+
.iter()
|
|
82
|
+
.map(|v| v.to_string())
|
|
83
|
+
.collect::<Vec<_>>()
|
|
84
|
+
.join(",")
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Parse a vector from pgvector result
|
|
89
|
+
fn parse_pgvector_from_string(s: &str) -> Result<Vec<f32>>
|
|
90
|
+
{
|
|
91
|
+
s.trim_start_matches('[')
|
|
92
|
+
.trim_end_matches(']')
|
|
93
|
+
.split(',')
|
|
94
|
+
.map(|part| {
|
|
95
|
+
part
|
|
96
|
+
.trim()
|
|
97
|
+
.parse::<f32>()
|
|
98
|
+
.map_err(|e| StoreError::InvalidFormat(format!("Invalid vector component: {}", e)).into())
|
|
99
|
+
})
|
|
100
|
+
.collect()
|
|
101
|
+
}
|
|
102
|
+
|
|
20
103
|
// _____ _____ ____ _
|
|
21
104
|
// |_ _|__ | ___| __ ___ _ __ ___ / ___| __ _| |
|
|
22
105
|
// | |/ _ \| |_ | '__/ _ \| '_ ` _ \\___ \ / _` | |
|
|
@@ -104,7 +187,28 @@ impl sqlbase::Row for postgres::Row
|
|
|
104
187
|
.collect(),
|
|
105
188
|
)),
|
|
106
189
|
&Type::JSONB => Ok(SqlResultValue::JsonValue(self.try_get(index)?)),
|
|
107
|
-
_ =>
|
|
190
|
+
_ =>
|
|
191
|
+
{
|
|
192
|
+
// Try to handle pgvector as a custom type - pgvector values come as text representation
|
|
193
|
+
if ty.name() == "vector"
|
|
194
|
+
{
|
|
195
|
+
let vector_str: String = self.try_get(index)?;
|
|
196
|
+
let vector = parse_pgvector_from_string(&vector_str)?;
|
|
197
|
+
// Return as blob since we don't have a dedicated vector type in SqlResultValue
|
|
198
|
+
// The caller will handle the blob as a vector
|
|
199
|
+
Ok(SqlResultValue::Blob(
|
|
200
|
+
vector
|
|
201
|
+
.iter()
|
|
202
|
+
.flat_map(|f| f.to_le_bytes())
|
|
203
|
+
.collect::<Vec<_>>()
|
|
204
|
+
.leak(),
|
|
205
|
+
))
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
{
|
|
209
|
+
Err(StoreError::UnsupportedPostgresType(ty.name().to_string()).into())
|
|
210
|
+
}
|
|
211
|
+
}
|
|
108
212
|
}
|
|
109
213
|
}
|
|
110
214
|
}
|
|
@@ -126,14 +230,12 @@ impl Drop for TransactionBase
|
|
|
126
230
|
fn drop(&mut self)
|
|
127
231
|
{
|
|
128
232
|
if *self.active.borrow()
|
|
233
|
+
&& let Err(e) = self.client.query("ROLLBACK", &[])
|
|
129
234
|
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
e
|
|
135
|
-
);
|
|
136
|
-
}
|
|
235
|
+
log::error!(
|
|
236
|
+
"Rollback failed with error {:?}, future use of the connection are likely to fail.",
|
|
237
|
+
e
|
|
238
|
+
);
|
|
137
239
|
}
|
|
138
240
|
}
|
|
139
241
|
}
|
|
@@ -307,6 +409,16 @@ mod templates
|
|
|
307
409
|
}
|
|
308
410
|
}
|
|
309
411
|
|
|
412
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
413
|
+
struct VectorIndexMetadata
|
|
414
|
+
{
|
|
415
|
+
name: String,
|
|
416
|
+
label: Option<String>,
|
|
417
|
+
property: Option<String>,
|
|
418
|
+
dimension: usize,
|
|
419
|
+
metric: VectorMetric,
|
|
420
|
+
}
|
|
421
|
+
|
|
310
422
|
// ____ _
|
|
311
423
|
// / ___|| |_ ___ _ __ ___
|
|
312
424
|
// \___ \| __/ _ \| '__/ _ \
|
|
@@ -330,16 +442,103 @@ impl Store
|
|
|
330
442
|
move || Ok(config.connect(NoTls)?),
|
|
331
443
|
pool::Options::default().minimum_pool_size(1).pool_size(3),
|
|
332
444
|
)?;
|
|
445
|
+
{
|
|
446
|
+
let mut setup_client = client.get()?;
|
|
447
|
+
setup_client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?;
|
|
448
|
+
}
|
|
333
449
|
|
|
334
450
|
let s = Self { client };
|
|
335
451
|
s.initialise()?;
|
|
336
452
|
Ok(s)
|
|
337
453
|
}
|
|
338
|
-
fn upgrade_database(&self,
|
|
339
|
-
-> Result<()>
|
|
454
|
+
fn upgrade_database(&self, transaction: &mut TransactionBox, from: utils::Version) -> Result<()>
|
|
340
455
|
{
|
|
341
|
-
|
|
342
|
-
|
|
456
|
+
match (from.major, from.minor)
|
|
457
|
+
{
|
|
458
|
+
(1, 2) =>
|
|
459
|
+
{
|
|
460
|
+
// Migrate vector index metadata from the 1.2 dual-key format:
|
|
461
|
+
// {graph}/vector_index_list -> Vec<String>
|
|
462
|
+
// {graph}/vector_indices/{name} -> VectorIndexMetadata (without name field)
|
|
463
|
+
// {graph}_{name}/metric -> VectorMetric (legacy, may or may not exist)
|
|
464
|
+
// to the 1.3 unified format:
|
|
465
|
+
// {graph}/vector_indices -> Vec<VectorIndexMetadata> (with name field)
|
|
466
|
+
|
|
467
|
+
#[derive(serde::Deserialize)]
|
|
468
|
+
struct LegacyVectorIndexMetadata
|
|
469
|
+
{
|
|
470
|
+
label: Option<String>,
|
|
471
|
+
property: Option<String>,
|
|
472
|
+
dimension: usize,
|
|
473
|
+
metric: VectorMetric,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let graphs: Vec<String> = self.get_metadata_value_json_or_default(transaction, "graphs")?;
|
|
477
|
+
|
|
478
|
+
for graph_name in &graphs
|
|
479
|
+
{
|
|
480
|
+
let list_key = format!("{}/vector_index_list", graph_name);
|
|
481
|
+
let index_names: Vec<String> =
|
|
482
|
+
self.get_metadata_value_json_or_default(transaction, &list_key)?;
|
|
483
|
+
|
|
484
|
+
let mut new_list: Vec<VectorIndexMetadata> = Vec::new();
|
|
485
|
+
for index_name in &index_names
|
|
486
|
+
{
|
|
487
|
+
let legacy_key = format!("{}/vector_indices/{}", graph_name, index_name);
|
|
488
|
+
match self
|
|
489
|
+
.get_metadata_value_json::<LegacyVectorIndexMetadata>(transaction, &legacy_key)
|
|
490
|
+
{
|
|
491
|
+
Ok(legacy) =>
|
|
492
|
+
{
|
|
493
|
+
new_list.push(VectorIndexMetadata {
|
|
494
|
+
name: index_name.clone(),
|
|
495
|
+
label: legacy.label,
|
|
496
|
+
property: legacy.property,
|
|
497
|
+
dimension: legacy.dimension,
|
|
498
|
+
metric: legacy.metric,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
Err(e) =>
|
|
502
|
+
{
|
|
503
|
+
log::warn!(
|
|
504
|
+
"Skipping vector index '{}' in graph '{}' during migration: {}",
|
|
505
|
+
index_name,
|
|
506
|
+
graph_name,
|
|
507
|
+
e
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Clean up old per-index metadata key
|
|
513
|
+
self.delete_metadata(transaction, &legacy_key)?;
|
|
514
|
+
|
|
515
|
+
// Clean up legacy metric key if it exists
|
|
516
|
+
let table_name = vector_index_table_name(graph_name, index_name);
|
|
517
|
+
let metric_key = format!("{}/metric", table_name);
|
|
518
|
+
self.delete_metadata(transaction, &metric_key)?;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Write consolidated list
|
|
522
|
+
self.set_metadata_value_json(
|
|
523
|
+
transaction,
|
|
524
|
+
format!("{}/vector_indices", graph_name),
|
|
525
|
+
&new_list,
|
|
526
|
+
)?;
|
|
527
|
+
|
|
528
|
+
// Remove old list key
|
|
529
|
+
self.delete_metadata(transaction, &list_key)?;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
Ok(())
|
|
533
|
+
}
|
|
534
|
+
_ => Err(
|
|
535
|
+
StoreError::IncompatibleVersion {
|
|
536
|
+
expected: consts::GQLITE_VERSION,
|
|
537
|
+
actual: from,
|
|
538
|
+
}
|
|
539
|
+
.into(),
|
|
540
|
+
),
|
|
541
|
+
}
|
|
343
542
|
}
|
|
344
543
|
/// Check if table exists
|
|
345
544
|
pub(crate) fn check_if_table_exists(
|
|
@@ -368,6 +567,10 @@ impl super::sqlbase::SqlMetaDataQueries for Store
|
|
|
368
567
|
{
|
|
369
568
|
Ok(include_str!("../../templates/sql/postgres/metadata_set.sql").to_string())
|
|
370
569
|
}
|
|
570
|
+
fn metadata_delete_query() -> Result<String>
|
|
571
|
+
{
|
|
572
|
+
Ok(include_str!("../../templates/sql/postgres/metadata_delete.sql").to_string())
|
|
573
|
+
}
|
|
371
574
|
}
|
|
372
575
|
|
|
373
576
|
impl super::sqlbase::SqlStore for Store
|
|
@@ -499,6 +702,529 @@ impl super::sqlbase::SqlStore for Store
|
|
|
499
702
|
}
|
|
500
703
|
Ok(())
|
|
501
704
|
}
|
|
705
|
+
|
|
706
|
+
/// Create a vector index in PostgreSQL using pgvector extension
|
|
707
|
+
fn create_vector_index_impl(
|
|
708
|
+
&self,
|
|
709
|
+
transaction: &mut Self::TransactionBox,
|
|
710
|
+
graph_name: &str,
|
|
711
|
+
spec: &super::store::VectorIndexSpec,
|
|
712
|
+
) -> Result<()>
|
|
713
|
+
{
|
|
714
|
+
if !spec.dtype.is_float()
|
|
715
|
+
{
|
|
716
|
+
return Err(
|
|
717
|
+
crate::error::StoreError::InvalidFormat(format!(
|
|
718
|
+
"PostgreSQL vector index only supports floating-point dtypes (F32 or F64); got {:?}",
|
|
719
|
+
spec.dtype
|
|
720
|
+
))
|
|
721
|
+
.into(),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Check if index already exists
|
|
726
|
+
let mut indices: Vec<VectorIndexMetadata> = self
|
|
727
|
+
.get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
|
|
728
|
+
if indices.iter().any(|m| m.name == spec.name)
|
|
729
|
+
{
|
|
730
|
+
return Err(
|
|
731
|
+
crate::error::StoreError::DuplicatedVectorIndex {
|
|
732
|
+
index_name: spec.name.clone(),
|
|
733
|
+
}
|
|
734
|
+
.into(),
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let table_name = vector_index_table_name(graph_name, &spec.name);
|
|
739
|
+
|
|
740
|
+
// Create vector index table with node_id UUID and embedding vector columns
|
|
741
|
+
let create_table_sql = format!(
|
|
742
|
+
"CREATE TABLE IF NOT EXISTS {} (node_id UUID PRIMARY KEY, embedding vector({}))",
|
|
743
|
+
table_name, spec.dimension
|
|
744
|
+
);
|
|
745
|
+
transaction.get_client().execute(&create_table_sql, &[])?;
|
|
746
|
+
|
|
747
|
+
// Create index based on metric type
|
|
748
|
+
let (index_type, operator_class) = match spec.metric
|
|
749
|
+
{
|
|
750
|
+
super::store::VectorMetric::Cosine => ("hnsw", "vector_cosine_ops"),
|
|
751
|
+
super::store::VectorMetric::L2 => ("hnsw", "vector_l2_ops"),
|
|
752
|
+
super::store::VectorMetric::DotProduct => ("hnsw", "vector_ip_ops"),
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
let index_name = format!("idx_{}_{}", graph_name, spec.name);
|
|
756
|
+
let create_index_sql = format!(
|
|
757
|
+
"CREATE INDEX IF NOT EXISTS {} ON {} USING {} (embedding {})",
|
|
758
|
+
index_name, table_name, index_type, operator_class
|
|
759
|
+
);
|
|
760
|
+
transaction.get_client().execute(&create_index_sql, &[])?;
|
|
761
|
+
|
|
762
|
+
// Append metadata and persist the unified list
|
|
763
|
+
indices.push(VectorIndexMetadata {
|
|
764
|
+
name: spec.name.clone(),
|
|
765
|
+
label: spec.label.clone(),
|
|
766
|
+
property: spec.property.clone(),
|
|
767
|
+
dimension: spec.dimension,
|
|
768
|
+
metric: spec.metric,
|
|
769
|
+
});
|
|
770
|
+
self.set_metadata_value_json(
|
|
771
|
+
transaction,
|
|
772
|
+
format!("{}/vector_indices", graph_name),
|
|
773
|
+
&indices,
|
|
774
|
+
)?;
|
|
775
|
+
|
|
776
|
+
if let Some(label) = &spec.label
|
|
777
|
+
&& let Some(property) = &spec.property
|
|
778
|
+
{
|
|
779
|
+
let nodes_table = format!("gqlite_{}_nodes", graph_name);
|
|
780
|
+
let select_nodes_sql = format!(
|
|
781
|
+
"SELECT node_key, properties FROM {} WHERE labels @> ARRAY[$1]",
|
|
782
|
+
nodes_table
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
let rows = match transaction.get_client().query(&select_nodes_sql, &[&label])
|
|
786
|
+
{
|
|
787
|
+
Ok(rows) => rows,
|
|
788
|
+
Err(_) => return Ok(()),
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
for row in rows
|
|
792
|
+
{
|
|
793
|
+
let node_uuid: uuid::Uuid = row.get(0);
|
|
794
|
+
|
|
795
|
+
// Get properties as either String or directly as serde_json::Value
|
|
796
|
+
let props = match row.try_get::<_, String>(1)
|
|
797
|
+
{
|
|
798
|
+
Ok(json_str) => serde_json::from_str::<serde_json::Value>(&json_str).ok(),
|
|
799
|
+
Err(_) =>
|
|
800
|
+
{
|
|
801
|
+
// Try getting as JSON directly
|
|
802
|
+
row.try_get::<_, serde_json::Value>(1).ok()
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
if let Some(props) = props
|
|
807
|
+
&& let Some(value) = props.get(property)
|
|
808
|
+
&& let Some(vector) =
|
|
809
|
+
crate::store::vector_extract::extract_f32_vec_from_serde_value(value)
|
|
810
|
+
&& !vector.is_empty()
|
|
811
|
+
&& !vector.iter().any(|v| !v.is_finite())
|
|
812
|
+
{
|
|
813
|
+
let vector_str = serialize_vector_for_pgvector(&vector);
|
|
814
|
+
let insert_sql = format!(
|
|
815
|
+
"INSERT INTO {} (node_id, embedding) VALUES ($1, '{}'::vector) ON CONFLICT (node_id) DO UPDATE SET embedding = EXCLUDED.embedding",
|
|
816
|
+
table_name, vector_str
|
|
817
|
+
);
|
|
818
|
+
transaction
|
|
819
|
+
.get_client()
|
|
820
|
+
.execute(&insert_sql, &[&node_uuid])?;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
Ok(())
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/// Upsert a vector embedding in PostgreSQL
|
|
829
|
+
fn upsert_vector_impl(
|
|
830
|
+
&self,
|
|
831
|
+
transaction: &mut Self::TransactionBox,
|
|
832
|
+
graph_name: &str,
|
|
833
|
+
index_name: &str,
|
|
834
|
+
node_id: crate::graph::Key,
|
|
835
|
+
tensor: &crate::value::Tensor,
|
|
836
|
+
) -> Result<()>
|
|
837
|
+
{
|
|
838
|
+
let f32_data: std::borrow::Cow<[f32]> = match tensor.dtype()
|
|
839
|
+
{
|
|
840
|
+
crate::value::DType::F32 => match tensor.as_f32_slice()
|
|
841
|
+
{
|
|
842
|
+
Some(s) => std::borrow::Cow::Borrowed(s),
|
|
843
|
+
None => std::borrow::Cow::Owned(tensor.to_f32_vec()),
|
|
844
|
+
},
|
|
845
|
+
crate::value::DType::F64 => std::borrow::Cow::Owned(tensor.to_f32_vec()),
|
|
846
|
+
other =>
|
|
847
|
+
{
|
|
848
|
+
return Err(
|
|
849
|
+
StoreError::InvalidFormat(format!(
|
|
850
|
+
"PostgreSQL vector index only accepts F32 or F64 tensors; got {:?}",
|
|
851
|
+
other
|
|
852
|
+
))
|
|
853
|
+
.into(),
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
if f32_data.iter().any(|v| !v.is_finite())
|
|
859
|
+
{
|
|
860
|
+
return Err(
|
|
861
|
+
StoreError::InvalidFormat("vector contains non-finite value (NaN or Inf)".to_string())
|
|
862
|
+
.into(),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let table_name = vector_index_table_name(graph_name, index_name);
|
|
867
|
+
let client = transaction.get_client();
|
|
868
|
+
|
|
869
|
+
// Serialize vector to pgvector format
|
|
870
|
+
let vector_str = serialize_vector_for_pgvector(&f32_data);
|
|
871
|
+
|
|
872
|
+
// Convert node_id to UUID
|
|
873
|
+
let node_uuid = uuid::Uuid::from_u128(node_id.uuid());
|
|
874
|
+
|
|
875
|
+
// Use PostgreSQL UPSERT pattern - embed vector in SQL since postgres-rs doesn't support pgvector parameters
|
|
876
|
+
let upsert_sql = format!(
|
|
877
|
+
"INSERT INTO {} (node_id, embedding) VALUES ($1, '{}'::vector) \
|
|
878
|
+
ON CONFLICT (node_id) DO UPDATE SET embedding = EXCLUDED.embedding",
|
|
879
|
+
table_name, vector_str
|
|
880
|
+
);
|
|
881
|
+
let result = client.execute(&upsert_sql, &[&node_uuid]);
|
|
882
|
+
match result
|
|
883
|
+
{
|
|
884
|
+
Err(e) if e.to_string().contains("does not exist") => Err(
|
|
885
|
+
StoreError::InvalidFormat(format!(
|
|
886
|
+
"Vector index '{}' does not exist. Create it first with create_vector_index.",
|
|
887
|
+
index_name
|
|
888
|
+
))
|
|
889
|
+
.into(),
|
|
890
|
+
),
|
|
891
|
+
other => other.map(|_| ()).map_err(Into::into),
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
fn list_vector_indexes_impl(
|
|
896
|
+
&self,
|
|
897
|
+
transaction: &mut Self::TransactionBox,
|
|
898
|
+
graph_name: &str,
|
|
899
|
+
) -> Result<Vec<super::store::VectorIndexSpec>>
|
|
900
|
+
{
|
|
901
|
+
let indices: Vec<VectorIndexMetadata> = self
|
|
902
|
+
.get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
|
|
903
|
+
|
|
904
|
+
let specs = indices
|
|
905
|
+
.into_iter()
|
|
906
|
+
.map(|m| super::store::VectorIndexSpec {
|
|
907
|
+
name: m.name,
|
|
908
|
+
label: m.label,
|
|
909
|
+
property: m.property,
|
|
910
|
+
dimension: m.dimension,
|
|
911
|
+
metric: m.metric,
|
|
912
|
+
..Default::default()
|
|
913
|
+
})
|
|
914
|
+
.collect();
|
|
915
|
+
|
|
916
|
+
Ok(specs)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/// Search for similar vectors in PostgreSQL
|
|
920
|
+
fn vector_search_impl(
|
|
921
|
+
&self,
|
|
922
|
+
transaction: &mut Self::TransactionBox,
|
|
923
|
+
graph_name: &str,
|
|
924
|
+
index_name: &str,
|
|
925
|
+
query: &crate::value::Tensor,
|
|
926
|
+
k: usize,
|
|
927
|
+
) -> Result<Vec<super::store::VectorSearchResult>>
|
|
928
|
+
{
|
|
929
|
+
if k == 0
|
|
930
|
+
{
|
|
931
|
+
return Ok(Vec::new());
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
let f32_query: std::borrow::Cow<[f32]> = match query.dtype()
|
|
935
|
+
{
|
|
936
|
+
crate::value::DType::F32 => match query.as_f32_slice()
|
|
937
|
+
{
|
|
938
|
+
Some(s) => std::borrow::Cow::Borrowed(s),
|
|
939
|
+
None => std::borrow::Cow::Owned(query.to_f32_vec()),
|
|
940
|
+
},
|
|
941
|
+
crate::value::DType::F64 => std::borrow::Cow::Owned(query.to_f32_vec()),
|
|
942
|
+
other =>
|
|
943
|
+
{
|
|
944
|
+
return Err(
|
|
945
|
+
StoreError::InvalidFormat(format!(
|
|
946
|
+
"PostgreSQL vector search only accepts F32 or F64 tensors; got {:?}",
|
|
947
|
+
other
|
|
948
|
+
))
|
|
949
|
+
.into(),
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
if f32_query.iter().any(|v| !v.is_finite())
|
|
955
|
+
{
|
|
956
|
+
return Err(
|
|
957
|
+
StoreError::InvalidFormat("vector contains non-finite value (NaN or Inf)".to_string())
|
|
958
|
+
.into(),
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
let table_name = vector_index_table_name(graph_name, index_name);
|
|
963
|
+
|
|
964
|
+
let indices: Vec<VectorIndexMetadata> = self
|
|
965
|
+
.get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
|
|
966
|
+
let metadata = indices
|
|
967
|
+
.into_iter()
|
|
968
|
+
.find(|m| m.name == index_name)
|
|
969
|
+
.ok_or_else(|| {
|
|
970
|
+
StoreError::InvalidFormat(format!(
|
|
971
|
+
"Vector index '{}' does not exist in graph '{}'",
|
|
972
|
+
index_name, graph_name
|
|
973
|
+
))
|
|
974
|
+
})?;
|
|
975
|
+
|
|
976
|
+
if f32_query.len() != metadata.dimension
|
|
977
|
+
{
|
|
978
|
+
return Err(
|
|
979
|
+
StoreError::InvalidFormat(format!(
|
|
980
|
+
"Vector dimension {} does not match index dimension {}",
|
|
981
|
+
f32_query.len(),
|
|
982
|
+
metadata.dimension
|
|
983
|
+
))
|
|
984
|
+
.into(),
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
let metric = metadata.metric;
|
|
989
|
+
|
|
990
|
+
if metric == super::VectorMetric::Cosine
|
|
991
|
+
{
|
|
992
|
+
let full_scan_sql = format!("SELECT node_id, embedding::text FROM {}", table_name);
|
|
993
|
+
let client = transaction.get_client();
|
|
994
|
+
let rows = client.query(&full_scan_sql, &[])?;
|
|
995
|
+
|
|
996
|
+
let mut results: Vec<super::store::VectorSearchResult> = rows
|
|
997
|
+
.into_iter()
|
|
998
|
+
.map(|row| {
|
|
999
|
+
let node_uuid: uuid::Uuid = row.get(0);
|
|
1000
|
+
let embedding_text: String = row.get(1);
|
|
1001
|
+
let embedding = parse_pgvector_from_string(&embedding_text)?;
|
|
1002
|
+
let score = cosine_distance(&f32_query, &embedding);
|
|
1003
|
+
|
|
1004
|
+
Ok(super::store::VectorSearchResult {
|
|
1005
|
+
node_id: crate::graph::Key::new(node_uuid.as_u128()),
|
|
1006
|
+
score,
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
.collect::<Result<_>>()?;
|
|
1010
|
+
|
|
1011
|
+
results.sort_by(|a, b| {
|
|
1012
|
+
a.score
|
|
1013
|
+
.partial_cmp(&b.score)
|
|
1014
|
+
.unwrap_or(std::cmp::Ordering::Equal)
|
|
1015
|
+
});
|
|
1016
|
+
results.truncate(k);
|
|
1017
|
+
return Ok(results);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
let operator = metric_to_pgvector_operator(Some(&metric));
|
|
1021
|
+
|
|
1022
|
+
let query_str = serialize_vector_for_pgvector(&f32_query);
|
|
1023
|
+
let search_sql = format!(
|
|
1024
|
+
"SELECT node_id, (embedding {} '{}'::vector) AS score FROM {} ORDER BY embedding {} '{}'::vector LIMIT $1",
|
|
1025
|
+
operator, query_str, table_name, operator, query_str,
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
let client = transaction.get_client();
|
|
1029
|
+
let rows = client.query(&search_sql, &[&(k as i64)])?;
|
|
1030
|
+
|
|
1031
|
+
let results: Vec<super::store::VectorSearchResult> = rows
|
|
1032
|
+
.into_iter()
|
|
1033
|
+
.map(|row| {
|
|
1034
|
+
let node_uuid: uuid::Uuid = row.get(0);
|
|
1035
|
+
// The distance value from pgvector might be a different type
|
|
1036
|
+
// Try to get it as a generic value and convert
|
|
1037
|
+
let score: f32 = match row.try_get::<_, f64>(1)
|
|
1038
|
+
{
|
|
1039
|
+
Ok(val) => val as f32,
|
|
1040
|
+
Err(_) =>
|
|
1041
|
+
{
|
|
1042
|
+
// Try as f32 directly
|
|
1043
|
+
match row.try_get::<_, f32>(1)
|
|
1044
|
+
{
|
|
1045
|
+
Ok(val) => val,
|
|
1046
|
+
Err(_) =>
|
|
1047
|
+
{
|
|
1048
|
+
// Try as i32 or i64
|
|
1049
|
+
match row.try_get::<_, i32>(1)
|
|
1050
|
+
{
|
|
1051
|
+
Ok(val) => val as f32,
|
|
1052
|
+
Err(_) => match row.try_get::<_, i64>(1)
|
|
1053
|
+
{
|
|
1054
|
+
Ok(val) => val as f32,
|
|
1055
|
+
Err(_) => 0.0,
|
|
1056
|
+
},
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
super::store::VectorSearchResult {
|
|
1064
|
+
node_id: crate::graph::Key::new(node_uuid.as_u128()),
|
|
1065
|
+
score,
|
|
1066
|
+
}
|
|
1067
|
+
})
|
|
1068
|
+
.collect();
|
|
1069
|
+
|
|
1070
|
+
Ok(results)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
fn drop_vector_index_impl(
|
|
1074
|
+
&self,
|
|
1075
|
+
transaction: &mut Self::TransactionBox,
|
|
1076
|
+
graph_name: &str,
|
|
1077
|
+
index_name: &str,
|
|
1078
|
+
) -> Result<()>
|
|
1079
|
+
{
|
|
1080
|
+
let mut indices: Vec<VectorIndexMetadata> = self
|
|
1081
|
+
.get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
|
|
1082
|
+
|
|
1083
|
+
if !indices.iter().any(|m| m.name == index_name)
|
|
1084
|
+
{
|
|
1085
|
+
return Err(
|
|
1086
|
+
StoreError::InvalidFormat(format!(
|
|
1087
|
+
"Vector index '{}' does not exist in graph '{}'",
|
|
1088
|
+
index_name, graph_name
|
|
1089
|
+
))
|
|
1090
|
+
.into(),
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
let table_name = vector_index_table_name(graph_name, index_name);
|
|
1095
|
+
let index_table_sql = format!("DROP TABLE IF EXISTS {}", table_name);
|
|
1096
|
+
transaction.get_client().execute(&index_table_sql, &[])?;
|
|
1097
|
+
|
|
1098
|
+
let hnsw_index_name = format!("idx_{}_{}", graph_name, index_name);
|
|
1099
|
+
let drop_hnsw_index_sql = format!("DROP INDEX IF EXISTS {}", hnsw_index_name);
|
|
1100
|
+
transaction
|
|
1101
|
+
.get_client()
|
|
1102
|
+
.execute(&drop_hnsw_index_sql, &[])?;
|
|
1103
|
+
|
|
1104
|
+
indices.retain(|m| m.name != index_name);
|
|
1105
|
+
self.set_metadata_value_json(
|
|
1106
|
+
transaction,
|
|
1107
|
+
format!("{}/vector_indices", graph_name),
|
|
1108
|
+
&indices,
|
|
1109
|
+
)?;
|
|
1110
|
+
|
|
1111
|
+
Ok(())
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
fn drop_graph_vector_indices_impl(
|
|
1115
|
+
&self,
|
|
1116
|
+
transaction: &mut Self::TransactionBox,
|
|
1117
|
+
graph_name: &str,
|
|
1118
|
+
) -> Result<()>
|
|
1119
|
+
{
|
|
1120
|
+
let indices_key = format!("{}/vector_indices", graph_name);
|
|
1121
|
+
let indices: Vec<VectorIndexMetadata> =
|
|
1122
|
+
self.get_metadata_value_json_or_default(transaction, &indices_key)?;
|
|
1123
|
+
|
|
1124
|
+
for m in &indices
|
|
1125
|
+
{
|
|
1126
|
+
let table_name = vector_index_table_name(graph_name, &m.name);
|
|
1127
|
+
transaction
|
|
1128
|
+
.get_client()
|
|
1129
|
+
.execute(&format!("DROP TABLE IF EXISTS {}", table_name), &[])?;
|
|
1130
|
+
let hnsw_index_name = format!("idx_{}_{}", graph_name, m.name);
|
|
1131
|
+
transaction
|
|
1132
|
+
.get_client()
|
|
1133
|
+
.execute(&format!("DROP INDEX IF EXISTS {}", hnsw_index_name), &[])?;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
self.delete_metadata(transaction, &indices_key)?;
|
|
1137
|
+
Ok(())
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
fn get_vector_indices_for_property_impl(
|
|
1141
|
+
&self,
|
|
1142
|
+
transaction: &mut Self::TransactionBox,
|
|
1143
|
+
graph_name: &str,
|
|
1144
|
+
label: &str,
|
|
1145
|
+
property: &str,
|
|
1146
|
+
) -> Result<Vec<(String, usize, super::store::VectorMetric)>>
|
|
1147
|
+
{
|
|
1148
|
+
let indices: Vec<VectorIndexMetadata> = self
|
|
1149
|
+
.get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
|
|
1150
|
+
|
|
1151
|
+
let result = indices
|
|
1152
|
+
.into_iter()
|
|
1153
|
+
.filter(|m| m.label.as_deref() == Some(label) && m.property.as_deref() == Some(property))
|
|
1154
|
+
.map(|m| (m.name, m.dimension, m.metric))
|
|
1155
|
+
.collect();
|
|
1156
|
+
|
|
1157
|
+
Ok(result)
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
fn apply_vector_indices_for_node_impl(
|
|
1161
|
+
&self,
|
|
1162
|
+
transaction: &mut Self::TransactionBox,
|
|
1163
|
+
graph_name: &str,
|
|
1164
|
+
node: &crate::graph::Node,
|
|
1165
|
+
) -> Result<()>
|
|
1166
|
+
{
|
|
1167
|
+
for label in node.labels()
|
|
1168
|
+
{
|
|
1169
|
+
for property in node.properties().keys()
|
|
1170
|
+
{
|
|
1171
|
+
let Ok(indices) =
|
|
1172
|
+
self.get_vector_indices_for_property_impl(transaction, graph_name, label, property)
|
|
1173
|
+
else
|
|
1174
|
+
{
|
|
1175
|
+
continue;
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
for (index_name, dimension, _metric) in indices
|
|
1179
|
+
{
|
|
1180
|
+
if let Some(val) = node.properties().get(property)
|
|
1181
|
+
&& let Some(vector) = crate::store::vector_extract::extract_f32_vec(val)
|
|
1182
|
+
&& vector.len() == dimension
|
|
1183
|
+
{
|
|
1184
|
+
let tensor = crate::value::Tensor::from_f32_slice(&vector);
|
|
1185
|
+
let _ =
|
|
1186
|
+
self.upsert_vector_impl(transaction, graph_name, &index_name, node.key(), &tensor);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
Ok(())
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
fn delete_vectors_for_nodes_impl(
|
|
1196
|
+
&self,
|
|
1197
|
+
transaction: &mut Self::TransactionBox,
|
|
1198
|
+
graph_name: &str,
|
|
1199
|
+
node_ids: &[String],
|
|
1200
|
+
) -> Result<()>
|
|
1201
|
+
{
|
|
1202
|
+
if node_ids.is_empty()
|
|
1203
|
+
{
|
|
1204
|
+
return Ok(());
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
let indices: Vec<VectorIndexMetadata> = self
|
|
1208
|
+
.get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
|
|
1209
|
+
|
|
1210
|
+
for m in &indices
|
|
1211
|
+
{
|
|
1212
|
+
let table_name = vector_index_table_name(graph_name, &m.name);
|
|
1213
|
+
let client = transaction.get_client();
|
|
1214
|
+
|
|
1215
|
+
for node_id in node_ids
|
|
1216
|
+
{
|
|
1217
|
+
let node_uuid = uuid::Uuid::parse_str(&format_hex_to_uuid(node_id))
|
|
1218
|
+
.map_err(|_| StoreError::InvalidFormat(format!("invalid node_id: '{}'", node_id)))?;
|
|
1219
|
+
client.execute(
|
|
1220
|
+
&format!("DELETE FROM {} WHERE node_id = $1", table_name),
|
|
1221
|
+
&[&node_uuid],
|
|
1222
|
+
)?;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
Ok(())
|
|
1227
|
+
}
|
|
502
1228
|
}
|
|
503
1229
|
|
|
504
1230
|
impl super::sqlbase::SqlQueries for Store
|
|
@@ -559,7 +1285,7 @@ impl super::sqlbase::SqlQueries for Store
|
|
|
559
1285
|
}
|
|
560
1286
|
|
|
561
1287
|
fn node_delete_query(graph_name: impl AsRef<str>, keys: impl AsRef<Vec<String>>)
|
|
562
|
-
|
|
1288
|
+
-> Result<String>
|
|
563
1289
|
{
|
|
564
1290
|
Ok(
|
|
565
1291
|
templates::NodeDelete {
|
|
@@ -608,7 +1334,7 @@ impl super::sqlbase::SqlQueries for Store
|
|
|
608
1334
|
)
|
|
609
1335
|
}
|
|
610
1336
|
fn edge_delete_query(graph_name: impl AsRef<str>, keys: impl AsRef<Vec<String>>)
|
|
611
|
-
|
|
1337
|
+
-> Result<String>
|
|
612
1338
|
{
|
|
613
1339
|
Ok(
|
|
614
1340
|
templates::EdgeDelete {
|