@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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text.Json;
|
|
5
|
+
|
|
6
|
+
namespace An5Orm
|
|
7
|
+
{
|
|
8
|
+
// ─── SQLite vector search ────────────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// SQLite has no vector type, so a `VECTOR(n)` column stores a BLOB of
|
|
11
|
+
// little-endian float32 values (see `EncodeVector`) and is ranked in one of
|
|
12
|
+
// four ways, in this order:
|
|
13
|
+
//
|
|
14
|
+
// 1. sqlite-vec native scalar distances, when the extension loads.
|
|
15
|
+
// 2. udf an5_vec_cosine / an5_vec_l2 / an5_vec_ip registered with
|
|
16
|
+
// the connection. Reads the BLOB and the legacy JSON text.
|
|
17
|
+
// 3. sql json_each brute force in plain SQL. Needs no user
|
|
18
|
+
// function, but only reaches rows stored as JSON text.
|
|
19
|
+
// 4. memory the column is loaded and scored in C#.
|
|
20
|
+
//
|
|
21
|
+
// Strategies 1-3 rank inside the database and transfer only the matching
|
|
22
|
+
// rows, which is why they are preferred over the in-memory path.
|
|
23
|
+
|
|
24
|
+
public static class SqliteVectors
|
|
25
|
+
{
|
|
26
|
+
/// <summary>Scalar functions the adapter registers itself.</summary>
|
|
27
|
+
public static readonly Dictionary<string, string> An5Functions = new()
|
|
28
|
+
{
|
|
29
|
+
["cosine"] = "an5_vec_cosine",
|
|
30
|
+
["euclidean"] = "an5_vec_l2",
|
|
31
|
+
["dot"] = "an5_vec_ip",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/// <summary>Scalar functions shipped by the sqlite-vec extension.</summary>
|
|
35
|
+
public static readonly Dictionary<string, string> SqliteVecFunctions = new()
|
|
36
|
+
{
|
|
37
|
+
["cosine"] = "vec_distance_cosine",
|
|
38
|
+
["euclidean"] = "vec_distance_l2",
|
|
39
|
+
["dot"] = "vec_distance_ip",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
public const string StrategySqliteVec = "sqlite-vec";
|
|
43
|
+
public const string StrategyUdf = "udf";
|
|
44
|
+
public const string StrategySql = "sql";
|
|
45
|
+
public const string StrategyMemory = "memory";
|
|
46
|
+
|
|
47
|
+
private const int BytesPerFloat = 4;
|
|
48
|
+
|
|
49
|
+
public static string NormalizeMetric(string metric)
|
|
50
|
+
=> metric == "euclidean" || metric == "dot" ? metric : "cosine";
|
|
51
|
+
|
|
52
|
+
// ── Codec ───────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/// <summary>
|
|
55
|
+
/// Reads a numeric array out of the shapes a driver can hand back: a
|
|
56
|
+
/// list, a JSON text column, or raw float32 bytes.
|
|
57
|
+
/// </summary>
|
|
58
|
+
public static List<double> ParseVector(object value)
|
|
59
|
+
{
|
|
60
|
+
switch (value)
|
|
61
|
+
{
|
|
62
|
+
case null:
|
|
63
|
+
return null;
|
|
64
|
+
case List<double> list:
|
|
65
|
+
return list.Count > 0 ? list : null;
|
|
66
|
+
case double[] array:
|
|
67
|
+
return array.Length > 0 ? array.ToList() : null;
|
|
68
|
+
case float[] floats:
|
|
69
|
+
return floats.Length > 0 ? floats.Select(f => (double)f).ToList() : null;
|
|
70
|
+
case byte[] bytes:
|
|
71
|
+
return DecodeVector(bytes);
|
|
72
|
+
case string text:
|
|
73
|
+
var trimmed = text.Trim();
|
|
74
|
+
if (!trimmed.StartsWith("[")) return null;
|
|
75
|
+
try { return ParseVector(JsonSerializer.Deserialize<double[]>(trimmed)); }
|
|
76
|
+
catch { return null; }
|
|
77
|
+
case System.Collections.IEnumerable seq when value is not string:
|
|
78
|
+
try { return ParseVector(seq.Cast<object>().Select(o => Convert.ToDouble(o)).ToList()); }
|
|
79
|
+
catch { return null; }
|
|
80
|
+
default:
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// <summary>
|
|
86
|
+
/// Decodes a stored vector: a float32 BLOB, a legacy JSON text column,
|
|
87
|
+
/// or an array the caller already decoded.
|
|
88
|
+
/// </summary>
|
|
89
|
+
public static List<double> DecodeVector(object value)
|
|
90
|
+
{
|
|
91
|
+
var direct = value is byte[] ? null : ParseVector(value);
|
|
92
|
+
if (direct != null) return direct;
|
|
93
|
+
if (!(value is byte[] bytes) || bytes.Length == 0 || bytes.Length % BytesPerFloat != 0) return null;
|
|
94
|
+
var outVec = new List<double>(bytes.Length / BytesPerFloat);
|
|
95
|
+
for (int i = 0; i < bytes.Length; i += BytesPerFloat)
|
|
96
|
+
outVec.Add(BitConverter.ToSingle(bytes, i));
|
|
97
|
+
return outVec;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// <summary>Encodes a vector as the little-endian float32 BLOB the column stores.</summary>
|
|
101
|
+
public static byte[] EncodeVector(IReadOnlyList<double> values)
|
|
102
|
+
{
|
|
103
|
+
var bytes = new byte[values.Count * BytesPerFloat];
|
|
104
|
+
for (int i = 0; i < values.Count; i++)
|
|
105
|
+
{
|
|
106
|
+
// Little-endian, written through the integer view of the float:
|
|
107
|
+
// SQLite reads the bytes in memory order.
|
|
108
|
+
var bits = BitConverter.SingleToInt32Bits((float)values[i]);
|
|
109
|
+
bytes[i * BytesPerFloat] = (byte)(bits & 0xFF);
|
|
110
|
+
bytes[i * BytesPerFloat + 1] = (byte)((bits >> 8) & 0xFF);
|
|
111
|
+
bytes[i * BytesPerFloat + 2] = (byte)((bits >> 16) & 0xFF);
|
|
112
|
+
bytes[i * BytesPerFloat + 3] = (byte)((bits >> 24) & 0xFF);
|
|
113
|
+
}
|
|
114
|
+
return bytes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// <summary>True when the value still has to become bytes on its way into a column.</summary>
|
|
118
|
+
public static bool NeedsVectorEncoding(object value)
|
|
119
|
+
{
|
|
120
|
+
if (value == null || value is string || value is byte[]) return false;
|
|
121
|
+
if (value is System.Collections.IEnumerable seq) return !(value is System.Collections.IDictionary);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// <summary>True when generated metadata describes a `VECTOR(n)` column.</summary>
|
|
126
|
+
public static bool IsVectorField(object definition)
|
|
127
|
+
{
|
|
128
|
+
if (definition == null) return false;
|
|
129
|
+
if (definition is string text)
|
|
130
|
+
return text.Contains("[]") || text.TrimStart().StartsWith("VECTOR", StringComparison.OrdinalIgnoreCase);
|
|
131
|
+
|
|
132
|
+
// The metadata carries a small record per field.
|
|
133
|
+
var kind = ReadMember(definition, "kind") as string;
|
|
134
|
+
if (kind == "vector") return true;
|
|
135
|
+
var sql = ReadMember(definition, "sql") as string ?? ReadMember(definition, "type") as string;
|
|
136
|
+
if (!string.IsNullOrWhiteSpace(sql))
|
|
137
|
+
{
|
|
138
|
+
var trimmed = sql.Trim();
|
|
139
|
+
if (trimmed.StartsWith("VECTOR", StringComparison.OrdinalIgnoreCase)
|
|
140
|
+
&& (trimmed.Length == 6 || trimmed[6] == '(' || trimmed[6] == ' '))
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
var ts = ReadMember(definition, "ts") as string;
|
|
144
|
+
return ts != null && ts.Contains("[]");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static object ReadMember(object target, string name)
|
|
148
|
+
{
|
|
149
|
+
if (target is System.Collections.IDictionary dict)
|
|
150
|
+
return dict.Contains(name) ? dict[name] : null;
|
|
151
|
+
var prop = target.GetType().GetProperty(name);
|
|
152
|
+
return prop != null ? prop.GetValue(target) : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Distance ────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/// <summary>
|
|
158
|
+
/// Distance between two equal-length vectors, lower is closer. Mirrors
|
|
159
|
+
/// the SQL functions so a search ranks the same way whichever strategy ran.
|
|
160
|
+
/// </summary>
|
|
161
|
+
public static double? VectorDistance(IReadOnlyList<double> a, IReadOnlyList<double> b, string metric)
|
|
162
|
+
{
|
|
163
|
+
if (a == null || b == null || a.Count == 0 || a.Count != b.Count) return null;
|
|
164
|
+
var resolved = NormalizeMetric(metric);
|
|
165
|
+
if (resolved == "euclidean")
|
|
166
|
+
{
|
|
167
|
+
double sum = 0;
|
|
168
|
+
for (int i = 0; i < a.Count; i++)
|
|
169
|
+
{
|
|
170
|
+
var diff = a[i] - b[i];
|
|
171
|
+
sum += diff * diff;
|
|
172
|
+
}
|
|
173
|
+
return Math.Sqrt(sum);
|
|
174
|
+
}
|
|
175
|
+
double dot = 0, m1 = 0, m2 = 0;
|
|
176
|
+
for (int i = 0; i < a.Count; i++)
|
|
177
|
+
{
|
|
178
|
+
dot += a[i] * b[i];
|
|
179
|
+
m1 += a[i] * a[i];
|
|
180
|
+
m2 += b[i] * b[i];
|
|
181
|
+
}
|
|
182
|
+
if (resolved == "dot") return -dot;
|
|
183
|
+
return m1 > 0 && m2 > 0 ? 1.0 - dot / (Math.Sqrt(m1) * Math.Sqrt(m2)) : 1.0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// <summary>The scalar function registered with the driver for one metric.</summary>
|
|
187
|
+
public static Func<object[], object> MakeDistanceFunction(string metric)
|
|
188
|
+
=> args =>
|
|
189
|
+
{
|
|
190
|
+
var a = DecodeVector(args.Length > 0 ? args[0] : null);
|
|
191
|
+
var b = DecodeVector(args.Length > 1 ? args[1] : null);
|
|
192
|
+
if (a == null || b == null) return DBNull.Value;
|
|
193
|
+
var distance = VectorDistance(a, b, metric);
|
|
194
|
+
return distance.HasValue ? (object)distance.Value : DBNull.Value;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ── Query building ──────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/// <summary>`json_valid` fails on a float32 BLOB, so the JSON path needs this guard.</summary>
|
|
200
|
+
private static string JsonText(string column)
|
|
201
|
+
=> $"CASE WHEN json_valid({column}) THEN {column} ELSE '[]' END";
|
|
202
|
+
|
|
203
|
+
private static string JsonDistanceExpr(string metric, string column)
|
|
204
|
+
{
|
|
205
|
+
var row = JsonText(column);
|
|
206
|
+
var sameLength = $"(SELECT COUNT(*) FROM json_each({row})) = (SELECT COUNT(*) FROM q)";
|
|
207
|
+
string terms;
|
|
208
|
+
switch (metric)
|
|
209
|
+
{
|
|
210
|
+
case "euclidean":
|
|
211
|
+
terms = $"sqrt((SELECT SUM((je.value - q.v) * (je.value - q.v)) FROM json_each({row}) je JOIN q ON q.k = je.key))";
|
|
212
|
+
break;
|
|
213
|
+
case "dot":
|
|
214
|
+
terms = $"-(SELECT SUM(je.value * q.v) FROM json_each({row}) je JOIN q ON q.k = je.key)";
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
terms = $"1.0 - (SELECT SUM(je.value * q.v) FROM json_each({row}) je JOIN q ON q.k = je.key)"
|
|
218
|
+
+ $" / NULLIF(sqrt((SELECT SUM(q.v * q.v) FROM q)) * sqrt((SELECT SUM(je.value * je.value) FROM json_each({row}) je)), 0)";
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
return $"CASE WHEN {sameLength} THEN {terms} END";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// <summary>
|
|
225
|
+
/// Ranking SQL for one strategy, with the parameters it needs appended to
|
|
226
|
+
/// `parameters`. `sqlite-vec` and `udf` bind the query vector as a float32
|
|
227
|
+
/// BLOB; `sql` binds it as JSON text because `json_each` reads text.
|
|
228
|
+
/// </summary>
|
|
229
|
+
public static string BuildQuery(
|
|
230
|
+
string strategy, string metric, string table, string column,
|
|
231
|
+
IReadOnlyList<double> vector, int take, string tail,
|
|
232
|
+
Dictionary<string, object> parameters)
|
|
233
|
+
{
|
|
234
|
+
metric = NormalizeMetric(metric);
|
|
235
|
+
// A row whose stored vector cannot be scored (a NULL, a text column,
|
|
236
|
+
// another dimension) yields a NULL distance; those rows are dropped
|
|
237
|
+
// rather than reported, so a caller never has to sort them out.
|
|
238
|
+
string Rank(string inner)
|
|
239
|
+
=> $"SELECT * FROM ({inner}) AS an5_ranked WHERE distance IS NOT NULL ORDER BY distance ASC LIMIT {take}";
|
|
240
|
+
|
|
241
|
+
if (strategy == StrategySqliteVec)
|
|
242
|
+
{
|
|
243
|
+
parameters["an5_vector"] = EncodeVector(vector);
|
|
244
|
+
parameters["an5_vector_bytes"] = vector.Count * BytesPerFloat;
|
|
245
|
+
// sqlite-vec only understands float32 BLOB operands, so rows stored
|
|
246
|
+
// any other way are excluded instead of aborting the query.
|
|
247
|
+
return Rank($"SELECT *, CASE WHEN vec_length(vec_f32({column})) = {vector.Count}"
|
|
248
|
+
+ $" THEN {SqliteVecFunctions[metric]}(vec_f32({column}), @an5_vector) END AS distance FROM {table}"
|
|
249
|
+
+ (string.IsNullOrWhiteSpace(tail) ? " WHERE " : tail + " AND ")
|
|
250
|
+
+ $"{column} IS NOT NULL");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (strategy == StrategyUdf)
|
|
254
|
+
{
|
|
255
|
+
parameters["an5_vector"] = EncodeVector(vector);
|
|
256
|
+
return Rank($"SELECT *, {An5Functions[metric]}({column}, @an5_vector) AS distance FROM {table}{tail}");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
parameters["an5_vector_json"] = JsonSerializer.Serialize(vector);
|
|
260
|
+
return "WITH q AS (SELECT je.key AS k, CAST(je.value AS REAL) AS v FROM json_each(@an5_vector_json) je) "
|
|
261
|
+
+ Rank($"SELECT *, {JsonDistanceExpr(metric, column)} AS distance FROM {table}{tail}");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Capabilities ────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/// <summary>
|
|
267
|
+
/// The ordered strategies to try. `declaredType` is the column's DDL type:
|
|
268
|
+
/// the JSON strategy only reaches rows stored as text, so a `BLOB` column
|
|
269
|
+
/// drops it from the order.
|
|
270
|
+
/// </summary>
|
|
271
|
+
public static List<string> PlanStrategies(
|
|
272
|
+
bool hasVec, bool hasUdf, bool hasJson1, string declaredType, string preference)
|
|
273
|
+
{
|
|
274
|
+
if (!string.IsNullOrWhiteSpace(preference) && preference != "auto")
|
|
275
|
+
return preference == StrategyMemory ? new List<string>() : new List<string> { preference };
|
|
276
|
+
|
|
277
|
+
var available = new List<string>();
|
|
278
|
+
if (hasVec) available.Add(StrategySqliteVec);
|
|
279
|
+
if (hasUdf) available.Add(StrategyUdf);
|
|
280
|
+
if (hasJson1) available.Add(StrategySql);
|
|
281
|
+
|
|
282
|
+
var declared = (declaredType ?? "").ToUpperInvariant();
|
|
283
|
+
if (declared.Length > 0
|
|
284
|
+
&& !new[] { "TEXT", "CHAR", "CLOB", "STRING", "JSON" }.Any(t => declared.Contains(t)))
|
|
285
|
+
{
|
|
286
|
+
// A BLOB column has no JSON to read, so json_each can only produce NULLs.
|
|
287
|
+
available.Remove(StrategySql);
|
|
288
|
+
}
|
|
289
|
+
return available;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// <summary>Reads a vector column's declared DDL type, or null when not found.</summary>
|
|
293
|
+
public static string ReadColumnType(
|
|
294
|
+
Func<string, Dictionary<string, object>, List<Dictionary<string, object>>> exec,
|
|
295
|
+
string table, string column)
|
|
296
|
+
{
|
|
297
|
+
try
|
|
298
|
+
{
|
|
299
|
+
var rows = exec(
|
|
300
|
+
"SELECT type FROM pragma_table_info(@an5_table) WHERE name = @an5_column",
|
|
301
|
+
new Dictionary<string, object> { ["an5_table"] = table, ["an5_column"] = column });
|
|
302
|
+
var value = rows.Count > 0 ? rows[0].GetValueOrDefault("type") : null;
|
|
303
|
+
return value == null ? null : value.ToString();
|
|
304
|
+
}
|
|
305
|
+
catch { return null; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// <summary>
|
|
309
|
+
/// Ranks a SQLite table inside the database. Returns null when the
|
|
310
|
+
/// connection offers no strategy, so the caller can fall back to scoring
|
|
311
|
+
/// in memory.
|
|
312
|
+
/// </summary>
|
|
313
|
+
public static List<Dictionary<string, object>> RunSearch(
|
|
314
|
+
Func<string, Dictionary<string, object>, List<Dictionary<string, object>>> exec,
|
|
315
|
+
string table, string rawTable, string column, string rawColumn,
|
|
316
|
+
IReadOnlyList<double> vector, string metric, int take, string tail,
|
|
317
|
+
Dictionary<string, object> parameters, SqliteVectorSupport support)
|
|
318
|
+
{
|
|
319
|
+
if (vector == null || vector.Count == 0) return null;
|
|
320
|
+
|
|
321
|
+
var caps = support?.Capabilities() ?? new SqliteVectorCapabilities { Json1 = true };
|
|
322
|
+
if (support != null) support.ProbeJson1(exec);
|
|
323
|
+
|
|
324
|
+
var declaredType = ReadColumnType(exec, rawTable, rawColumn);
|
|
325
|
+
foreach (var strategy in PlanStrategies(caps.Vec, caps.Udf, caps.Json1, declaredType, support?.Preference))
|
|
326
|
+
{
|
|
327
|
+
if (strategy == StrategySql) {
|
|
328
|
+
var binary = exec($"SELECT 1 FROM {table}" + (string.IsNullOrWhiteSpace(tail) ? " WHERE " : tail + " AND ")
|
|
329
|
+
+ $"typeof({column}) = 'blob' LIMIT 1", parameters);
|
|
330
|
+
if (binary.Count > 0) continue;
|
|
331
|
+
}
|
|
332
|
+
var bound = new Dictionary<string, object>();
|
|
333
|
+
foreach (var kv in parameters) bound[kv.Key] = kv.Value;
|
|
334
|
+
var sql = BuildQuery(strategy, metric, table, column, vector, take, tail, bound);
|
|
335
|
+
try
|
|
336
|
+
{
|
|
337
|
+
var rows = exec(sql, bound);
|
|
338
|
+
if (rows != null) return rows;
|
|
339
|
+
}
|
|
340
|
+
catch
|
|
341
|
+
{
|
|
342
|
+
// The connection advertised the capability but the query
|
|
343
|
+
// failed, e.g. an extension that did not really load. The
|
|
344
|
+
// next strategy is cheaper than reporting the failure.
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// <summary>What a SQLite connection can do, resolved once and cached.</summary>
|
|
353
|
+
public class SqliteVectorCapabilities
|
|
354
|
+
{
|
|
355
|
+
/// <summary>`vec_version()` answered, so the sqlite-vec extension is loaded.</summary>
|
|
356
|
+
public bool Vec { get; set; }
|
|
357
|
+
/// <summary>The adapter's own distance functions are registered.</summary>
|
|
358
|
+
public bool Udf { get; set; }
|
|
359
|
+
/// <summary>`json_each()` is available, which the pure-SQL strategy needs.</summary>
|
|
360
|
+
public bool Json1 { get; set; }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// <summary>
|
|
364
|
+
/// The settings one SQLite connection needs for in-database vector search.
|
|
365
|
+
/// Filled in by the engine, which knows how to open connections.
|
|
366
|
+
/// </summary>
|
|
367
|
+
public class SqliteVectorSupport
|
|
368
|
+
{
|
|
369
|
+
/// <summary>Path to the sqlite-vec extension binary to load.</summary>
|
|
370
|
+
public string SqliteVecPath { get; set; }
|
|
371
|
+
/// <summary>Pin a strategy instead of probing for the fastest one available.</summary>
|
|
372
|
+
public string Preference { get; set; }
|
|
373
|
+
|
|
374
|
+
private SqliteVectorCapabilities _capabilities;
|
|
375
|
+
|
|
376
|
+
public SqliteVectorCapabilities Capabilities()
|
|
377
|
+
=> _capabilities ??= new SqliteVectorCapabilities { Udf = true, Json1 = true };
|
|
378
|
+
|
|
379
|
+
/// <summary>Confirms JSON1 before a query is built that needs it.</summary>
|
|
380
|
+
public void ProbeJson1(Func<string, Dictionary<string, object>, List<Dictionary<string, object>>> exec)
|
|
381
|
+
{
|
|
382
|
+
var caps = Capabilities();
|
|
383
|
+
if (caps.Json1) return;
|
|
384
|
+
try
|
|
385
|
+
{
|
|
386
|
+
exec("SELECT json_valid('[1]') AS an5_json_ok", new Dictionary<string, object>());
|
|
387
|
+
caps.Json1 = true;
|
|
388
|
+
}
|
|
389
|
+
catch { caps.Json1 = false; }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// <summary>Records what the connection actually manages, after the engine tried.</summary>
|
|
393
|
+
internal void Report(bool vec, bool udf, bool json1)
|
|
394
|
+
{
|
|
395
|
+
var caps = Capabilities();
|
|
396
|
+
caps.Vec = vec;
|
|
397
|
+
caps.Udf = udf;
|
|
398
|
+
caps.Json1 = json1;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
package/golang/an5_adapter.go
CHANGED
|
@@ -68,6 +68,9 @@ func GetRelationsForModel(modelName string) map[string]RelationDef {
|
|
|
68
68
|
type An5Adapter struct {
|
|
69
69
|
DB *sql.DB
|
|
70
70
|
Dialect Dialect
|
|
71
|
+
// VectorSupport carries the SQLite vector settings. Set it before the first
|
|
72
|
+
// search to load sqlite-vec or to pin a strategy.
|
|
73
|
+
VectorSupport sqlite.VectorSupport
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
// NewAn5Adapter constructs an An5Adapter instance.
|
|
@@ -167,14 +170,47 @@ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetV
|
|
|
167
170
|
vecBytes, _ := json.Marshal(targetVector)
|
|
168
171
|
vecStr := string(vecBytes)
|
|
169
172
|
|
|
170
|
-
// 1.
|
|
173
|
+
// 1. SQLite ranks through sqlite-vec, a driver function or json_each,
|
|
174
|
+
// whichever this connection supports, so the table never has to leave the
|
|
175
|
+
// database. `database/sql` cannot register a user function, so the UDF
|
|
176
|
+
// strategy needs the driver to install them; see sqlite.RegisterVectorFunctions.
|
|
177
|
+
if a.Dialect == base.DialectSqlite {
|
|
178
|
+
tail := ""
|
|
179
|
+
if strings.TrimSpace(whereClause) != "" {
|
|
180
|
+
tail = " WHERE " + whereClause
|
|
181
|
+
}
|
|
182
|
+
support := a.VectorSupport
|
|
183
|
+
ranked, ran, err := sqlite.RunVectorSearch(
|
|
184
|
+
ctx,
|
|
185
|
+
a.DB,
|
|
186
|
+
a.QueryRaw,
|
|
187
|
+
base.QuoteTable(tableName, a.Dialect),
|
|
188
|
+
sqlite.NormalizeTableName(tableName),
|
|
189
|
+
base.QuoteIdentifier(vectorField, a.Dialect),
|
|
190
|
+
vectorField,
|
|
191
|
+
targetVector,
|
|
192
|
+
distanceMetric,
|
|
193
|
+
take,
|
|
194
|
+
tail,
|
|
195
|
+
args,
|
|
196
|
+
support,
|
|
197
|
+
)
|
|
198
|
+
if err != nil {
|
|
199
|
+
return nil, err
|
|
200
|
+
}
|
|
201
|
+
if ran {
|
|
202
|
+
return DecodeVectorRows(ranked, vectorField), nil
|
|
203
|
+
}
|
|
204
|
+
// No in-database strategy: score the column in Go instead.
|
|
205
|
+
return a.vectorSearchInMemory(ctx, tableName, targetVector, take, whereClause, vectorField, distanceMetric, args)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 2. Primary path: Native database SQL vector query execution via dialect provider (mssql / postgres)
|
|
171
209
|
var sqlQuery string
|
|
172
210
|
if a.Dialect == base.DialectPostgres {
|
|
173
211
|
sqlQuery = postgres.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
|
|
174
212
|
} else if a.Dialect == base.DialectMssql {
|
|
175
213
|
sqlQuery = mssql.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
|
|
176
|
-
} else if a.Dialect == base.DialectSqlite {
|
|
177
|
-
sqlQuery = sqlite.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
|
|
178
214
|
}
|
|
179
215
|
|
|
180
216
|
if sqlQuery != "" {
|
|
@@ -185,7 +221,7 @@ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetV
|
|
|
185
221
|
}
|
|
186
222
|
}
|
|
187
223
|
|
|
188
|
-
//
|
|
224
|
+
// 3. Secondary fallback: Fetch rows and compute vector distance in-memory if DB lacks native vector extensions
|
|
189
225
|
fallbackQuery := "SELECT * FROM " + base.QuoteTable(tableName, a.Dialect)
|
|
190
226
|
if strings.TrimSpace(whereClause) != "" {
|
|
191
227
|
fallbackQuery += " WHERE " + whereClause
|
|
@@ -199,6 +235,45 @@ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetV
|
|
|
199
235
|
return VectorSearchFallback(rows, targetVector, take, vectorField, distanceMetric), nil
|
|
200
236
|
}
|
|
201
237
|
|
|
238
|
+
// vectorSearchInMemory loads the table and scores the vector column in Go.
|
|
239
|
+
//
|
|
240
|
+
// The last resort for every provider, and the only option on a SQLite connection
|
|
241
|
+
// that offers neither sqlite-vec, a driver function nor JSON1. The whole table is
|
|
242
|
+
// loaded, so it is only reasonable for a table small enough to fit in memory.
|
|
243
|
+
func (a *An5Adapter) vectorSearchInMemory(ctx context.Context, tableName string, targetVector []float64, take int, whereClause string, vectorField string, distanceMetric string, args []interface{}) ([]map[string]interface{}, error) {
|
|
244
|
+
query := "SELECT * FROM " + base.QuoteTable(tableName, a.Dialect)
|
|
245
|
+
if strings.TrimSpace(whereClause) != "" {
|
|
246
|
+
query += " WHERE " + whereClause
|
|
247
|
+
}
|
|
248
|
+
rows, err := a.QueryRaw(ctx, query, args...)
|
|
249
|
+
if err != nil {
|
|
250
|
+
return nil, err
|
|
251
|
+
}
|
|
252
|
+
return VectorSearchFallback(rows, targetVector, take, vectorField, distanceMetric), nil
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// DecodeVectorRows replaces every stored `VECTOR(n)` column with a decoded
|
|
256
|
+
// float64 slice, so a caller reads numbers rather than float32 bytes or JSON text.
|
|
257
|
+
func DecodeVectorRows(rows []map[string]interface{}, vectorField string) []map[string]interface{} {
|
|
258
|
+
for _, row := range rows {
|
|
259
|
+
for key, value := range row {
|
|
260
|
+
if key != vectorField {
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
decoded := base.DecodeVector(value)
|
|
264
|
+
if decoded == nil {
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
// A `distance` column is a real number, not a stored vector.
|
|
268
|
+
if key == "distance" {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
row[key] = decoded
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return rows
|
|
275
|
+
}
|
|
276
|
+
|
|
202
277
|
// VectorSearchFallback ranks rows in-memory.
|
|
203
278
|
func VectorSearchFallback(rows []map[string]interface{}, targetVector []float64, take int, vectorField string, distanceMetric string) []map[string]interface{} {
|
|
204
279
|
type scored struct {
|
|
@@ -213,36 +288,14 @@ func VectorSearchFallback(rows []map[string]interface{}, targetVector []float64,
|
|
|
213
288
|
continue
|
|
214
289
|
}
|
|
215
290
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
_ = json.Unmarshal(v, &rowVec)
|
|
222
|
-
case []float64:
|
|
223
|
-
rowVec = v
|
|
224
|
-
case []interface{}:
|
|
225
|
-
for _, item := range v {
|
|
226
|
-
if f, ok := item.(float64); ok {
|
|
227
|
-
rowVec = append(rowVec, f)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if len(rowVec) != len(targetVector) {
|
|
291
|
+
// A float32 BLOB, a legacy JSON text column and an already-decoded slice
|
|
292
|
+
// all read the same way.
|
|
293
|
+
rowVec := base.DecodeVector(raw)
|
|
294
|
+
dist, ok := base.VectorDistance(targetVector, rowVec, distanceMetric)
|
|
295
|
+
if !ok {
|
|
233
296
|
continue
|
|
234
297
|
}
|
|
235
298
|
|
|
236
|
-
var dist float64
|
|
237
|
-
switch strings.ToLower(distanceMetric) {
|
|
238
|
-
case "euclidean":
|
|
239
|
-
dist = base.EuclideanDistance(targetVector, rowVec)
|
|
240
|
-
case "dot":
|
|
241
|
-
dist = -base.DotProduct(targetVector, rowVec)
|
|
242
|
-
default: // cosine
|
|
243
|
-
dist = 1.0 - base.CosineSimilarity(targetVector, rowVec)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
299
|
rCopy := make(map[string]interface{})
|
|
247
300
|
for k, val := range r {
|
|
248
301
|
rCopy[k] = val
|
|
@@ -311,4 +364,3 @@ func (a *An5Adapter) ExecuteProc(ctx context.Context, procName string, args ...i
|
|
|
311
364
|
}
|
|
312
365
|
return a.ExecuteRaw(ctx, sql, args...)
|
|
313
366
|
}
|
|
314
|
-
|