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.
- package/CHANGELOG.md +4 -0
- package/README.md +38 -25
- package/dist/eigen-db.js +190 -187
- 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/dist/result-set.d.ts +18 -7
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
- package/src/lib/__tests__/result-set.test.ts +146 -27
- package/src/lib/__tests__/vector-db.test.ts +188 -4
- package/src/lib/result-set.ts +55 -24
- package/src/lib/types.ts +5 -1
- package/src/lib/vector-db.ts +8 -4
|
@@ -248,7 +248,7 @@ describe("VectorDB", () => {
|
|
|
248
248
|
expect(results[2].similarity).toBeCloseTo(0.0, 2);
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
it("query respects
|
|
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], {
|
|
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
|
|
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
|
|
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({
|
package/src/lib/result-set.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utility functions for sorting scores and producing query results.
|
|
5
5
|
* Two modes:
|
|
6
|
-
* 1.
|
|
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
|
|
37
|
+
* Sort scores and return the top results as a plain array.
|
|
24
38
|
* All keys are resolved eagerly.
|
|
25
|
-
*
|
|
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
|
|
46
|
+
export function queryResults(
|
|
28
47
|
scores: Float32Array,
|
|
29
48
|
resolveKey: KeyResolver,
|
|
30
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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[]. */
|
package/src/lib/vector-db.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
351
|
+
return iterableResults(scores, resolveKey, resultOptions);
|
|
348
352
|
}
|
|
349
|
-
return
|
|
353
|
+
return queryResults(scores, resolveKey, resultOptions);
|
|
350
354
|
}
|
|
351
355
|
|
|
352
356
|
/**
|