@an5/adapters 0.2.11 → 0.2.13
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.
- package/README.md +133 -2
- package/dist/an5Adapter.d.ts +37 -0
- package/dist/an5Adapter.js +138 -35
- package/dist/base/types.d.ts +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -1
- package/dist/sqlite/browserEngine.d.ts +13 -0
- package/dist/sqlite/browserEngine.js +13 -0
- package/dist/sqlite/engine.d.ts +5 -0
- package/dist/sqlite/engine.js +10 -0
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/sqlite/vector.d.ts +155 -0
- package/dist/sqlite/vector.js +436 -0
- package/dotnet/An5Adapter.cs +85 -27
- package/dotnet/Base/Core.cs +14 -0
- package/dotnet/Mssql/MssqlEngine.cs +44 -1
- package/dotnet/Postgres/PostgresEngine.cs +2 -0
- package/dotnet/Sqlite/SqliteEngine.cs +82 -2
- package/dotnet/Sqlite/SqliteVectors.cs +401 -0
- package/golang/an5_adapter.go +84 -32
- package/golang/base/vectors.go +263 -0
- package/golang/sqlite/vector.go +248 -0
- package/golang/sqlite_vector_test.go +288 -0
- package/golang/table_client.go +67 -3
- package/java/pom.xml +177 -0
- package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
- package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
- package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
- package/java/src/main/java/an5/adapters/An5Query.java +100 -0
- package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
- package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
- package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
- package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
- package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
- package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
- package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
- package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
- package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
- package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
- package/java/src/test/java/An5AdaptersSmoke.java +733 -0
- package/java/src/test/java/Json.java +174 -0
- package/java/src/test/java/QuerySemantics.java +111 -0
- package/kotlin/build.gradle.kts +145 -0
- package/kotlin/settings.gradle.kts +10 -0
- package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
- package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
- package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
- package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
- package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
- package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
- package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
- package/native/sqlite/CMakeLists.txt +12 -0
- package/native/sqlite/README.md +117 -0
- package/native/sqlite/an5_vector.c +209 -0
- package/package.json +33 -7
- package/python/an5_adapter.py +37 -6
- package/python/base/__init__.py +15 -0
- package/python/base/vectors.py +434 -0
- package/python/sqlite/__init__.py +2 -2
- package/python/sqlite/provider.py +21 -1
- package/python/table_client.py +129 -59
- package/rust/src/adapter.rs +342 -24
- package/rust/src/base/mod.rs +7 -0
- package/rust/src/base/vectors.rs +293 -0
- package/rust/src/lib.rs +3 -3
- package/scripts/sqlite-native-build.js +32 -0
- package/swift/Package.swift +22 -0
- package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
- package/swift/Sources/An5Adapters/Metadata.swift +183 -0
- package/swift/Sources/An5Adapters/Query.swift +105 -0
- package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
- package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
- package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
- package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
- package/swift/Sources/An5Adapters/TableClient.swift +671 -0
- package/swift/Sources/An5Adapters/Types.swift +127 -0
- package/swift/Sources/An5Adapters/Vectors.swift +86 -0
- package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
- package/swift/Sources/CSQLite/module.modulemap +8 -0
- package/swift/Sources/CSQLite/shim.h +9 -0
- package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
- package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
- package/typescript/src/an5Adapter.ts +146 -29
- package/typescript/src/base/types.ts +15 -0
- package/typescript/src/index.ts +13 -0
- package/typescript/src/sqlite/browserEngine.ts +24 -0
- package/typescript/src/sqlite/engine.ts +13 -0
- package/typescript/src/sqlite/index.ts +1 -1
- package/typescript/src/sqlite/vector.ts +541 -0
- package/golang/sqlite/sqlite.go +0 -14
package/rust/src/adapter.rs
CHANGED
|
@@ -10,8 +10,12 @@ use sqlx::{Any, AnyConnection, AnyPool, Row};
|
|
|
10
10
|
use std::collections::HashMap;
|
|
11
11
|
|
|
12
12
|
use crate::base::{
|
|
13
|
-
build_order_by, build_where,
|
|
14
|
-
|
|
13
|
+
build_order_by, build_where, decode_vector, encode_vector, get_fields_for_model, is_vector_field,
|
|
14
|
+
needs_vector_encoding, resolve_table, set_adapter_metadata, vector_distance, AdapterMetadata,
|
|
15
|
+
Dialect, RelationDef, Where,
|
|
16
|
+
};
|
|
17
|
+
use crate::base::vectors::{
|
|
18
|
+
distance_function, normalize_metric, plan_vector_strategies, STRATEGY_SQLITE_VEC, STRATEGY_UDF,
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
/// A database row as a plain JSON object.
|
|
@@ -213,6 +217,9 @@ pub struct An5Adapter {
|
|
|
213
217
|
pool: AnyPool,
|
|
214
218
|
dialect: Dialect,
|
|
215
219
|
connection_string: String,
|
|
220
|
+
/// SQLite only: pins one of `sqlite-vec`, `udf`, `sql` or `memory` instead of
|
|
221
|
+
/// probing for the fastest strategy available. Empty probes.
|
|
222
|
+
pub vector_strategy: Option<String>,
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
impl An5Adapter {
|
|
@@ -227,6 +234,7 @@ impl An5Adapter {
|
|
|
227
234
|
dialect: Dialect::detect(connection_string),
|
|
228
235
|
pool,
|
|
229
236
|
connection_string: connection_string.to_string(),
|
|
237
|
+
vector_strategy: None,
|
|
230
238
|
})
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -237,6 +245,7 @@ impl An5Adapter {
|
|
|
237
245
|
dialect: Dialect::detect(connection_string),
|
|
238
246
|
pool,
|
|
239
247
|
connection_string: connection_string.to_string(),
|
|
248
|
+
vector_strategy: None,
|
|
240
249
|
}
|
|
241
250
|
}
|
|
242
251
|
|
|
@@ -307,6 +316,45 @@ impl An5Adapter {
|
|
|
307
316
|
rows_to_maps(&rows)
|
|
308
317
|
}
|
|
309
318
|
|
|
319
|
+
/// What this SQLite connection can do for vector search, probed with cheap
|
|
320
|
+
/// `SELECT`s.
|
|
321
|
+
///
|
|
322
|
+
/// `sqlx` connects through a pooled `AnyPool` and exposes no way to register a
|
|
323
|
+
/// SQL function or load an extension, so a stock adapter normally reports only
|
|
324
|
+
/// JSON1. That is enough to rank with `json_each`; the sqlite-vec and UDF
|
|
325
|
+
/// strategies need a driver built for them.
|
|
326
|
+
pub async fn sqlite_capabilities(&self) -> HashMap<&'static str, bool> {
|
|
327
|
+
let mut capabilities = HashMap::from([
|
|
328
|
+
("vec", self.probe("SELECT vec_version()").await),
|
|
329
|
+
("udf", self.probe("SELECT an5_vec_cosine(zeroblob(4), zeroblob(4))").await),
|
|
330
|
+
("json1", self.probe("SELECT json_valid('[1]')").await),
|
|
331
|
+
]);
|
|
332
|
+
// The keys are static strings, so the map can be handed out directly.
|
|
333
|
+
capabilities.shrink_to_fit();
|
|
334
|
+
capabilities
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// A vector column's declared DDL type, which is what decides whether the
|
|
338
|
+
/// `json_each` strategy can reach the rows in it.
|
|
339
|
+
pub async fn sqlite_column_type(&self, table: &str, column: &str) -> Option<String> {
|
|
340
|
+
let rows = self
|
|
341
|
+
.query_raw(
|
|
342
|
+
"SELECT type FROM pragma_table_info(?) WHERE name = ?",
|
|
343
|
+
&[Value::from(table), Value::from(column)],
|
|
344
|
+
)
|
|
345
|
+
.await
|
|
346
|
+
.ok()?;
|
|
347
|
+
rows.first()
|
|
348
|
+
.and_then(|row| row.get("type"))
|
|
349
|
+
.and_then(Value::as_str)
|
|
350
|
+
.map(str::to_string)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// True when a probe statement ran, whatever it returned.
|
|
354
|
+
async fn probe(&self, sql: &str) -> bool {
|
|
355
|
+
self.query_raw(sql, &[]).await.is_ok()
|
|
356
|
+
}
|
|
357
|
+
|
|
310
358
|
/// Execute a non-SELECT statement and return the affected row count.
|
|
311
359
|
pub async fn execute_raw(&self, sql: &str, args: &[Value]) -> Result<u64> {
|
|
312
360
|
let mut q = sqlx::query(sql);
|
|
@@ -360,6 +408,9 @@ fn bind_value<'q>(
|
|
|
360
408
|
}
|
|
361
409
|
}
|
|
362
410
|
Value::String(s) => q.bind(s.clone()),
|
|
411
|
+
// A vector is bound as the float32 BLOB a `VECTOR(n)` column stores; any
|
|
412
|
+
// other structure has no SQLite representation.
|
|
413
|
+
Value::Object(_) if blob_bytes(v).is_some() => q.bind(blob_bytes(v)),
|
|
363
414
|
other => q.bind(other.to_string()),
|
|
364
415
|
}
|
|
365
416
|
}
|
|
@@ -384,15 +435,55 @@ fn column_to_value(row: &sqlx::any::AnyRow, idx: usize) -> Result<Value> {
|
|
|
384
435
|
probe!(i64, Value::from);
|
|
385
436
|
probe!(f64, Value::from);
|
|
386
437
|
probe!(String, Value::String);
|
|
387
|
-
probe!(Vec<u8>, |b: Vec<u8>|
|
|
388
|
-
Value::String(String::from_utf8_lossy(&b).into_owned())
|
|
389
|
-
});
|
|
438
|
+
probe!(Vec<u8>, |b: Vec<u8>| bytes_to_value(&b));
|
|
390
439
|
|
|
391
440
|
// An undecodable column (driver-specific type) becomes null rather than
|
|
392
441
|
// failing the whole query.
|
|
393
442
|
Ok(Value::Null)
|
|
394
443
|
}
|
|
395
444
|
|
|
445
|
+
/// The member name marking a value as raw bytes rather than text.
|
|
446
|
+
const BLOB_KEY: &str = "__an5Blob";
|
|
447
|
+
|
|
448
|
+
/// Prefix kept for rows written before the marker object, so they still read back.
|
|
449
|
+
const BLOB_PREFIX: &str = "\u{0}an5:blob:";
|
|
450
|
+
|
|
451
|
+
/// Carries bytes through the JSON row model.
|
|
452
|
+
///
|
|
453
|
+
/// SQLite has no vector type, so a `VECTOR(n)` column is a BLOB of float32 values,
|
|
454
|
+
/// and the row model is a JSON object with no byte type. A value from `blob_value`
|
|
455
|
+
/// is bound and read as raw bytes; text columns stay plain strings.
|
|
456
|
+
pub fn blob_value(bytes: &[u8]) -> Value {
|
|
457
|
+
let mut map = RowMap::new();
|
|
458
|
+
let mut hex = String::with_capacity(bytes.len() * 2);
|
|
459
|
+
for byte in bytes {
|
|
460
|
+
hex.push_str(&format!("{:02x}", byte));
|
|
461
|
+
}
|
|
462
|
+
map.insert(BLOB_KEY.to_string(), Value::String(hex));
|
|
463
|
+
Value::Object(map)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// Recovers the bytes behind a value from [`blob_value`].
|
|
467
|
+
pub fn blob_bytes(value: &Value) -> Option<Vec<u8>> {
|
|
468
|
+
let hex = match value {
|
|
469
|
+
Value::Object(map) => map.get(BLOB_KEY)?.as_str()?.to_string(),
|
|
470
|
+
Value::String(text) => text.strip_prefix(BLOB_PREFIX)?.to_string(),
|
|
471
|
+
_ => return None,
|
|
472
|
+
};
|
|
473
|
+
if hex.len() % 2 != 0 {
|
|
474
|
+
return None;
|
|
475
|
+
}
|
|
476
|
+
(0..hex.len())
|
|
477
|
+
.step_by(2)
|
|
478
|
+
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
|
|
479
|
+
.collect()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/// A text BLOB stays text; anything binary becomes a [`blob_value`].
|
|
483
|
+
fn bytes_to_value(bytes: &[u8]) -> Value {
|
|
484
|
+
blob_value(bytes)
|
|
485
|
+
}
|
|
486
|
+
|
|
396
487
|
fn rows_to_maps(rows: &[sqlx::any::AnyRow]) -> Result<Vec<RowMap>> {
|
|
397
488
|
let mut out = Vec::with_capacity(rows.len());
|
|
398
489
|
for row in rows {
|
|
@@ -525,7 +616,7 @@ impl TableClient {
|
|
|
525
616
|
if let Some(inc) = &args.include {
|
|
526
617
|
self.resolve_includes(&mut rows, inc).await?;
|
|
527
618
|
}
|
|
528
|
-
Ok(rows)
|
|
619
|
+
Ok(self.decode_vector_rows(rows))
|
|
529
620
|
}
|
|
530
621
|
|
|
531
622
|
/// First row matching the args, or `None`.
|
|
@@ -571,7 +662,7 @@ impl TableClient {
|
|
|
571
662
|
}
|
|
572
663
|
let mut data = data.clone();
|
|
573
664
|
apply_generated_primary_key(&self.table_name, &mut data);
|
|
574
|
-
let data = &data;
|
|
665
|
+
let data = &self.encode_vector_fields(&data);
|
|
575
666
|
let (scalars, _relations) = split_relation_writes(&self.table_name, data);
|
|
576
667
|
|
|
577
668
|
let mut cols: Vec<String> = Vec::new();
|
|
@@ -596,7 +687,14 @@ impl TableClient {
|
|
|
596
687
|
);
|
|
597
688
|
|
|
598
689
|
match self.adapter.query_raw(&sql, &args).await {
|
|
599
|
-
Ok(rows) if !rows.is_empty() =>
|
|
690
|
+
Ok(rows) if !rows.is_empty() => {
|
|
691
|
+
// The inserted row comes back with the vector still in its stored form,
|
|
692
|
+
// so it needs the same decoding a `find` would apply. Without it the
|
|
693
|
+
// caller receives the internal blob envelope where the vector should be.
|
|
694
|
+
Ok(self
|
|
695
|
+
.decode_vector_rows(vec![rows.into_iter().next().unwrap()])
|
|
696
|
+
.remove(0))
|
|
697
|
+
}
|
|
600
698
|
// The engine rejected the returning clause; insert without it.
|
|
601
699
|
_ => {
|
|
602
700
|
let sql = format!(
|
|
@@ -814,6 +912,198 @@ impl TableClient {
|
|
|
814
912
|
self.adapter.query_raw(&sql, &w.args).await
|
|
815
913
|
}
|
|
816
914
|
|
|
915
|
+
/// The `VECTOR(n)` columns of this model, from the generated metadata.
|
|
916
|
+
fn vector_fields(&self) -> Vec<String> {
|
|
917
|
+
match get_fields_for_model(&self.table_name) {
|
|
918
|
+
Some(Value::Object(fields)) => fields
|
|
919
|
+
.iter()
|
|
920
|
+
.filter(|(_, definition)| is_vector_field(definition))
|
|
921
|
+
.map(|(name, _)| name.clone())
|
|
922
|
+
.collect(),
|
|
923
|
+
_ => Vec::new(),
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/// Encodes every `VECTOR(n)` value on its way into SQLite.
|
|
928
|
+
///
|
|
929
|
+
/// The column holds a float32 BLOB, which is a third of the size of the JSON
|
|
930
|
+
/// text it replaces. Values that already arrived as text or bytes are left
|
|
931
|
+
/// alone, so rows written by an older version stay readable.
|
|
932
|
+
fn encode_vector_fields(&self, data: &RowMap) -> RowMap {
|
|
933
|
+
if self.adapter.dialect != Dialect::Sqlite {
|
|
934
|
+
return data.clone();
|
|
935
|
+
}
|
|
936
|
+
let names = self.vector_fields();
|
|
937
|
+
if names.is_empty() {
|
|
938
|
+
return data.clone();
|
|
939
|
+
}
|
|
940
|
+
let mut out = data.clone();
|
|
941
|
+
for name in names {
|
|
942
|
+
if let Some(value) = out.get(&name) {
|
|
943
|
+
if needs_vector_encoding(value) {
|
|
944
|
+
if let Some(values) = decode_vector(value) {
|
|
945
|
+
out.insert(name, blob_value(&encode_vector(&values)));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
out
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/// Decodes `VECTOR(n)` columns back into arrays of numbers after a read.
|
|
954
|
+
fn decode_vector_rows(&self, rows: Vec<RowMap>) -> Vec<RowMap> {
|
|
955
|
+
if self.adapter.dialect != Dialect::Sqlite || rows.is_empty() {
|
|
956
|
+
return rows;
|
|
957
|
+
}
|
|
958
|
+
let names = self.vector_fields();
|
|
959
|
+
if names.is_empty() {
|
|
960
|
+
return rows;
|
|
961
|
+
}
|
|
962
|
+
let mut out = rows;
|
|
963
|
+
for row in &mut out {
|
|
964
|
+
for name in &names {
|
|
965
|
+
if let Some(value) = row.get(name) {
|
|
966
|
+
if let Some(values) = decode_vector(value) {
|
|
967
|
+
row.insert(name.clone(), Value::from(values));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
out
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/// SQLite ranks a `VECTOR(n)` column inside the database.
|
|
976
|
+
///
|
|
977
|
+
/// Returns `None` when the connection offers no strategy, so the caller falls
|
|
978
|
+
/// back to scoring in Rust.
|
|
979
|
+
async fn sqlite_vector_search(
|
|
980
|
+
&self,
|
|
981
|
+
args: &VectorSearchArgs,
|
|
982
|
+
field: &str,
|
|
983
|
+
metric: &str,
|
|
984
|
+
take: i64,
|
|
985
|
+
w: &Where,
|
|
986
|
+
) -> Result<Option<Vec<RowMap>>> {
|
|
987
|
+
if args.vector.is_empty() {
|
|
988
|
+
return Ok(Some(Vec::new()));
|
|
989
|
+
}
|
|
990
|
+
let metric = normalize_metric(metric);
|
|
991
|
+
let column = self.adapter.dialect.quote_identifier(field);
|
|
992
|
+
let table = self.quoted_table();
|
|
993
|
+
|
|
994
|
+
let capabilities = self.adapter.sqlite_capabilities().await;
|
|
995
|
+
let declared = self
|
|
996
|
+
.adapter
|
|
997
|
+
.sqlite_column_type(&self.physical_table, field)
|
|
998
|
+
.await;
|
|
999
|
+
let tail = if w.is_empty() {
|
|
1000
|
+
String::new()
|
|
1001
|
+
} else {
|
|
1002
|
+
format!(" WHERE {}", w.clause)
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
for strategy in plan_vector_strategies(
|
|
1006
|
+
&capabilities,
|
|
1007
|
+
declared.as_deref(),
|
|
1008
|
+
self.adapter.vector_strategy.as_deref(),
|
|
1009
|
+
) {
|
|
1010
|
+
if strategy == crate::base::STRATEGY_SQL {
|
|
1011
|
+
let clause = if tail.is_empty() { " WHERE ".to_string() } else { format!("{tail} AND ") };
|
|
1012
|
+
let binary = self.adapter.query_raw(&format!("SELECT 1 FROM {table}{clause}typeof({column}) = 'blob' LIMIT 1"), &w.args).await?;
|
|
1013
|
+
if !binary.is_empty() { continue; }
|
|
1014
|
+
}
|
|
1015
|
+
let mut bind: Vec<Value> = Vec::new();
|
|
1016
|
+
let sql = match strategy {
|
|
1017
|
+
STRATEGY_SQLITE_VEC | STRATEGY_UDF => {
|
|
1018
|
+
let Some(function) = distance_function(strategy, metric) else {
|
|
1019
|
+
continue;
|
|
1020
|
+
};
|
|
1021
|
+
bind.push(blob_value(&encode_vector(&args.vector)));
|
|
1022
|
+
let guard = match strategy {
|
|
1023
|
+
// sqlite-vec only understands float32 BLOB operands, so rows
|
|
1024
|
+
// stored any other way are excluded rather than aborting the
|
|
1025
|
+
// query.
|
|
1026
|
+
STRATEGY_SQLITE_VEC => format!("{column} IS NOT NULL"),
|
|
1027
|
+
_ => String::new(),
|
|
1028
|
+
};
|
|
1029
|
+
let distance = if strategy == STRATEGY_SQLITE_VEC {
|
|
1030
|
+
format!("CASE WHEN vec_length(vec_f32({column})) = {} THEN {function}(vec_f32({column}), ?) END", args.vector.len())
|
|
1031
|
+
} else { format!("{function}({column}, ?)") };
|
|
1032
|
+
// The guard and the caller's WHERE are one clause: appending the
|
|
1033
|
+
// tail after it would produce a second WHERE.
|
|
1034
|
+
let clause = match (guard.is_empty(), tail.is_empty()) {
|
|
1035
|
+
(true, _) => tail.clone(),
|
|
1036
|
+
(false, true) => format!(" WHERE {guard}"),
|
|
1037
|
+
(false, false) => {
|
|
1038
|
+
format!(" WHERE {guard} AND{}", tail.replacen(" WHERE ", " ", 1))
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
format!("SELECT *, {distance} AS distance FROM {table}{clause}")
|
|
1042
|
+
}
|
|
1043
|
+
_ => {
|
|
1044
|
+
// json_each reads the query vector as JSON text.
|
|
1045
|
+
bind.push(Value::from(
|
|
1046
|
+
serde_json::to_string(&args.vector).unwrap_or_else(|_| "[]".to_string()),
|
|
1047
|
+
));
|
|
1048
|
+
format!(
|
|
1049
|
+
"WITH q AS (SELECT je.key AS k, CAST(je.value AS REAL) AS v FROM json_each(?) je) SELECT *, {} AS distance FROM {table}{tail}",
|
|
1050
|
+
json_distance_expr(metric, &column)
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// A row whose stored vector cannot be scored yields a NULL distance;
|
|
1056
|
+
// those rows are dropped rather than handed to the caller.
|
|
1057
|
+
let sql = format!(
|
|
1058
|
+
"SELECT * FROM ({sql}) AS an5_ranked WHERE distance IS NOT NULL ORDER BY distance ASC LIMIT {take}"
|
|
1059
|
+
);
|
|
1060
|
+
bind.extend(w.args.clone());
|
|
1061
|
+
if let Ok(rows) = self.adapter.query_raw(&sql, &bind).await {
|
|
1062
|
+
return Ok(Some(self.decode_vector_rows(rows)));
|
|
1063
|
+
}
|
|
1064
|
+
// The connection advertised the capability but the query failed, e.g.
|
|
1065
|
+
// an extension that did not really load. The next strategy is cheaper
|
|
1066
|
+
// than reporting the failure.
|
|
1067
|
+
}
|
|
1068
|
+
Ok(None)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/// Ranks the vector column in Rust.
|
|
1072
|
+
///
|
|
1073
|
+
/// The last resort for every provider, and the only option on a SQLite
|
|
1074
|
+
/// connection that offers no in-database strategy. The whole table is loaded,
|
|
1075
|
+
/// so it is only reasonable for a table small enough to fit in memory.
|
|
1076
|
+
async fn score_vectors_in_memory(
|
|
1077
|
+
&self,
|
|
1078
|
+
args: &VectorSearchArgs,
|
|
1079
|
+
field: &str,
|
|
1080
|
+
metric: &str,
|
|
1081
|
+
take: i64,
|
|
1082
|
+
) -> Result<Vec<RowMap>> {
|
|
1083
|
+
let rows = self
|
|
1084
|
+
.find_many(&FindManyArgs {
|
|
1085
|
+
r#where: args.r#where.clone(),
|
|
1086
|
+
..Default::default()
|
|
1087
|
+
})
|
|
1088
|
+
.await?;
|
|
1089
|
+
|
|
1090
|
+
let mut scored: Vec<(RowMap, f64)> = Vec::new();
|
|
1091
|
+
for mut row in rows {
|
|
1092
|
+
let Some(row_vec) = row.get(field).and_then(decode_vector) else {
|
|
1093
|
+
continue;
|
|
1094
|
+
};
|
|
1095
|
+
if row_vec.len() != args.vector.len() {
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
let distance = vector_distance(&args.vector, &row_vec, metric);
|
|
1099
|
+
row.insert("distance".to_string(), Value::from(distance));
|
|
1100
|
+
scored.push((row, distance));
|
|
1101
|
+
}
|
|
1102
|
+
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
1103
|
+
scored.truncate(take.max(0) as usize);
|
|
1104
|
+
Ok(scored.into_iter().map(|(row, _)| row).collect())
|
|
1105
|
+
}
|
|
1106
|
+
|
|
817
1107
|
/// Vector similarity search.
|
|
818
1108
|
///
|
|
819
1109
|
/// Tries the database's native distance function first and falls back to
|
|
@@ -859,6 +1149,20 @@ impl TableClient {
|
|
|
859
1149
|
Dialect::Sqlite => None,
|
|
860
1150
|
};
|
|
861
1151
|
|
|
1152
|
+
// SQLite has no vector operator, so the ranking is built from the
|
|
1153
|
+
// strategies this connection supports: sqlite-vec, a registered distance
|
|
1154
|
+
// function, or json_each in plain SQL. `sqlx` reaches none of the first two
|
|
1155
|
+
// through an `AnyPool`, so json_each is normally the one that runs.
|
|
1156
|
+
if self.adapter.dialect == Dialect::Sqlite {
|
|
1157
|
+
if let Some(rows) = self
|
|
1158
|
+
.sqlite_vector_search(args, field, metric, take, &w)
|
|
1159
|
+
.await?
|
|
1160
|
+
{
|
|
1161
|
+
return Ok(rows);
|
|
1162
|
+
}
|
|
1163
|
+
return self.score_vectors_in_memory(args, field, metric, take).await;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
862
1166
|
if let Some(sql) = native_sql {
|
|
863
1167
|
let mut bind: Vec<Value> = Vec::new();
|
|
864
1168
|
if !w.is_empty() {
|
|
@@ -888,23 +1192,13 @@ impl TableClient {
|
|
|
888
1192
|
|
|
889
1193
|
let mut scored: Vec<(RowMap, f64)> = Vec::new();
|
|
890
1194
|
for mut row in rows {
|
|
891
|
-
let
|
|
892
|
-
|
|
893
|
-
Some(Value::String(s)) => parse_vector(&s),
|
|
894
|
-
Some(Value::Array(items)) => Some(
|
|
895
|
-
items
|
|
896
|
-
.iter()
|
|
897
|
-
.filter_map(|v| v.as_f64())
|
|
898
|
-
.collect::<Vec<f64>>(),
|
|
899
|
-
),
|
|
900
|
-
_ => None,
|
|
1195
|
+
let Some(rv) = row.get(field).and_then(decode_vector) else {
|
|
1196
|
+
continue;
|
|
901
1197
|
};
|
|
902
|
-
if
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
scored.push((row, d));
|
|
907
|
-
}
|
|
1198
|
+
if rv.len() == args.vector.len() {
|
|
1199
|
+
let d = vector_distance(&args.vector, &rv, metric);
|
|
1200
|
+
row.insert("distance".to_string(), Value::from(d));
|
|
1201
|
+
scored.push((row, d));
|
|
908
1202
|
}
|
|
909
1203
|
}
|
|
910
1204
|
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
@@ -1185,6 +1479,30 @@ fn build_set_clause(data: &Value, ph: &dyn Fn(usize) -> String) -> Result<(Strin
|
|
|
1185
1479
|
}
|
|
1186
1480
|
|
|
1187
1481
|
/// Parses a JSON array string into a vector.
|
|
1482
|
+
/// The distance expression for the `json_each` strategy.
|
|
1483
|
+
///
|
|
1484
|
+
/// `json_valid` fails on a float32 BLOB, so the column is guarded before it is
|
|
1485
|
+
/// walked, and a row whose stored vector has another dimension yields NULL.
|
|
1486
|
+
fn json_distance_expr(metric: &str, column: &str) -> String {
|
|
1487
|
+
let row = format!(
|
|
1488
|
+
"CASE WHEN json_valid({column}) THEN {column} ELSE '[]' END"
|
|
1489
|
+
);
|
|
1490
|
+
let same_length =
|
|
1491
|
+
format!("(SELECT COUNT(*) FROM json_each({row})) = (SELECT COUNT(*) FROM q)");
|
|
1492
|
+
let terms = match normalize_metric(metric) {
|
|
1493
|
+
"euclidean" => format!(
|
|
1494
|
+
"sqrt((SELECT SUM((je.value - q.v) * (je.value - q.v)) FROM json_each({row}) je JOIN q ON q.k = je.key))"
|
|
1495
|
+
),
|
|
1496
|
+
"dot" => format!(
|
|
1497
|
+
"-(SELECT SUM(je.value * q.v) FROM json_each({row}) je JOIN q ON q.k = je.key)"
|
|
1498
|
+
),
|
|
1499
|
+
_ => format!(
|
|
1500
|
+
"1.0 - (SELECT SUM(je.value * q.v) FROM json_each({row}) je JOIN q ON q.k = je.key) / NULLIF(sqrt((SELECT SUM(q.v * q.v) FROM q)) * sqrt((SELECT SUM(je.value * je.value) FROM json_each({row}) je)), 0)"
|
|
1501
|
+
),
|
|
1502
|
+
};
|
|
1503
|
+
format!("CASE WHEN {same_length} THEN {terms} END")
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1188
1506
|
fn parse_vector(raw: &str) -> Option<Vec<f64>> {
|
|
1189
1507
|
let trimmed = raw.trim().trim_start_matches('[').trim_end_matches(']');
|
|
1190
1508
|
if trimmed.trim().is_empty() {
|
package/rust/src/base/mod.rs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
pub mod dialect;
|
|
7
7
|
pub mod metadata;
|
|
8
|
+
pub mod vectors;
|
|
8
9
|
pub mod where_builder;
|
|
9
10
|
|
|
10
11
|
pub use dialect::{
|
|
@@ -14,4 +15,10 @@ pub use metadata::{
|
|
|
14
15
|
add_table_override, get_fields_for_model, get_model_to_table, get_relations_for_model,
|
|
15
16
|
resolve_table, set_adapter_metadata, AdapterMetadata, RelationDef,
|
|
16
17
|
};
|
|
18
|
+
pub use vectors::{
|
|
19
|
+
decode_vector, decode_vector_bytes, distance_function, encode_vector, is_vector_field,
|
|
20
|
+
needs_vector_encoding, normalize_metric, parse_vector, plan_vector_strategies,
|
|
21
|
+
AN5_VECTOR_FUNCTIONS, SQLITE_VEC_FUNCTIONS, STRATEGY_MEMORY, STRATEGY_SQL, STRATEGY_SQLITE_VEC,
|
|
22
|
+
STRATEGY_UDF,
|
|
23
|
+
};
|
|
17
24
|
pub use where_builder::{build_order_by, build_where, SqlArg, Where};
|