eigen-db 4.4.0 → 5.0.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.
@@ -248,7 +248,7 @@ describe("VectorDB", () => {
248
248
  expect(results[2].similarity).toBeCloseTo(0.0, 2);
249
249
  });
250
250
 
251
- it("query respects topK option", async () => {
251
+ it("query respects limit option", async () => {
252
252
  const db = await VectorDB.open({
253
253
  dimensions: 4,
254
254
  storage,
@@ -259,7 +259,7 @@ describe("VectorDB", () => {
259
259
  db.set("b", [0, 1, 0, 0]);
260
260
  db.set("c", [0, 0, 1, 0]);
261
261
 
262
- const results = db.query([1, 0, 0, 0], { topK: 2 });
262
+ const results = db.query([1, 0, 0, 0], { limit: 2 });
263
263
  expect(results.length).toBe(2);
264
264
  });
265
265
 
@@ -371,7 +371,7 @@ describe("VectorDB", () => {
371
371
  expect(results[1].key).toBe("xy-axis");
372
372
  });
373
373
 
374
- it("query topK defaults to Infinity (returns all results)", async () => {
374
+ it("query limit defaults to Infinity (returns all results)", async () => {
375
375
  const db = await VectorDB.open({
376
376
  dimensions: 4,
377
377
  storage,
@@ -384,11 +384,195 @@ describe("VectorDB", () => {
384
384
  db.set(`v${i}`, vec);
385
385
  }
386
386
 
387
- // Without topK, all 10 results should be returned
387
+ // Without limit, all 10 results should be returned
388
388
  const results = db.query([1, 0, 0, 0]);
389
389
  expect(results.length).toBe(10);
390
390
  });
391
391
 
392
+ it("query with order ascend returns least similar first", async () => {
393
+ const db = await VectorDB.open({
394
+ dimensions: 4,
395
+ storage,
396
+ wasmBinary,
397
+ });
398
+
399
+ db.set("x-axis", [1, 0, 0, 0]);
400
+ db.set("y-axis", [0, 1, 0, 0]);
401
+ db.set("xy-axis", [1, 1, 0, 0]);
402
+
403
+ const results = db.query([1, 0, 0, 0], { order: "ascend" });
404
+ expect(results.length).toBe(3);
405
+ // y-axis (similarity ≈ 0) should be first in ascending order
406
+ expect(results[0].key).toBe("y-axis");
407
+ expect(results[0].similarity).toBeCloseTo(0.0, 2);
408
+ // x-axis (similarity ≈ 1) should be last
409
+ expect(results[2].key).toBe("x-axis");
410
+ expect(results[2].similarity).toBeCloseTo(1.0, 2);
411
+ });
412
+
413
+ it("query with order ascend and limit returns bottomK", async () => {
414
+ const db = await VectorDB.open({
415
+ dimensions: 4,
416
+ storage,
417
+ wasmBinary,
418
+ });
419
+
420
+ db.set("x-axis", [1, 0, 0, 0]);
421
+ db.set("y-axis", [0, 1, 0, 0]);
422
+ db.set("xy-axis", [1, 1, 0, 0]);
423
+
424
+ const results = db.query([1, 0, 0, 0], { order: "ascend", limit: 1 });
425
+ expect(results.length).toBe(1);
426
+ expect(results[0].key).toBe("y-axis");
427
+ });
428
+
429
+ it("query respects maxSimilarity option", async () => {
430
+ const db = await VectorDB.open({
431
+ dimensions: 4,
432
+ storage,
433
+ wasmBinary,
434
+ });
435
+
436
+ db.set("x-axis", [1, 0, 0, 0]);
437
+ db.set("y-axis", [0, 1, 0, 0]);
438
+ db.set("xy-axis", [1, 1, 0, 0]);
439
+
440
+ // Only return results with similarity ≤ 0.8 from the x-axis query
441
+ const results = db.query([1, 0, 0, 0], { maxSimilarity: 0.8 });
442
+ // x-axis: similarity ≈ 1 (excluded), xy-axis: similarity ≈ 0.71, y-axis: similarity ≈ 0
443
+ expect(results.length).toBe(2);
444
+ expect(results[0].key).toBe("xy-axis");
445
+ expect(results[1].key).toBe("y-axis");
446
+ });
447
+
448
+ it("query with both minSimilarity and maxSimilarity", async () => {
449
+ const db = await VectorDB.open({
450
+ dimensions: 4,
451
+ storage,
452
+ wasmBinary,
453
+ });
454
+
455
+ db.set("x-axis", [1, 0, 0, 0]);
456
+ db.set("y-axis", [0, 1, 0, 0]);
457
+ db.set("xy-axis", [1, 1, 0, 0]);
458
+
459
+ // Only return results with 0.5 ≤ similarity ≤ 0.8
460
+ const results = db.query([1, 0, 0, 0], { minSimilarity: 0.5, maxSimilarity: 0.8 });
461
+ // xy-axis: similarity ≈ 0.71 (included)
462
+ // x-axis ≈ 1.0 (excluded), y-axis ≈ 0.0 (excluded)
463
+ expect(results.length).toBe(1);
464
+ expect(results[0].key).toBe("xy-axis");
465
+ });
466
+
467
+ it("query maxSimilarity works with iterable mode", async () => {
468
+ const db = await VectorDB.open({
469
+ dimensions: 4,
470
+ storage,
471
+ wasmBinary,
472
+ });
473
+
474
+ db.set("x-axis", [1, 0, 0, 0]);
475
+ db.set("y-axis", [0, 1, 0, 0]);
476
+ db.set("xy-axis", [1, 1, 0, 0]);
477
+
478
+ const results = [...db.query([1, 0, 0, 0], { maxSimilarity: 0.8, iterable: true })];
479
+ expect(results.length).toBe(2);
480
+ expect(results[0].key).toBe("xy-axis");
481
+ expect(results[1].key).toBe("y-axis");
482
+ });
483
+
484
+ it("query order ascend with iterable mode", async () => {
485
+ const db = await VectorDB.open({
486
+ dimensions: 4,
487
+ storage,
488
+ wasmBinary,
489
+ });
490
+
491
+ db.set("x-axis", [1, 0, 0, 0]);
492
+ db.set("y-axis", [0, 1, 0, 0]);
493
+ db.set("xy-axis", [1, 1, 0, 0]);
494
+
495
+ const results = [...db.query([1, 0, 0, 0], { order: "ascend", iterable: true })];
496
+ expect(results.length).toBe(3);
497
+ expect(results[0].key).toBe("y-axis");
498
+ expect(results[2].key).toBe("x-axis");
499
+ });
500
+
501
+ it("query supports full similarity range [-1, 1] with opposite vectors", async () => {
502
+ const db = await VectorDB.open({
503
+ dimensions: 4,
504
+ storage,
505
+ wasmBinary,
506
+ });
507
+
508
+ db.set("same", [1, 0, 0, 0]); // similarity ≈ 1
509
+ db.set("ortho", [0, 1, 0, 0]); // similarity ≈ 0
510
+ db.set("opposite", [-1, 0, 0, 0]); // similarity ≈ -1
511
+
512
+ const results = db.query([1, 0, 0, 0]);
513
+ expect(results.length).toBe(3);
514
+ expect(results[0].key).toBe("same");
515
+ expect(results[0].similarity).toBeCloseTo(1.0, 2);
516
+ expect(results[1].key).toBe("ortho");
517
+ expect(results[1].similarity).toBeCloseTo(0.0, 2);
518
+ expect(results[2].key).toBe("opposite");
519
+ expect(results[2].similarity).toBeCloseTo(-1.0, 2);
520
+ });
521
+
522
+ it("query minSimilarity works with negative values", async () => {
523
+ const db = await VectorDB.open({
524
+ dimensions: 4,
525
+ storage,
526
+ wasmBinary,
527
+ });
528
+
529
+ db.set("same", [1, 0, 0, 0]); // similarity ≈ 1
530
+ db.set("ortho", [0, 1, 0, 0]); // similarity ≈ 0
531
+ db.set("opposite", [-1, 0, 0, 0]); // similarity ≈ -1
532
+
533
+ // minSimilarity = -0.5 should include same and ortho, exclude opposite
534
+ const results = db.query([1, 0, 0, 0], { minSimilarity: -0.5 });
535
+ expect(results.length).toBe(2);
536
+ expect(results[0].key).toBe("same");
537
+ expect(results[1].key).toBe("ortho");
538
+ });
539
+
540
+ it("query maxSimilarity works with negative values", async () => {
541
+ const db = await VectorDB.open({
542
+ dimensions: 4,
543
+ storage,
544
+ wasmBinary,
545
+ });
546
+
547
+ db.set("same", [1, 0, 0, 0]); // similarity ≈ 1
548
+ db.set("ortho", [0, 1, 0, 0]); // similarity ≈ 0
549
+ db.set("opposite", [-1, 0, 0, 0]); // similarity ≈ -1
550
+
551
+ // maxSimilarity = -0.5 should include only opposite
552
+ const results = db.query([1, 0, 0, 0], { maxSimilarity: -0.5 });
553
+ expect(results.length).toBe(1);
554
+ expect(results[0].key).toBe("opposite");
555
+ });
556
+
557
+ it("query ascending order with negative similarities", async () => {
558
+ const db = await VectorDB.open({
559
+ dimensions: 4,
560
+ storage,
561
+ wasmBinary,
562
+ });
563
+
564
+ db.set("same", [1, 0, 0, 0]);
565
+ db.set("ortho", [0, 1, 0, 0]);
566
+ db.set("opposite", [-1, 0, 0, 0]);
567
+
568
+ const results = db.query([1, 0, 0, 0], { order: "ascend" });
569
+ expect(results.length).toBe(3);
570
+ expect(results[0].key).toBe("opposite");
571
+ expect(results[0].similarity).toBeCloseTo(-1.0, 2);
572
+ expect(results[2].key).toBe("same");
573
+ expect(results[2].similarity).toBeCloseTo(1.0, 2);
574
+ });
575
+
392
576
  // --- flush and persistence ---
393
577
  it("flush persists data and reopen loads it", async () => {
394
578
  const db1 = await VectorDB.open({
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Utility functions for sorting scores and producing query results.
5
5
  * Two modes:
6
- * 1. topKResults — eagerly materializes a ResultItem[] (default query path)
6
+ * 1. queryResults — 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
9
  *
@@ -19,41 +19,65 @@ export interface ResultItem {
19
19
 
20
20
  export type KeyResolver = (index: number) => string;
21
21
 
22
+ export interface ResultOptions {
23
+ limit: number;
24
+ order: "ascend" | "descend";
25
+ minSimilarity?: number;
26
+ maxSimilarity?: number;
27
+ }
28
+
29
+ /** Check whether a score falls within the [minSimilarity, maxSimilarity] range. */
30
+ function inRange(similarity: number, minSimilarity?: number, maxSimilarity?: number): boolean {
31
+ if (minSimilarity !== undefined && similarity < minSimilarity) return false;
32
+ if (maxSimilarity !== undefined && similarity > maxSimilarity) return false;
33
+ return true;
34
+ }
35
+
22
36
  /**
23
- * Sort by descending similarity and return the top K results as a plain array.
37
+ * Sort scores and return the top results as a plain array.
24
38
  * All keys are resolved eagerly.
25
- * If minSimilarity is provided, results with similarity < minSimilarity are excluded.
39
+ *
40
+ * `order` controls sort direction:
41
+ * - "descend" (default) — highest similarity first
42
+ * - "ascend" — lowest similarity first
43
+ *
44
+ * Results outside [minSimilarity, maxSimilarity] are excluded.
26
45
  */
27
- export function topKResults(
46
+ export function queryResults(
28
47
  scores: Float32Array,
29
48
  resolveKey: KeyResolver,
30
- topK: number,
31
- minSimilarity?: number,
49
+ options: ResultOptions,
32
50
  ): ResultItem[] {
51
+ const { limit, order, minSimilarity, maxSimilarity } = options;
33
52
  const n = scores.length;
34
53
  if (n === 0) return [];
35
54
 
36
55
  const indices = new Uint32Array(n);
37
56
  for (let i = 0; i < n; i++) indices[i] = i;
38
- indices.sort((a, b) => scores[b] - scores[a]);
39
57
 
40
- const k = Math.min(topK, n);
58
+ if (order === "ascend") {
59
+ indices.sort((a, b) => scores[a] - scores[b]);
60
+ } else {
61
+ indices.sort((a, b) => scores[b] - scores[a]);
62
+ }
63
+
64
+ const k = Math.min(limit, n);
41
65
  const results: ResultItem[] = [];
42
- for (let i = 0; i < k; i++) {
66
+ for (let i = 0; i < n && results.length < k; i++) {
43
67
  const idx = indices[i];
44
68
  const similarity = scores[idx];
45
- if (minSimilarity !== undefined && similarity < minSimilarity) break;
69
+ if (!inRange(similarity, minSimilarity, maxSimilarity)) continue;
46
70
  results.push({ key: resolveKey(idx), similarity });
47
71
  }
48
72
  return results;
49
73
  }
50
74
 
51
75
  /**
52
- * Sort by descending similarity and return a lazy iterable over the top K results.
76
+ * Sort scores and return a lazy iterable over the results.
53
77
  * Keys are resolved only when each item is consumed, saving allocations
54
78
  * when the caller iterates partially (e.g., pagination).
55
79
  *
56
- * If minSimilarity is provided, iteration stops when similarity < minSimilarity.
80
+ * Results outside [minSimilarity, maxSimilarity] are skipped.
57
81
  *
58
82
  * The returned iterable is re-iterable — each call to [Symbol.iterator]()
59
83
  * produces a fresh cursor over the same pre-sorted data.
@@ -61,31 +85,38 @@ export function topKResults(
61
85
  export function iterableResults(
62
86
  scores: Float32Array,
63
87
  resolveKey: KeyResolver,
64
- topK: number,
65
- minSimilarity?: number,
88
+ options: ResultOptions,
66
89
  ): Iterable<ResultItem> {
90
+ const { limit, order, minSimilarity, maxSimilarity } = options;
67
91
  const n = scores.length;
68
92
  if (n === 0) return [];
69
93
 
70
94
  const indices = new Uint32Array(n);
71
95
  for (let i = 0; i < n; i++) indices[i] = i;
72
- indices.sort((a, b) => scores[b] - scores[a]);
73
96
 
74
- const k = Math.min(topK, n);
97
+ if (order === "ascend") {
98
+ indices.sort((a, b) => scores[a] - scores[b]);
99
+ } else {
100
+ indices.sort((a, b) => scores[b] - scores[a]);
101
+ }
75
102
 
76
103
  return {
77
104
  [Symbol.iterator](): Iterator<ResultItem> {
78
105
  let i = 0;
106
+ let emitted = 0;
79
107
  return {
80
108
  next(): IteratorResult<ResultItem> {
81
- if (i >= k) return { done: true, value: undefined };
82
- const idx = indices[i++];
83
- const similarity = scores[idx];
84
- if (minSimilarity !== undefined && similarity < minSimilarity) return { done: true, value: undefined };
85
- return {
86
- done: false,
87
- value: { key: resolveKey(idx), similarity },
88
- };
109
+ while (i < n && emitted < limit) {
110
+ const idx = indices[i++];
111
+ const similarity = scores[idx];
112
+ if (!inRange(similarity, minSimilarity, maxSimilarity)) continue;
113
+ emitted++;
114
+ return {
115
+ done: false,
116
+ value: { key: resolveKey(idx), similarity },
117
+ };
118
+ }
119
+ return { done: true, value: undefined };
89
120
  },
90
121
  };
91
122
  },
package/src/lib/types.ts CHANGED
@@ -38,9 +38,13 @@ export interface SetOptions {
38
38
  */
39
39
  export interface QueryOptions {
40
40
  /** Maximum number of results to return. Defaults to Infinity (all results). */
41
- topK?: number;
41
+ limit?: number;
42
+ /** Result ordering. "ascend" sorts by ascending similarity, "descend" sorts by descending similarity. Defaults to "descend". */
43
+ order?: "ascend" | "descend";
42
44
  /** Minimum similarity threshold (inclusive). Results with similarity < minSimilarity are excluded. */
43
45
  minSimilarity?: number;
46
+ /** Maximum similarity threshold (inclusive). Results with similarity > maxSimilarity are excluded. */
47
+ maxSimilarity?: number;
44
48
  /** Override normalization for this call. */
45
49
  normalize?: boolean;
46
50
  /** When true, returns an Iterable<ResultItem> instead of ResultItem[]. */
@@ -17,7 +17,7 @@ import { VectorCapacityExceededError } from "./errors";
17
17
  import { decodeLexicon, encodeLexicon } from "./lexicon";
18
18
  import { MemoryManager } from "./memory-manager";
19
19
  import type { ResultItem } from "./result-set";
20
- import { iterableResults, topKResults } from "./result-set";
20
+ import { iterableResults, queryResults } from "./result-set";
21
21
  import { getSimdWasmBinary } from "./simd-binary";
22
22
  import type { StorageProvider } from "./storage";
23
23
  import { InMemoryStorageProvider } from "./storage";
@@ -281,8 +281,10 @@ export class VectorDB {
281
281
  query(value: VectorInput, options?: QueryOptions): ResultItem[] | Iterable<ResultItem> {
282
282
  this.assertOpen();
283
283
 
284
- const k = options?.topK ?? Infinity;
284
+ const limit = options?.limit ?? Infinity;
285
+ const order = options?.order ?? "descend";
285
286
  const minSimilarity = options?.minSimilarity;
287
+ const maxSimilarity = options?.maxSimilarity;
286
288
  const iterable = options && "iterable" in options && options.iterable;
287
289
 
288
290
  if (this.size === 0) {
@@ -343,10 +345,12 @@ export class VectorDB {
343
345
  return slotToKey[slotIndex];
344
346
  };
345
347
 
348
+ const resultOptions = { limit, order, minSimilarity, maxSimilarity } as const;
349
+
346
350
  if (iterable) {
347
- return iterableResults(scores, resolveKey, k, minSimilarity);
351
+ return iterableResults(scores, resolveKey, resultOptions);
348
352
  }
349
- return topKResults(scores, resolveKey, k, minSimilarity);
353
+ return queryResults(scores, resolveKey, resultOptions);
350
354
  }
351
355
 
352
356
  /**