@an5/adapters 0.2.10 → 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 +163 -2
- package/dist/an5Adapter.d.ts +37 -0
- package/dist/an5Adapter.js +190 -65
- package/dist/base/metadata.d.ts +2 -0
- package/dist/base/metadata.js +10 -0
- package/dist/base/sql.d.ts +2 -0
- package/dist/base/sql.js +24 -25
- package/dist/base/types.d.ts +14 -0
- package/dist/googlesheets/adapter.js +16 -8
- package/dist/googlesheets/config.d.ts +13 -0
- package/dist/googlesheets/config.js +5 -1
- package/dist/googlesheets/oauth.d.ts +3 -0
- package/dist/googlesheets/oauth.js +43 -0
- package/dist/googlesheets/parseConnectionString.js +12 -0
- package/dist/googlesheets/tableClient.js +5 -23
- 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 +40 -9
- 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 +200 -58
- package/typescript/src/base/metadata.ts +8 -0
- package/typescript/src/base/sql.ts +23 -16
- package/typescript/src/base/types.ts +15 -0
- package/typescript/src/googlesheets/adapter.ts +17 -8
- package/typescript/src/googlesheets/config.ts +10 -1
- package/typescript/src/googlesheets/oauth.ts +31 -0
- package/typescript/src/googlesheets/parseConnectionString.ts +8 -0
- package/typescript/src/googlesheets/tableClient.ts +5 -19
- 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,375 @@
|
|
|
1
|
+
package an5.adapters.base;
|
|
2
|
+
|
|
3
|
+
import java.nio.ByteBuffer;
|
|
4
|
+
import java.nio.ByteOrder;
|
|
5
|
+
import java.util.ArrayList;
|
|
6
|
+
import java.util.Arrays;
|
|
7
|
+
import java.util.HashMap;
|
|
8
|
+
import java.util.List;
|
|
9
|
+
import java.util.Locale;
|
|
10
|
+
import java.util.Map;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SQLite vector search: the codec, the strategy plan and the ranking SQL.
|
|
14
|
+
*
|
|
15
|
+
* <p>SQLite has no vector type, so a {@code VECTOR(n)} column stores a BLOB of little-endian
|
|
16
|
+
* float32 values (see {@link #encodeVector(double[])}) and is ranked in one of four ways, in
|
|
17
|
+
* this order:
|
|
18
|
+
*
|
|
19
|
+
* <ol>
|
|
20
|
+
* <li>{@code sqlite-vec} — native scalar distances, when the extension loads.
|
|
21
|
+
* <li>{@code udf} — {@code an5_vec_cosine} / {@code an5_vec_l2} / {@code an5_vec_ip}
|
|
22
|
+
* registered with the driver. Reads the BLOB and the legacy JSON text.
|
|
23
|
+
* <li>{@code sql} — {@code json_each} brute force in plain SQL. Needs no user function, so
|
|
24
|
+
* it works through plain JDBC, but only reaches rows stored as JSON text.
|
|
25
|
+
* <li>{@code memory} — the column is loaded and scored in Java.
|
|
26
|
+
* </ol>
|
|
27
|
+
*
|
|
28
|
+
* <p>Strategies 1-3 rank inside the database and transfer only the matching rows, which is why
|
|
29
|
+
* they are preferred over the in-memory path. JDBC has no API for registering a user function
|
|
30
|
+
* or loading an extension, so a stock adapter reaches {@code sql} and {@code memory}: {@code
|
|
31
|
+
* sqlite-vec} has to be loaded by the driver itself (the Xerial driver offers a {@code
|
|
32
|
+
* _load_extension} connection property), and the UDFs need a driver built for it.
|
|
33
|
+
*
|
|
34
|
+
* <p>The docstring in the TypeScript {@code sqlite/vector.ts} is the shared specification every
|
|
35
|
+
* runtime implements.
|
|
36
|
+
*/
|
|
37
|
+
public final class SqliteVectors {
|
|
38
|
+
|
|
39
|
+
private SqliteVectors() {}
|
|
40
|
+
|
|
41
|
+
/** The size of one stored value; a column's byte length is its dimension times this. */
|
|
42
|
+
public static final int BYTES_PER_FLOAT = 4;
|
|
43
|
+
|
|
44
|
+
public static final String STRATEGY_SQLITE_VEC = "sqlite-vec";
|
|
45
|
+
public static final String STRATEGY_UDF = "udf";
|
|
46
|
+
public static final String STRATEGY_SQL = "sql";
|
|
47
|
+
public static final String STRATEGY_MEMORY = "memory";
|
|
48
|
+
|
|
49
|
+
private static final Map<String, String> AN5_FUNCTIONS = new HashMap<String, String>();
|
|
50
|
+
private static final Map<String, String> SQLITE_VEC_FUNCTIONS = new HashMap<String, String>();
|
|
51
|
+
|
|
52
|
+
static {
|
|
53
|
+
AN5_FUNCTIONS.put("cosine", "an5_vec_cosine");
|
|
54
|
+
AN5_FUNCTIONS.put("euclidean", "an5_vec_l2");
|
|
55
|
+
AN5_FUNCTIONS.put("dot", "an5_vec_ip");
|
|
56
|
+
SQLITE_VEC_FUNCTIONS.put("cosine", "vec_distance_cosine");
|
|
57
|
+
SQLITE_VEC_FUNCTIONS.put("euclidean", "vec_distance_l2");
|
|
58
|
+
SQLITE_VEC_FUNCTIONS.put("dot", "vec_distance_ip");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The scalar functions the adapter registers itself. */
|
|
62
|
+
public static Map<String, String> an5Functions() {
|
|
63
|
+
return new HashMap<String, String>(AN5_FUNCTIONS);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The scalar functions the sqlite-vec extension provides. */
|
|
67
|
+
public static Map<String, String> sqliteVecFunctions() {
|
|
68
|
+
return new HashMap<String, String>(SQLITE_VEC_FUNCTIONS);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Returns a known metric name, defaulting to cosine. */
|
|
72
|
+
public static String normalizeMetric(String metric) {
|
|
73
|
+
if (metric == null) {
|
|
74
|
+
return "cosine";
|
|
75
|
+
}
|
|
76
|
+
String lowered = metric.toLowerCase(Locale.ROOT);
|
|
77
|
+
return "euclidean".equals(lowered) || "dot".equals(lowered) ? lowered : "cosine";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The function a strategy uses for a metric, or {@code null} when it has none. */
|
|
81
|
+
public static String distanceFunction(String strategy, String metric) {
|
|
82
|
+
Map<String, String> table;
|
|
83
|
+
if (STRATEGY_SQLITE_VEC.equals(strategy)) {
|
|
84
|
+
table = SQLITE_VEC_FUNCTIONS;
|
|
85
|
+
} else if (STRATEGY_UDF.equals(strategy)) {
|
|
86
|
+
table = AN5_FUNCTIONS;
|
|
87
|
+
} else {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return table.get(normalizeMetric(metric));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Codec ──────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** Encodes a vector as the little-endian float32 BLOB the column stores. */
|
|
96
|
+
public static byte[] encodeVector(double[] values) {
|
|
97
|
+
ByteBuffer buffer = ByteBuffer.allocate(values.length * BYTES_PER_FLOAT);
|
|
98
|
+
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
99
|
+
for (double value : values) {
|
|
100
|
+
buffer.putFloat((float) value);
|
|
101
|
+
}
|
|
102
|
+
return buffer.array();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True when the value still has to become bytes on its way into a column. */
|
|
106
|
+
public static boolean needsVectorEncoding(Object value) {
|
|
107
|
+
return value instanceof double[] || value instanceof float[] || value instanceof List;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decodes a stored vector: a float32 BLOB, a legacy JSON text column, or an array the
|
|
112
|
+
* driver already decoded.
|
|
113
|
+
*
|
|
114
|
+
* @param expectedLength the query vector's length, or {@code 0} to accept any length
|
|
115
|
+
*/
|
|
116
|
+
public static double[] decodeVector(Object value, int expectedLength) {
|
|
117
|
+
if (value == null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (value instanceof byte[]) {
|
|
121
|
+
return decodeVectorBytes((byte[]) value, expectedLength);
|
|
122
|
+
}
|
|
123
|
+
if (value instanceof double[]) {
|
|
124
|
+
double[] copy = ((double[]) value).clone();
|
|
125
|
+
return expectedLength > 0 && copy.length != expectedLength ? null : copy;
|
|
126
|
+
}
|
|
127
|
+
if (value instanceof float[]) {
|
|
128
|
+
float[] source = (float[]) value;
|
|
129
|
+
double[] copy = new double[source.length];
|
|
130
|
+
for (int i = 0; i < source.length; i++) {
|
|
131
|
+
copy[i] = source[i];
|
|
132
|
+
}
|
|
133
|
+
return expectedLength > 0 && copy.length != expectedLength ? null : copy;
|
|
134
|
+
}
|
|
135
|
+
if (value instanceof List) {
|
|
136
|
+
List<?> items = (List<?>) value;
|
|
137
|
+
double[] copy = new double[items.size()];
|
|
138
|
+
for (int i = 0; i < items.size(); i++) {
|
|
139
|
+
if (!(items.get(i) instanceof Number)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
copy[i] = ((Number) items.get(i)).doubleValue();
|
|
143
|
+
}
|
|
144
|
+
return expectedLength > 0 && copy.length != expectedLength ? null : copy;
|
|
145
|
+
}
|
|
146
|
+
// A column written by an older version holds JSON text, which Vectors already reads.
|
|
147
|
+
return Vectors.parseVector(value, expectedLength);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Decodes the float32 BLOB a {@code VECTOR(n)} column stores.
|
|
152
|
+
*
|
|
153
|
+
* <p>JSON text is not a multiple of four bytes once encoded and starts with {@code '['}, so
|
|
154
|
+
* the two are told apart by length and first byte.
|
|
155
|
+
*/
|
|
156
|
+
public static double[] decodeVectorBytes(byte[] bytes, int expectedLength) {
|
|
157
|
+
if (bytes == null || bytes.length == 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (bytes[0] == '[' || bytes.length % BYTES_PER_FLOAT != 0) {
|
|
161
|
+
return Vectors.parseVector(new String(bytes, java.nio.charset.StandardCharsets.UTF_8),
|
|
162
|
+
expectedLength);
|
|
163
|
+
}
|
|
164
|
+
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
|
165
|
+
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
166
|
+
double[] values = new double[bytes.length / BYTES_PER_FLOAT];
|
|
167
|
+
for (int i = 0; i < values.length; i++) {
|
|
168
|
+
values[i] = buffer.getFloat();
|
|
169
|
+
}
|
|
170
|
+
if (expectedLength > 0 && values.length != expectedLength) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return values;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** True when generated metadata describes a {@code VECTOR(n)} column. */
|
|
177
|
+
public static boolean isVectorField(Object definition) {
|
|
178
|
+
if (definition == null) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (definition instanceof String) {
|
|
182
|
+
String text = (String) definition;
|
|
183
|
+
return text.contains("[]") || text.trim().toUpperCase(Locale.ROOT).startsWith("VECTOR");
|
|
184
|
+
}
|
|
185
|
+
if (definition instanceof Map) {
|
|
186
|
+
Map<?, ?> record = (Map<?, ?>) definition;
|
|
187
|
+
if ("vector".equals(record.get("kind"))) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
for (String key : new String[] {"sql", "type"}) {
|
|
191
|
+
Object sql = record.get(key);
|
|
192
|
+
if (sql instanceof String && sqlLooksVector((String) sql)) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
Object ts = record.get("ts");
|
|
197
|
+
return ts instanceof String && ((String) ts).contains("[]");
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** True when a declared SQL type names a vector column. */
|
|
203
|
+
public static boolean sqlLooksVector(String sql) {
|
|
204
|
+
String trimmed = sql.trim().toUpperCase(Locale.ROOT);
|
|
205
|
+
if (!trimmed.startsWith("VECTOR")) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
String rest = trimmed.substring("VECTOR".length());
|
|
209
|
+
return rest.isEmpty() || rest.startsWith("(") || rest.startsWith(" ");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Strategy plan ───────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** What a connection can do, probed once and cached by the adapter. */
|
|
215
|
+
public static final class Capabilities {
|
|
216
|
+
/** {@code vec_version()} answered, so the sqlite-vec extension is loaded. */
|
|
217
|
+
public boolean vec;
|
|
218
|
+
/** The adapter's own distance functions are registered. */
|
|
219
|
+
public boolean udf;
|
|
220
|
+
/** {@code json_each()} is available, which the pure-SQL strategy needs. */
|
|
221
|
+
public boolean json1;
|
|
222
|
+
|
|
223
|
+
public Capabilities() {}
|
|
224
|
+
|
|
225
|
+
public Capabilities(boolean vec, boolean udf, boolean json1) {
|
|
226
|
+
this.vec = vec;
|
|
227
|
+
this.udf = udf;
|
|
228
|
+
this.json1 = json1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The ordered strategies to try.
|
|
234
|
+
*
|
|
235
|
+
* <p>{@code declaredType} is the column's DDL type: the JSON strategy only reaches rows
|
|
236
|
+
* stored as text, so a {@code BLOB} column drops it from the order.
|
|
237
|
+
*/
|
|
238
|
+
public static List<String> planStrategies(
|
|
239
|
+
Capabilities capabilities, String declaredType, String preference) {
|
|
240
|
+
if (preference != null && !preference.isEmpty() && !"auto".equals(preference)) {
|
|
241
|
+
// `memory` is the caller's own path, not a query, so it yields no plan.
|
|
242
|
+
return "memory".equals(preference)
|
|
243
|
+
? new ArrayList<String>()
|
|
244
|
+
: new ArrayList<String>(Arrays.asList(preference));
|
|
245
|
+
}
|
|
246
|
+
List<String> available = new ArrayList<String>();
|
|
247
|
+
if (capabilities.vec) {
|
|
248
|
+
available.add(STRATEGY_SQLITE_VEC);
|
|
249
|
+
}
|
|
250
|
+
if (capabilities.udf) {
|
|
251
|
+
available.add(STRATEGY_UDF);
|
|
252
|
+
}
|
|
253
|
+
if (capabilities.json1) {
|
|
254
|
+
available.add(STRATEGY_SQL);
|
|
255
|
+
}
|
|
256
|
+
String declared = declaredType == null ? "" : declaredType.trim().toUpperCase(Locale.ROOT);
|
|
257
|
+
if (!declared.isEmpty()
|
|
258
|
+
&& !declared.contains("TEXT")
|
|
259
|
+
&& !declared.contains("CHAR")
|
|
260
|
+
&& !declared.contains("CLOB")
|
|
261
|
+
&& !declared.contains("STRING")
|
|
262
|
+
&& !declared.contains("JSON")) {
|
|
263
|
+
// A BLOB column has no JSON to read, so json_each could only produce NULLs.
|
|
264
|
+
available.remove(STRATEGY_SQL);
|
|
265
|
+
}
|
|
266
|
+
return available;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Query building ──────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/** {@code json_valid} fails on a float32 BLOB, so the JSON path needs this guard. */
|
|
272
|
+
private static String jsonText(String column) {
|
|
273
|
+
return "CASE WHEN json_valid(" + column + ") THEN " + column + " ELSE '[]' END";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private static String jsonDistanceExpr(String metric, String column) {
|
|
277
|
+
String row = jsonText(column);
|
|
278
|
+
String sameLength = "(SELECT COUNT(*) FROM json_each(" + row + ")) = (SELECT COUNT(*) FROM q)";
|
|
279
|
+
String terms;
|
|
280
|
+
if ("euclidean".equals(metric)) {
|
|
281
|
+
terms =
|
|
282
|
+
"sqrt((SELECT SUM((je.value - q.v) * (je.value - q.v)) FROM json_each("
|
|
283
|
+
+ row
|
|
284
|
+
+ ") je JOIN q ON q.k = je.key))";
|
|
285
|
+
} else if ("dot".equals(metric)) {
|
|
286
|
+
terms =
|
|
287
|
+
"-(SELECT SUM(je.value * q.v) FROM json_each(" + row + ") je JOIN q ON q.k = je.key)";
|
|
288
|
+
} else {
|
|
289
|
+
terms =
|
|
290
|
+
"1.0 - (SELECT SUM(je.value * q.v) FROM json_each("
|
|
291
|
+
+ row
|
|
292
|
+
+ ") je JOIN q ON q.k = je.key)"
|
|
293
|
+
+ " / NULLIF(sqrt((SELECT SUM(q.v * q.v) FROM q)) * sqrt((SELECT SUM(je.value * je.value)"
|
|
294
|
+
+ " FROM json_each("
|
|
295
|
+
+ row
|
|
296
|
+
+ ") je)), 0)";
|
|
297
|
+
}
|
|
298
|
+
return "CASE WHEN " + sameLength + " THEN " + terms + " END";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The ranking SQL for one strategy.
|
|
303
|
+
*
|
|
304
|
+
* <p>{@code sqlite-vec} and {@code udf} bind the query vector as a float32 BLOB; {@code sql}
|
|
305
|
+
* binds it as JSON text because {@code json_each} reads text.
|
|
306
|
+
*
|
|
307
|
+
* <p>A row whose stored vector cannot be scored (a {@code NULL}, a text column, another
|
|
308
|
+
* dimension) yields a {@code NULL} distance; those rows are dropped rather than reported, so
|
|
309
|
+
* a caller never has to sort them out of the result.
|
|
310
|
+
*/
|
|
311
|
+
public static String buildRankingQuery(
|
|
312
|
+
String strategy,
|
|
313
|
+
String metric,
|
|
314
|
+
String table,
|
|
315
|
+
String column,
|
|
316
|
+
double[] vector,
|
|
317
|
+
int take,
|
|
318
|
+
String tail,
|
|
319
|
+
String placeholder,
|
|
320
|
+
List<Object> bind) {
|
|
321
|
+
String normalized = normalizeMetric(metric);
|
|
322
|
+
String inner;
|
|
323
|
+
if (STRATEGY_SQLITE_VEC.equals(strategy) || STRATEGY_UDF.equals(strategy)) {
|
|
324
|
+
String function = distanceFunction(strategy, normalized);
|
|
325
|
+
if (function == null) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
String distance = function + "(" + column + ", " + placeholder + ")";
|
|
329
|
+
String guard = "";
|
|
330
|
+
if (STRATEGY_SQLITE_VEC.equals(strategy)) {
|
|
331
|
+
// sqlite-vec only understands float32 BLOB operands, so rows stored any other way are
|
|
332
|
+
// excluded instead of aborting the query. The guard joins the caller's WHERE with AND,
|
|
333
|
+
// since appending the tail after it would produce a second WHERE.
|
|
334
|
+
guard = column + " IS NOT NULL";
|
|
335
|
+
distance = "CASE WHEN vec_length(vec_f32(" + column + ")) = " + vector.length
|
|
336
|
+
+ " THEN " + function + "(vec_f32(" + column + "), " + placeholder + ") END";
|
|
337
|
+
bind.add(encodeVector(vector));
|
|
338
|
+
} else {
|
|
339
|
+
bind.add(encodeVector(vector));
|
|
340
|
+
}
|
|
341
|
+
if (guard.isEmpty()) {
|
|
342
|
+
inner = "SELECT *, " + distance + " AS distance FROM " + table + tail;
|
|
343
|
+
} else if (tail.isEmpty()) {
|
|
344
|
+
inner = "SELECT *, " + distance + " AS distance FROM " + table + " WHERE " + guard;
|
|
345
|
+
} else {
|
|
346
|
+
inner =
|
|
347
|
+
"SELECT *, "
|
|
348
|
+
+ distance
|
|
349
|
+
+ " AS distance FROM "
|
|
350
|
+
+ table
|
|
351
|
+
+ " WHERE "
|
|
352
|
+
+ guard
|
|
353
|
+
+ " AND"
|
|
354
|
+
+ tail.replaceFirst("(?i)\\s+WHERE\\s+", " ");
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
bind.add(Vectors.formatVector(vector));
|
|
358
|
+
inner =
|
|
359
|
+
"SELECT *, "
|
|
360
|
+
+ jsonDistanceExpr(normalized, column)
|
|
361
|
+
+ " AS distance FROM "
|
|
362
|
+
+ table
|
|
363
|
+
+ tail;
|
|
364
|
+
inner =
|
|
365
|
+
"WITH q AS (SELECT je.key AS k, CAST(je.value AS REAL) AS v FROM json_each("
|
|
366
|
+
+ placeholder
|
|
367
|
+
+ ") je) "
|
|
368
|
+
+ inner;
|
|
369
|
+
}
|
|
370
|
+
return "SELECT * FROM ("
|
|
371
|
+
+ inner
|
|
372
|
+
+ ") AS an5_ranked WHERE distance IS NOT NULL ORDER BY distance ASC LIMIT "
|
|
373
|
+
+ take;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
package an5.adapters.base;
|
|
2
|
+
|
|
3
|
+
/** The distance metrics {@code vectorSearch} supports, and the math behind them. */
|
|
4
|
+
public final class Vectors {
|
|
5
|
+
|
|
6
|
+
private Vectors() {}
|
|
7
|
+
|
|
8
|
+
/** Cosine similarity in {@code [-1, 1]}; {@code 0} when the lengths differ or either side is empty. */
|
|
9
|
+
public static double cosineSimilarity(double[] left, double[] right) {
|
|
10
|
+
if (left == null || right == null || left.length != right.length || left.length == 0) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
double dot = 0;
|
|
14
|
+
double leftMagnitude = 0;
|
|
15
|
+
double rightMagnitude = 0;
|
|
16
|
+
for (int i = 0; i < left.length; i++) {
|
|
17
|
+
dot += left[i] * right[i];
|
|
18
|
+
leftMagnitude += left[i] * left[i];
|
|
19
|
+
rightMagnitude += right[i] * right[i];
|
|
20
|
+
}
|
|
21
|
+
if (leftMagnitude == 0 || rightMagnitude == 0) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
return dot / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** L2 distance; {@code 0} when the lengths differ or either side is empty. */
|
|
28
|
+
public static double euclideanDistance(double[] left, double[] right) {
|
|
29
|
+
if (left == null || right == null || left.length != right.length || left.length == 0) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
double sum = 0;
|
|
33
|
+
for (int i = 0; i < left.length; i++) {
|
|
34
|
+
double difference = left[i] - right[i];
|
|
35
|
+
sum += difference * difference;
|
|
36
|
+
}
|
|
37
|
+
return Math.sqrt(sum);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Inner product; {@code 0} when the lengths differ or either side is empty. */
|
|
41
|
+
public static double dotProduct(double[] left, double[] right) {
|
|
42
|
+
if (left == null || right == null || left.length != right.length) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
double sum = 0;
|
|
46
|
+
for (int i = 0; i < left.length; i++) {
|
|
47
|
+
sum += left[i] * right[i];
|
|
48
|
+
}
|
|
49
|
+
return sum;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The distance to sort ascending by, for a metric name.
|
|
54
|
+
*
|
|
55
|
+
* <p>Cosine similarity is turned into a distance as {@code 1 - similarity} and dot
|
|
56
|
+
* product is negated, so that ascending order means "closer" for every metric and the
|
|
57
|
+
* caller never has to remember which ones are inverted.
|
|
58
|
+
*/
|
|
59
|
+
public static double distance(String metric, double[] left, double[] right) {
|
|
60
|
+
if ("dot".equalsIgnoreCase(metric)) {
|
|
61
|
+
return -dotProduct(left, right);
|
|
62
|
+
}
|
|
63
|
+
if ("euclidean".equalsIgnoreCase(metric)) {
|
|
64
|
+
return euclideanDistance(left, right);
|
|
65
|
+
}
|
|
66
|
+
return 1 - cosineSimilarity(left, right);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reads a stored vector back out of a cell.
|
|
71
|
+
*
|
|
72
|
+
* <p>All three vector stores AN5 targets hand the column back as text — SQL Server's
|
|
73
|
+
* {@code VECTOR}, PostgreSQL's {@code vector}, and SQLite's JSON blob — so the in-memory
|
|
74
|
+
* fallback parses the same {@code [0.1, 0.2]} form. A row whose length does not match the
|
|
75
|
+
* query vector yields {@code null} rather than a silently wrong score.
|
|
76
|
+
*
|
|
77
|
+
* @param expectedLength the query vector's length, or {@code 0} to accept any length
|
|
78
|
+
*/
|
|
79
|
+
public static double[] parseVector(Object value, int expectedLength) {
|
|
80
|
+
if (value == null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (value instanceof double[]) {
|
|
84
|
+
double[] copy = ((double[]) value).clone();
|
|
85
|
+
return expectedLength > 0 && copy.length != expectedLength ? null : copy;
|
|
86
|
+
}
|
|
87
|
+
String text = String.valueOf(value).trim();
|
|
88
|
+
int open = text.indexOf('[');
|
|
89
|
+
int close = text.lastIndexOf(']');
|
|
90
|
+
if (open < 0 || close <= open) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
String body = text.substring(open + 1, close).trim();
|
|
94
|
+
if (body.isEmpty()) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
String[] parts = body.split(",");
|
|
98
|
+
double[] parsed = new double[parts.length];
|
|
99
|
+
for (int i = 0; i < parts.length; i++) {
|
|
100
|
+
try {
|
|
101
|
+
parsed[i] = Double.parseDouble(parts[i].trim());
|
|
102
|
+
} catch (NumberFormatException error) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (parsed.length == 0 || (expectedLength > 0 && parsed.length != expectedLength)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Renders a vector the way the vector stores expect it, as {@code [0.1, 0.2]}. */
|
|
113
|
+
public static String formatVector(double[] vector) {
|
|
114
|
+
StringBuilder out = new StringBuilder("[");
|
|
115
|
+
for (int i = 0; i < vector.length; i++) {
|
|
116
|
+
if (i > 0) {
|
|
117
|
+
out.append(", ");
|
|
118
|
+
}
|
|
119
|
+
out.append(vector[i]);
|
|
120
|
+
}
|
|
121
|
+
return out.append(']').toString();
|
|
122
|
+
}
|
|
123
|
+
}
|