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.
@@ -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].score).toBeCloseTo(1.0, 2);
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].score).toBeGreaterThan(0);
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].score).toBeCloseTo(0.0, 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; score: number }[] = [];
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 score high
277
- expect(results[0].score).toBeCloseTo(1.0, 2);
278
- expect(results[1].score).toBeCloseTo(1.0, 2);
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";
@@ -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
- score: number;
16
+ distance: number;
14
17
  }
15
18
 
16
19
  export type KeyResolver = (index: number) => string;
17
20
 
18
21
  /**
19
- * Sort scores descending and return the top K results as a plain array.
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(scores: Float32Array, resolveKey: KeyResolver, topK: number): ResultItem[] {
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[] = new Array(k);
40
+ const results: ResultItem[] = [];
32
41
  for (let i = 0; i < k; i++) {
33
42
  const idx = indices[i];
34
- results[i] = { key: resolveKey(idx), score: scores[idx] };
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 scores descending and return a lazy iterable over the top K results.
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(scores: Float32Array, resolveKey: KeyResolver, topK: number): Iterable<ResultItem> {
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), score: scores[idx] },
86
+ value: { key: resolveKey(idx), distance },
67
87
  };
68
88
  },
69
89
  };
@@ -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/f38AAg8BA2VudgZtZW1vcnkCAAEDAwIAAQcaAglub3JtYWxpemUAAApzZWFyY2hfYWxsAAEKsgQCtQIFAX8BewN9AXsDf/0MAAAAAAAAAAAAAAAAAAAAACEDIAFBfHEhCEEAIQICQANAIAIgCE8NASAAIAJBAnRqIQogAyAK/QAEACAK/QAEAP3mAf3kASEDIAJBBGohAgwACwsgA/0fACAD/R8BkiAD/R8CIAP9HwOSkiEEIAghCQJAA0AgCSABTw0BIAAgCUECdGohCiAEIAoqAgAgCioCAJSSIQQgCUEBaiEJDAALCyAEkSEFIAVDAAAAAFsEQA8LQwAAgD8gBZUhBiAG/RMhB0EAIQICQANAIAIgCE8NASAAIAJBAnRqIQogCiAK/QAEACAH/eYB/QsEACACQQRqIQIMAAsLIAghCQJAA0AgCSABTw0BIAAgCUECdGohCiAKIAoqAgAgBpQ4AgAgCUEBaiEJDAALCwv4AQQCfwF7AX0GfyAEQXxxIQogBEECdCEOQQAhBQJAA0AgBSADTw0BIAEgBSAObGohCf0MAAAAAAAAAAAAAAAAAAAAACEHQQAhBgJAA0AgBiAKTw0BIAAgBkECdGohDCAJIAZBAnRqIQ0gByAM/QAEACAN/QAEAP3mAf3kASEHIAZBBGohBgwACwsgB/0fACAH/R8BkiAH/R8CIAf9HwOSkiEIIAohCwJAA0AgCyAETw0BIAAgC0ECdGohDCAJIAtBAnRqIQ0gCCAMKgIAIA0qAgCUkiEIIAtBAWohCwwACwsgAiAFQQJ0aiAIOAIAIAVBAWohBQwACwsL";
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);