eigen-db 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +70 -20
- package/dist/eigen-db.js +192 -191
- package/dist/eigen-db.js.map +1 -1
- package/dist/eigen-db.umd.cjs +1 -1
- package/dist/eigen-db.umd.cjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/__tests__/result-set.test.ts +53 -8
- package/src/lib/__tests__/vector-db.test.ts +64 -9
- package/src/lib/index.ts +1 -8
- package/src/lib/result-set.ts +28 -8
- package/src/lib/simd-binary.ts +1 -1
- package/src/lib/simd.wat +270 -128
- package/src/lib/types.ts +4 -10
- package/src/lib/vector-db.ts +11 -14
|
@@ -180,17 +180,17 @@ describe("VectorDB", () => {
|
|
|
180
180
|
const results = db.query([1, 0, 0, 0]);
|
|
181
181
|
expect(results.length).toBe(3);
|
|
182
182
|
|
|
183
|
-
// x-axis should be the best match (identical direction)
|
|
183
|
+
// x-axis should be the best match (identical direction, distance ≈ 0)
|
|
184
184
|
expect(results[0].key).toBe("x-axis");
|
|
185
|
-
expect(results[0].
|
|
185
|
+
expect(results[0].distance).toBeCloseTo(0.0, 2);
|
|
186
186
|
|
|
187
187
|
// xy-axis should be second (partially aligned)
|
|
188
188
|
expect(results[1].key).toBe("xy-axis");
|
|
189
|
-
expect(results[1].
|
|
189
|
+
expect(results[1].distance).toBeLessThan(1);
|
|
190
190
|
|
|
191
|
-
// y-axis should be last (orthogonal)
|
|
191
|
+
// y-axis should be last (orthogonal, distance ≈ 1)
|
|
192
192
|
expect(results[2].key).toBe("y-axis");
|
|
193
|
-
expect(results[2].
|
|
193
|
+
expect(results[2].distance).toBeCloseTo(1.0, 2);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
it("query respects topK option", async () => {
|
|
@@ -251,7 +251,7 @@ describe("VectorDB", () => {
|
|
|
251
251
|
expect(all).toHaveLength(5);
|
|
252
252
|
|
|
253
253
|
// Partial iteration (simulate pagination)
|
|
254
|
-
const page: { key: string;
|
|
254
|
+
const page: { key: string; distance: number }[] = [];
|
|
255
255
|
for (const item of results) {
|
|
256
256
|
page.push(item);
|
|
257
257
|
if (page.length === 2) break;
|
|
@@ -273,12 +273,67 @@ describe("VectorDB", () => {
|
|
|
273
273
|
db.set("point", [0, 1, 0, 0]);
|
|
274
274
|
|
|
275
275
|
const results = db.query([0, 1, 0, 0]);
|
|
276
|
-
// Both 'point' and 'other' are now along y-axis, so both should
|
|
277
|
-
expect(results[0].
|
|
278
|
-
expect(results[1].
|
|
276
|
+
// Both 'point' and 'other' are now along y-axis, so both should have distance ≈ 0
|
|
277
|
+
expect(results[0].distance).toBeCloseTo(0.0, 2);
|
|
278
|
+
expect(results[1].distance).toBeCloseTo(0.0, 2);
|
|
279
279
|
expect(db.size).toBe(2);
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
+
it("query respects maxDistance option", async () => {
|
|
283
|
+
const db = await VectorDB.open({
|
|
284
|
+
dimensions: 4,
|
|
285
|
+
storage,
|
|
286
|
+
wasmBinary,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
db.set("x-axis", [1, 0, 0, 0]);
|
|
290
|
+
db.set("y-axis", [0, 1, 0, 0]);
|
|
291
|
+
db.set("xy-axis", [1, 1, 0, 0]);
|
|
292
|
+
|
|
293
|
+
// Only return results with distance ≤ 0.5 from the x-axis query
|
|
294
|
+
const results = db.query([1, 0, 0, 0], { maxDistance: 0.5 });
|
|
295
|
+
// x-axis: distance ≈ 0, xy-axis: distance ≈ 0.29
|
|
296
|
+
// y-axis: distance ≈ 1 (excluded)
|
|
297
|
+
expect(results.length).toBe(2);
|
|
298
|
+
expect(results[0].key).toBe("x-axis");
|
|
299
|
+
expect(results[1].key).toBe("xy-axis");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("query maxDistance works with iterable mode", async () => {
|
|
303
|
+
const db = await VectorDB.open({
|
|
304
|
+
dimensions: 4,
|
|
305
|
+
storage,
|
|
306
|
+
wasmBinary,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
db.set("x-axis", [1, 0, 0, 0]);
|
|
310
|
+
db.set("y-axis", [0, 1, 0, 0]);
|
|
311
|
+
db.set("xy-axis", [1, 1, 0, 0]);
|
|
312
|
+
|
|
313
|
+
const results = [...db.query([1, 0, 0, 0], { maxDistance: 0.5, iterable: true })];
|
|
314
|
+
expect(results.length).toBe(2);
|
|
315
|
+
expect(results[0].key).toBe("x-axis");
|
|
316
|
+
expect(results[1].key).toBe("xy-axis");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("query topK defaults to Infinity (returns all results)", async () => {
|
|
320
|
+
const db = await VectorDB.open({
|
|
321
|
+
dimensions: 4,
|
|
322
|
+
storage,
|
|
323
|
+
wasmBinary,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < 10; i++) {
|
|
327
|
+
const vec = [0, 0, 0, 0];
|
|
328
|
+
vec[i % 4] = 1;
|
|
329
|
+
db.set(`v${i}`, vec);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Without topK, all 10 results should be returned
|
|
333
|
+
const results = db.query([1, 0, 0, 0]);
|
|
334
|
+
expect(results.length).toBe(10);
|
|
335
|
+
});
|
|
336
|
+
|
|
282
337
|
// --- flush and persistence ---
|
|
283
338
|
it("flush persists data and reopen loads it", async () => {
|
|
284
339
|
const db1 = await VectorDB.open({
|
package/src/lib/index.ts
CHANGED
|
@@ -9,12 +9,5 @@ export { VectorCapacityExceededError } from "./errors";
|
|
|
9
9
|
export type { ResultItem } from "./result-set";
|
|
10
10
|
export { InMemoryStorageProvider, OPFSStorageProvider } from "./storage";
|
|
11
11
|
export type { StorageProvider } from "./storage";
|
|
12
|
-
export type {
|
|
13
|
-
IterableQueryOptions,
|
|
14
|
-
OpenOptions,
|
|
15
|
-
OpenOptionsInternal,
|
|
16
|
-
QueryOptions,
|
|
17
|
-
SetOptions,
|
|
18
|
-
VectorInput,
|
|
19
|
-
} from "./types";
|
|
12
|
+
export type { OpenOptions, OpenOptionsInternal, QueryOptions, SetOptions, VectorInput } from "./types";
|
|
20
13
|
export { VectorDB as DB } from "./vector-db";
|
package/src/lib/result-set.ts
CHANGED
|
@@ -6,20 +6,29 @@
|
|
|
6
6
|
* 1. topKResults — eagerly materializes a ResultItem[] (default query path)
|
|
7
7
|
* 2. iterableResults — returns a lazy Iterable<ResultItem> where keys are
|
|
8
8
|
* resolved only as each item is consumed (for pagination / streaming)
|
|
9
|
+
*
|
|
10
|
+
* Distance is defined as `1 - dotProduct`. For normalized vectors (the default),
|
|
11
|
+
* this equals cosine distance, ranging from 0 (identical) to 2 (opposite).
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
export interface ResultItem {
|
|
12
15
|
key: string;
|
|
13
|
-
|
|
16
|
+
distance: number;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export type KeyResolver = (index: number) => string;
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
|
-
* Sort
|
|
22
|
+
* Sort by ascending distance and return the top K results as a plain array.
|
|
20
23
|
* All keys are resolved eagerly.
|
|
24
|
+
* If maxDistance is provided, results with distance > maxDistance are excluded.
|
|
21
25
|
*/
|
|
22
|
-
export function topKResults(
|
|
26
|
+
export function topKResults(
|
|
27
|
+
scores: Float32Array,
|
|
28
|
+
resolveKey: KeyResolver,
|
|
29
|
+
topK: number,
|
|
30
|
+
maxDistance?: number,
|
|
31
|
+
): ResultItem[] {
|
|
23
32
|
const n = scores.length;
|
|
24
33
|
if (n === 0) return [];
|
|
25
34
|
|
|
@@ -28,23 +37,32 @@ export function topKResults(scores: Float32Array, resolveKey: KeyResolver, topK:
|
|
|
28
37
|
indices.sort((a, b) => scores[b] - scores[a]);
|
|
29
38
|
|
|
30
39
|
const k = Math.min(topK, n);
|
|
31
|
-
const results: ResultItem[] =
|
|
40
|
+
const results: ResultItem[] = [];
|
|
32
41
|
for (let i = 0; i < k; i++) {
|
|
33
42
|
const idx = indices[i];
|
|
34
|
-
|
|
43
|
+
const distance = 1 - scores[idx];
|
|
44
|
+
if (maxDistance !== undefined && distance > maxDistance) break;
|
|
45
|
+
results.push({ key: resolveKey(idx), distance });
|
|
35
46
|
}
|
|
36
47
|
return results;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
|
-
* Sort
|
|
51
|
+
* Sort by ascending distance and return a lazy iterable over the top K results.
|
|
41
52
|
* Keys are resolved only when each item is consumed, saving allocations
|
|
42
53
|
* when the caller iterates partially (e.g., pagination).
|
|
43
54
|
*
|
|
55
|
+
* If maxDistance is provided, iteration stops when distance > maxDistance.
|
|
56
|
+
*
|
|
44
57
|
* The returned iterable is re-iterable — each call to [Symbol.iterator]()
|
|
45
58
|
* produces a fresh cursor over the same pre-sorted data.
|
|
46
59
|
*/
|
|
47
|
-
export function iterableResults(
|
|
60
|
+
export function iterableResults(
|
|
61
|
+
scores: Float32Array,
|
|
62
|
+
resolveKey: KeyResolver,
|
|
63
|
+
topK: number,
|
|
64
|
+
maxDistance?: number,
|
|
65
|
+
): Iterable<ResultItem> {
|
|
48
66
|
const n = scores.length;
|
|
49
67
|
if (n === 0) return [];
|
|
50
68
|
|
|
@@ -61,9 +79,11 @@ export function iterableResults(scores: Float32Array, resolveKey: KeyResolver, t
|
|
|
61
79
|
next(): IteratorResult<ResultItem> {
|
|
62
80
|
if (i >= k) return { done: true, value: undefined };
|
|
63
81
|
const idx = indices[i++];
|
|
82
|
+
const distance = 1 - scores[idx];
|
|
83
|
+
if (maxDistance !== undefined && distance > maxDistance) return { done: true, value: undefined };
|
|
64
84
|
return {
|
|
65
85
|
done: false,
|
|
66
|
-
value: { key: resolveKey(idx),
|
|
86
|
+
value: { key: resolveKey(idx), distance },
|
|
67
87
|
};
|
|
68
88
|
},
|
|
69
89
|
};
|
package/src/lib/simd-binary.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED - Do not edit. Run: npx tsx scripts/compile-wat.ts
|
|
2
|
-
const SIMD_WASM_BASE64 = "AGFzbQEAAAABDgJgAn9/AGAFf39/
|
|
2
|
+
const SIMD_WASM_BASE64 = "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";
|
|
3
3
|
|
|
4
4
|
export function getSimdWasmBinary(): Uint8Array {
|
|
5
5
|
const binaryString = atob(SIMD_WASM_BASE64);
|