@an5/adapters 0.2.11 → 0.2.14
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 +137 -5
- package/dist/an5Adapter.d.ts +42 -1
- package/dist/an5Adapter.js +146 -38
- 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 +432 -0
- package/dotnet/An5Adapter.cs +99 -28
- 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 +1030 -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 +431 -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 +155 -31
- 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 +537 -0
- package/golang/sqlite/sqlite.go +0 -14
|
@@ -27,6 +27,15 @@ import {
|
|
|
27
27
|
type NBaseVectorClient,
|
|
28
28
|
} from './nbase';
|
|
29
29
|
import { bindParam, buildOrderBy, parseWhere, quote } from './base/sql';
|
|
30
|
+
import {
|
|
31
|
+
decodeVector,
|
|
32
|
+
isVectorField,
|
|
33
|
+
needsVectorEncoding,
|
|
34
|
+
runSqliteVectorSearch,
|
|
35
|
+
vectorDistance,
|
|
36
|
+
type SqliteVectorSupport,
|
|
37
|
+
type VectorStrategyPreference,
|
|
38
|
+
} from './sqlite/vector';
|
|
30
39
|
import {
|
|
31
40
|
createAdapterProxy,
|
|
32
41
|
getFieldsForModel,
|
|
@@ -72,7 +81,10 @@ function isDuplicateKeyError(error: unknown, dialect: Dialect): boolean {
|
|
|
72
81
|
function appendUpdateSet(sets: string[], params: Record<string, any>, col: string, val: any, dialect: Dialect): void {
|
|
73
82
|
const quoted = quote(col, dialect);
|
|
74
83
|
const safeCol = sanitizeParamName(col);
|
|
75
|
-
|
|
84
|
+
// `TypedArray.prototype.set` is the copy method, not the update operator, so a
|
|
85
|
+
// vector written as a float32 BLOB must not be read as `{ set: ... }`.
|
|
86
|
+
const isLiteral = Array.isArray(val) || ArrayBuffer.isView(val) || val instanceof ArrayBuffer;
|
|
87
|
+
if (val && typeof val === 'object' && !(val instanceof Date) && !isLiteral) {
|
|
76
88
|
if (val.increment !== undefined) {
|
|
77
89
|
const p = bindParam(params, `s_${safeCol}_inc`, val.increment);
|
|
78
90
|
sets.push(`${quoted} = ${quoted} + @${p}`);
|
|
@@ -386,6 +398,22 @@ export class An5Adapter {
|
|
|
386
398
|
return (await this.requireEngine()).exec<T>(query, params);
|
|
387
399
|
}
|
|
388
400
|
|
|
401
|
+
/**
|
|
402
|
+
* The SQLite engine's vector hooks, or null when this is not a SQLite adapter.
|
|
403
|
+
* `vectorSearch` needs them to load sqlite-vec and register the distance
|
|
404
|
+
* functions; a connection with no hooks still searches in memory.
|
|
405
|
+
*/
|
|
406
|
+
async sqliteVectorSupport(): Promise<SqliteVectorSupport | null> {
|
|
407
|
+
if (this.sheetsAdapter) return null;
|
|
408
|
+
const engine = await this.requireEngine();
|
|
409
|
+
return engine.dialect === 'sqlite' ? (engine.vectorSupport ?? null) : null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `vectorStrategy` from the config, so a project can pin one strategy. */
|
|
413
|
+
get vectorStrategy(): VectorStrategyPreference | undefined {
|
|
414
|
+
return this._engineConfig?.vectorStrategy ?? (this.engine as any)?.vectorSupport?.options?.vectorStrategy;
|
|
415
|
+
}
|
|
416
|
+
|
|
389
417
|
/** INTERNAL: used by AdapterTableClient for DML statements needing row count */
|
|
390
418
|
async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
391
419
|
return (await this.requireEngine()).executeRaw(query, params);
|
|
@@ -472,7 +500,7 @@ export class An5Adapter {
|
|
|
472
500
|
if (Array.isArray(fn)) return Promise.all(fn);
|
|
473
501
|
const engine = await this.requireEngine();
|
|
474
502
|
const handle = await engine.beginTransaction();
|
|
475
|
-
const txAdapter = new An5AdapterTx(handle, this.dialect);
|
|
503
|
+
const txAdapter = new An5AdapterTx(handle, this.dialect, engine.vectorSupport ?? null, this.vectorStrategy);
|
|
476
504
|
try {
|
|
477
505
|
const result = await fn(txAdapter);
|
|
478
506
|
await handle.commit();
|
|
@@ -488,7 +516,7 @@ export class An5Adapter {
|
|
|
488
516
|
throw new Error('Interactive transactions are not supported by the Google Sheets adapter');
|
|
489
517
|
}
|
|
490
518
|
const engine = await this.requireEngine();
|
|
491
|
-
return new An5AdapterTx(await engine.beginTransaction(), this.dialect);
|
|
519
|
+
return new An5AdapterTx(await engine.beginTransaction(), this.dialect, engine.vectorSupport ?? null, this.vectorStrategy);
|
|
492
520
|
}
|
|
493
521
|
|
|
494
522
|
table<T = any>(modelName: string): AdapterTableClient<T> | SheetsTableClient<T> {
|
|
@@ -686,10 +714,16 @@ export class An5AdapterTx {
|
|
|
686
714
|
constructor(
|
|
687
715
|
private readonly handle: TransactionHandle,
|
|
688
716
|
public readonly dialect: Dialect,
|
|
717
|
+
private readonly parentVectorSupport: SqliteVectorSupport | null = null,
|
|
718
|
+
public readonly vectorStrategy?: VectorStrategyPreference | undefined,
|
|
689
719
|
) {
|
|
690
720
|
return createAdapterProxy(this, (name) => this.table(name));
|
|
691
721
|
}
|
|
692
722
|
|
|
723
|
+
async sqliteVectorSupport(): Promise<SqliteVectorSupport | null> {
|
|
724
|
+
return this.dialect === 'sqlite' ? this.parentVectorSupport : null;
|
|
725
|
+
}
|
|
726
|
+
|
|
693
727
|
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
694
728
|
this.ensureOpen();
|
|
695
729
|
return this.handle.exec<T>(query, params);
|
|
@@ -732,6 +766,14 @@ export class AdapterTableClient<T = any> {
|
|
|
732
766
|
public get dialect(): Dialect { return this.adapter.dialect; }
|
|
733
767
|
|
|
734
768
|
public get tableName(): string {
|
|
769
|
+
const t = this.mappedTableName;
|
|
770
|
+
if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
|
|
771
|
+
if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
|
|
772
|
+
return quote(t, this.dialect);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** The table name this model maps to, before any quoting is applied. */
|
|
776
|
+
private get mappedTableName(): string {
|
|
735
777
|
const name = this.modelName;
|
|
736
778
|
let t = name;
|
|
737
779
|
const modelToTable = getModelToTable();
|
|
@@ -744,9 +786,17 @@ export class AdapterTableClient<T = any> {
|
|
|
744
786
|
if (modelToTable[lower]) t = modelToTable[lower];
|
|
745
787
|
}
|
|
746
788
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
789
|
+
return t;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* The bare table name, for helpers that take a string literal. `@@map` values
|
|
794
|
+
* may be pre-quoted or schema-qualified, neither of which `pragma_table_info`
|
|
795
|
+
* accepts, so the last segment is unwrapped here.
|
|
796
|
+
*/
|
|
797
|
+
public get rawTableName(): string {
|
|
798
|
+
const last = this.mappedTableName.split('.').pop() ?? this.mappedTableName;
|
|
799
|
+
return last.replace(/^[\s]*[\[\]"`]+|[\[\]"`]+[\s]*$/g, '');
|
|
750
800
|
}
|
|
751
801
|
|
|
752
802
|
private get nolock(): string {
|
|
@@ -770,6 +820,53 @@ export class AdapterTableClient<T = any> {
|
|
|
770
820
|
return this.adapter._executeRaw(query, params);
|
|
771
821
|
}
|
|
772
822
|
|
|
823
|
+
/**
|
|
824
|
+
* Encode every `VECTOR(n)` value on its way into SQLite.
|
|
825
|
+
*
|
|
826
|
+
* The column holds a float32 BLOB, which is a third of the size of the JSON
|
|
827
|
+
* text it replaces and is what sqlite-vec reads. Values that already arrived
|
|
828
|
+
* as text or bytes are left alone, so rows written by an older version stay
|
|
829
|
+
* readable.
|
|
830
|
+
*/
|
|
831
|
+
private encodeVectorFields(data: Record<string, any>): Record<string, any> {
|
|
832
|
+
if (this.dialect !== 'sqlite') return data;
|
|
833
|
+
const fields = getFieldsForModel(this.modelName);
|
|
834
|
+
const vectorFields = Object.keys(fields).filter((name) => isVectorField(fields[name]));
|
|
835
|
+
if (vectorFields.length === 0) return data;
|
|
836
|
+
const { encodeVector } = require('./sqlite/vector');
|
|
837
|
+
const out = { ...data };
|
|
838
|
+
for (const name of vectorFields) {
|
|
839
|
+
const value = out[name];
|
|
840
|
+
if (needsVectorEncoding(value)) {
|
|
841
|
+
const parsed = Array.from(value as ArrayLike<number>);
|
|
842
|
+
if (parsed.length > 0) out[name] = encodeVector(parsed);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return out;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Decode `VECTOR(n)` columns back into `number[]` after a read. */
|
|
849
|
+
private decodeVectorFields<T>(rows: T[]): T[] {
|
|
850
|
+
if (this.dialect !== 'sqlite' || !Array.isArray(rows) || rows.length === 0) return rows;
|
|
851
|
+
const fields = getFieldsForModel(this.modelName);
|
|
852
|
+
const vectorFields = Object.keys(fields).filter((name) => isVectorField(fields[name]));
|
|
853
|
+
if (vectorFields.length === 0) return rows;
|
|
854
|
+
return rows.map((row: any) => {
|
|
855
|
+
if (!row || typeof row !== 'object') return row;
|
|
856
|
+
let changed = false;
|
|
857
|
+
const out: any = { ...row };
|
|
858
|
+
for (const name of vectorFields) {
|
|
859
|
+
if (!(name in out)) continue;
|
|
860
|
+
const decoded = decodeVector(out[name]);
|
|
861
|
+
if (decoded) {
|
|
862
|
+
out[name] = decoded;
|
|
863
|
+
changed = true;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return changed ? out : row;
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
773
870
|
async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
|
|
774
871
|
const params: Record<string, any> = {};
|
|
775
872
|
const whereCtx = this.whereContext();
|
|
@@ -825,7 +922,7 @@ export class AdapterTableClient<T = any> {
|
|
|
825
922
|
if (orderSql) query += ` ${orderSql}`;
|
|
826
923
|
}
|
|
827
924
|
|
|
828
|
-
const rows = await this.doExec<T>(query, params);
|
|
925
|
+
const rows = this.decodeVectorFields(await this.doExec<T>(query, params));
|
|
829
926
|
|
|
830
927
|
if (args?.include) {
|
|
831
928
|
await resolveIncludes(this.modelName, rows, args.include, this.adapter);
|
|
@@ -875,7 +972,7 @@ export class AdapterTableClient<T = any> {
|
|
|
875
972
|
const fields = getFieldsForModel(this.modelName);
|
|
876
973
|
const idFieldName = resolveIdField(fields);
|
|
877
974
|
|
|
878
|
-
const data: any =
|
|
975
|
+
const data: any = this.encodeVectorFields(rawData);
|
|
879
976
|
if (idFieldName) {
|
|
880
977
|
const fieldDef: any = fields[idFieldName];
|
|
881
978
|
const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
|
|
@@ -954,9 +1051,10 @@ export class AdapterTableClient<T = any> {
|
|
|
954
1051
|
|
|
955
1052
|
for (let r = 0; r < args.data.length; r++) {
|
|
956
1053
|
const row = args.data[r] as any;
|
|
1054
|
+
const encoded = this.encodeVectorFields(row);
|
|
957
1055
|
const vals = firstCols.map(col => {
|
|
958
1056
|
const p = `r${r}_${col}`;
|
|
959
|
-
params[p] =
|
|
1057
|
+
params[p] = encoded[col] ?? null;
|
|
960
1058
|
return `@${p}`;
|
|
961
1059
|
});
|
|
962
1060
|
rowPlaceholders.push(`(${vals.join(', ')})`);
|
|
@@ -988,13 +1086,14 @@ export class AdapterTableClient<T = any> {
|
|
|
988
1086
|
}
|
|
989
1087
|
}
|
|
990
1088
|
|
|
1089
|
+
const data = this.encodeVectorFields(rawData);
|
|
991
1090
|
const setCols = Object.keys(rawData).filter(k => rawData[k] !== undefined);
|
|
992
1091
|
if (setCols.length > 0) {
|
|
993
1092
|
const params: Record<string, any> = {};
|
|
994
1093
|
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
|
|
995
1094
|
const sets: string[] = [];
|
|
996
1095
|
for (const col of setCols) {
|
|
997
|
-
appendUpdateSet(sets, params, col,
|
|
1096
|
+
appendUpdateSet(sets, params, col, data[col], this.dialect);
|
|
998
1097
|
}
|
|
999
1098
|
if (sets.length > 0) {
|
|
1000
1099
|
const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
@@ -1050,11 +1149,12 @@ export class AdapterTableClient<T = any> {
|
|
|
1050
1149
|
async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
|
|
1051
1150
|
const params: Record<string, any> = {};
|
|
1052
1151
|
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
|
|
1152
|
+
const data = this.encodeVectorFields(args.data as Record<string, any>);
|
|
1053
1153
|
const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
|
|
1054
1154
|
const sets: string[] = [];
|
|
1055
1155
|
|
|
1056
1156
|
for (const col of setCols) {
|
|
1057
|
-
appendUpdateSet(sets, params, col,
|
|
1157
|
+
appendUpdateSet(sets, params, col, data[col], this.dialect);
|
|
1058
1158
|
}
|
|
1059
1159
|
|
|
1060
1160
|
if (sets.length === 0) return { count: 0 };
|
|
@@ -1368,6 +1468,7 @@ export class AdapterTableClient<T = any> {
|
|
|
1368
1468
|
vectorField?: string;
|
|
1369
1469
|
distanceMetric?: 'cosine' | 'euclidean' | 'dot';
|
|
1370
1470
|
vectorElementType?: 'float32' | 'float16' | 'uint8';
|
|
1471
|
+
vectorStrategy?: VectorStrategyPreference;
|
|
1371
1472
|
}): Promise<(T & { distance: number })[]> {
|
|
1372
1473
|
const vectorField = args.vectorField || 'embedding';
|
|
1373
1474
|
const METRICS = ['cosine', 'euclidean', 'dot'];
|
|
@@ -1410,6 +1511,28 @@ export class AdapterTableClient<T = any> {
|
|
|
1410
1511
|
const msg = String(err?.message || '').toLowerCase();
|
|
1411
1512
|
if (!msg.includes('vector') && !msg.includes('operator does not exist')) throw err;
|
|
1412
1513
|
}
|
|
1514
|
+
} else if (this.dialect === 'sqlite') {
|
|
1515
|
+
// SQLite has no vector type, so the ranking runs through sqlite-vec, a
|
|
1516
|
+
// driver function, or json_each — whichever this connection supports.
|
|
1517
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
1518
|
+
const support = await (this.adapter as any).sqliteVectorSupport?.() ?? null;
|
|
1519
|
+
const ranked = await runSqliteVectorSearch({
|
|
1520
|
+
exec: (sql, p) => this.doExec<any>(sql, p),
|
|
1521
|
+
vectorSupport: support,
|
|
1522
|
+
table: this.tableName,
|
|
1523
|
+
rawTable: this.rawTableName,
|
|
1524
|
+
column: col,
|
|
1525
|
+
rawColumn: vectorField,
|
|
1526
|
+
vector: args.vector,
|
|
1527
|
+
metric,
|
|
1528
|
+
take,
|
|
1529
|
+
tail: whereSql ? ` WHERE ${whereSql}` : '',
|
|
1530
|
+
params,
|
|
1531
|
+
preference: args.vectorStrategy ?? (this.adapter as any).vectorStrategy ?? support?.options?.vectorStrategy,
|
|
1532
|
+
});
|
|
1533
|
+
if (ranked) return this.decodeVectorFields(ranked.rows) as (T & { distance: number })[];
|
|
1534
|
+
// No in-database strategy: score the column in the client instead.
|
|
1535
|
+
return this.scoreVectorsInMemory(args, metric, take, vectorField);
|
|
1413
1536
|
} else if (this.dialect === 'mssql') {
|
|
1414
1537
|
const distFn = metric === 'cosine' ? 'cosine' : metric === 'euclidean' ? 'euclidean' : 'dot';
|
|
1415
1538
|
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
@@ -1436,30 +1559,31 @@ export class AdapterTableClient<T = any> {
|
|
|
1436
1559
|
}
|
|
1437
1560
|
|
|
1438
1561
|
// 2. Fallback: in-memory similarity computation when native vector support is unavailable
|
|
1562
|
+
return this.scoreVectorsInMemory(args, metric, take, vectorField);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Rank the vector column in the client. This is the last resort for every
|
|
1567
|
+
* provider, and the only option on SQLite connections that offer neither
|
|
1568
|
+
* sqlite-vec, a driver function nor JSON1.
|
|
1569
|
+
*
|
|
1570
|
+
* The whole table is loaded, so it is only reasonable for a table small
|
|
1571
|
+
* enough to fit in memory.
|
|
1572
|
+
*/
|
|
1573
|
+
private async scoreVectorsInMemory(
|
|
1574
|
+
args: { vector: number[]; where?: any },
|
|
1575
|
+
metric: 'cosine' | 'euclidean' | 'dot',
|
|
1576
|
+
take: number,
|
|
1577
|
+
vectorField = 'embedding',
|
|
1578
|
+
): Promise<(T & { distance: number })[]> {
|
|
1439
1579
|
const rows = await this.findMany({ where: args.where });
|
|
1440
1580
|
|
|
1441
1581
|
const scored: { row: T; dist: number }[] = [];
|
|
1442
1582
|
for (const row of rows) {
|
|
1443
|
-
const
|
|
1444
|
-
if (!
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
|
|
1448
|
-
|
|
1449
|
-
let dot = 0, m1 = 0, m2 = 0;
|
|
1450
|
-
for (let i = 0; i < args.vector.length; i++) {
|
|
1451
|
-
const av = args.vector[i] ?? 0;
|
|
1452
|
-
const bv = vec[i] ?? 0;
|
|
1453
|
-
dot += av * bv;
|
|
1454
|
-
m1 += av ** 2;
|
|
1455
|
-
m2 += bv ** 2;
|
|
1456
|
-
}
|
|
1457
|
-
const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
|
|
1458
|
-
const dist = metric === 'cosine'
|
|
1459
|
-
? 1 - cosine
|
|
1460
|
-
: metric === 'dot'
|
|
1461
|
-
? -dot
|
|
1462
|
-
: Math.sqrt(args.vector.reduce((s, v, i) => s + (v - (vec[i] ?? 0)) ** 2, 0));
|
|
1583
|
+
const vec = decodeVector((row as any)[vectorField]);
|
|
1584
|
+
if (!vec) continue;
|
|
1585
|
+
const dist = vectorDistance(args.vector, vec, metric);
|
|
1586
|
+
if (dist === null) continue;
|
|
1463
1587
|
scored.push({ row, dist });
|
|
1464
1588
|
}
|
|
1465
1589
|
scored.sort((a, b) => a.dist - b.dist);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SqliteVectorSupport, VectorStrategyPreference } from '../sqlite/vector';
|
|
2
|
+
|
|
1
3
|
export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
|
|
2
4
|
|
|
3
5
|
export interface An5AdapterConfig {
|
|
@@ -8,6 +10,17 @@ export interface An5AdapterConfig {
|
|
|
8
10
|
* run in NBase while the rows stay in `connectionString`'s database.
|
|
9
11
|
*/
|
|
10
12
|
nbase?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Path to sqlite-vec or the AN5 native vector extension. On SQLite it is loaded before the
|
|
15
|
+
* vector capabilities are probed, so `vectorSearch` ranks inside the database
|
|
16
|
+
* instead of loading the table into memory. Other providers ignore it.
|
|
17
|
+
*/
|
|
18
|
+
sqliteVec?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Pin the SQLite vector search strategy. `auto` (the default) probes for
|
|
21
|
+
* sqlite-vec, a driver function and JSON1, and uses the first that works.
|
|
22
|
+
*/
|
|
23
|
+
vectorStrategy?: VectorStrategyPreference;
|
|
11
24
|
engine?: QueryEngine;
|
|
12
25
|
db?: any;
|
|
13
26
|
driver?: any;
|
|
@@ -30,4 +43,6 @@ export interface QueryEngine {
|
|
|
30
43
|
connect(): Promise<void>;
|
|
31
44
|
disconnect(): Promise<void>;
|
|
32
45
|
beginTransaction(): Promise<TransactionHandle>;
|
|
46
|
+
/** SQLite only: extension loading and distance-function registration. */
|
|
47
|
+
vectorSupport?: SqliteVectorSupport;
|
|
33
48
|
}
|
package/typescript/src/index.ts
CHANGED
|
@@ -29,6 +29,19 @@ export type {
|
|
|
29
29
|
AdapterMetadata,
|
|
30
30
|
Dialect,
|
|
31
31
|
} from './an5Adapter';
|
|
32
|
+
export {
|
|
33
|
+
encodeVector,
|
|
34
|
+
decodeVector,
|
|
35
|
+
vectorDistance,
|
|
36
|
+
isVectorField,
|
|
37
|
+
AN5_VECTOR_FUNCTIONS,
|
|
38
|
+
SQLITE_VEC_FUNCTIONS,
|
|
39
|
+
} from './sqlite/vector';
|
|
40
|
+
export type {
|
|
41
|
+
SqliteVectorStrategy,
|
|
42
|
+
VectorMetric,
|
|
43
|
+
VectorStrategyPreference,
|
|
44
|
+
} from './sqlite/vector';
|
|
32
45
|
export type { TypedAn5Adapter, AdapterAPI } from './typed';
|
|
33
46
|
export {
|
|
34
47
|
getLlmConfig, setLlmConfig,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Dialect, QueryEngine, TransactionHandle } from '../base/types';
|
|
2
|
+
import { SqliteVectorSupport } from './vector';
|
|
2
3
|
|
|
3
4
|
// ─── Browser SQLite Driver & Config ──────────────────────────────────────────────────
|
|
4
5
|
|
|
@@ -13,18 +14,41 @@ export interface SqliteBrowserConfig {
|
|
|
13
14
|
driver?: SqliteDriver;
|
|
14
15
|
exec?: <T = any>(query: string, params?: Record<string, any>) => Promise<T[]> | T[];
|
|
15
16
|
executeRaw?: (query: string, params?: Record<string, any>) => Promise<number> | number;
|
|
17
|
+
/** Path to the sqlite-vec extension, for WASM builds that allow loading one. */
|
|
18
|
+
sqliteVec?: string;
|
|
19
|
+
/** Pin the vector search strategy instead of probing for one. */
|
|
20
|
+
vectorStrategy?: 'auto' | 'sqlite-vec' | 'udf' | 'sql' | 'memory';
|
|
21
|
+
/** Register a scalar function with the driver, when its API allows it. */
|
|
22
|
+
registerFunction?: (name: string, fn: (...args: any[]) => any) => void;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
// ─── Browser SQLite Engine ──────────────────────────────────────────────────────────
|
|
19
26
|
|
|
20
27
|
export class SqliteBrowserEngine implements QueryEngine {
|
|
21
28
|
dialect: Dialect = 'sqlite';
|
|
29
|
+
/**
|
|
30
|
+
* A WASM build cannot register a JS callback as an SQL function, so this only
|
|
31
|
+
* reaches the `sql` strategy (json_each in plain SQL) and the in-memory one.
|
|
32
|
+
* A custom `registerFunction` hook unlocks `udf` as well.
|
|
33
|
+
*/
|
|
34
|
+
readonly vectorSupport: SqliteVectorSupport;
|
|
22
35
|
private db: any = null;
|
|
23
36
|
private driver: SqliteDriver | null = null;
|
|
24
37
|
private customExec: ((query: string, params?: Record<string, any>) => Promise<any[]> | any[]) | null = null;
|
|
25
38
|
private customExecuteRaw: ((query: string, params?: Record<string, any>) => Promise<number> | number) | null = null;
|
|
26
39
|
|
|
27
40
|
constructor(config?: SqliteBrowserConfig | any) {
|
|
41
|
+
this.vectorSupport = new SqliteVectorSupport({
|
|
42
|
+
native: () => this.db,
|
|
43
|
+
registerFunction: (name, fn) => {
|
|
44
|
+
const register = config?.registerFunction;
|
|
45
|
+
if (typeof register === 'function') return register(name, fn);
|
|
46
|
+
const db = this.db;
|
|
47
|
+
if (db && typeof db.create_function === 'function') return db.create_function(name, fn);
|
|
48
|
+
throw new Error('this SQLite build cannot register functions');
|
|
49
|
+
},
|
|
50
|
+
}, { sqliteVec: config?.sqliteVec, vectorStrategy: config?.vectorStrategy });
|
|
51
|
+
|
|
28
52
|
if (!config) return;
|
|
29
53
|
|
|
30
54
|
if (config.exec && typeof config.exec === 'function') {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from '../base/types';
|
|
2
|
+
import { SqliteVectorSupport } from './vector';
|
|
2
3
|
|
|
3
4
|
// ─── SQLite Engine ─────────────────────────────────────────────────────────────────
|
|
4
5
|
|
|
@@ -7,6 +8,8 @@ try { BetterSqlite3 = require('better-sqlite3'); } catch { }
|
|
|
7
8
|
|
|
8
9
|
export class SqliteEngine implements QueryEngine {
|
|
9
10
|
dialect: Dialect = 'sqlite';
|
|
11
|
+
/** Registers `an5_vec_*` and loads sqlite-vec for in-database vector search. */
|
|
12
|
+
readonly vectorSupport: SqliteVectorSupport;
|
|
10
13
|
private db: any = null;
|
|
11
14
|
private readonly filePath: string;
|
|
12
15
|
|
|
@@ -16,6 +19,15 @@ export class SqliteEngine implements QueryEngine {
|
|
|
16
19
|
.replace(/^sqlite:\/\/\//i, '/')
|
|
17
20
|
.replace(/^sqlite:\/\//i, '')
|
|
18
21
|
.replace(/^sqlite:/i, '');
|
|
22
|
+
this.vectorSupport = new SqliteVectorSupport({
|
|
23
|
+
native: () => this.getDb(),
|
|
24
|
+
registerFunction: (name, fn) => this.getDb().function(name, { deterministic: true }, fn),
|
|
25
|
+
}, { sqliteVec: adapterConfig.sqliteVec, vectorStrategy: adapterConfig.vectorStrategy });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The better-sqlite3 handle, needed for extension loading. */
|
|
29
|
+
native(): any {
|
|
30
|
+
return this.getDb();
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
private getDb(): any {
|
|
@@ -42,6 +54,7 @@ export class SqliteEngine implements QueryEngine {
|
|
|
42
54
|
|
|
43
55
|
async disconnect(): Promise<void> {
|
|
44
56
|
if (this.db) { this.db.close(); this.db = null; }
|
|
57
|
+
this.vectorSupport.reset();
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
async beginTransaction(): Promise<TransactionHandle> {
|