eigen-db 4.3.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 +8 -0
- package/README.md +74 -23
- package/dist/compute.d.ts +20 -0
- package/dist/eigen-db.js +228 -173
- 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/errors.d.ts +7 -0
- package/dist/index.d.ts +12 -0
- package/dist/lexicon.d.ts +28 -0
- package/dist/memory-manager.d.ts +68 -0
- package/dist/result-set.d.ts +46 -0
- package/dist/simd-binary.d.ts +1 -0
- package/dist/storage.d.ts +38 -0
- package/dist/types.d.ts +48 -0
- package/dist/vector-db.d.ts +131 -0
- package/dist/wasm-compute.d.ts +13 -0
- package/package.json +4 -4
- package/src/lib/__tests__/result-set.test.ts +146 -27
- package/src/lib/__tests__/vector-db.test.ts +476 -4
- package/src/lib/result-set.ts +55 -24
- package/src/lib/types.ts +5 -1
- package/src/lib/vector-db.ts +99 -20
|
@@ -94,6 +94,16 @@ describe("VectorDB", () => {
|
|
|
94
94
|
expect(db.size).toBe(0);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it("exposes dimensions property", async () => {
|
|
98
|
+
const db = await VectorDB.open({
|
|
99
|
+
dimensions: 4,
|
|
100
|
+
storage,
|
|
101
|
+
wasmBinary,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(db.dimensions).toBe(4);
|
|
105
|
+
});
|
|
106
|
+
|
|
97
107
|
// --- set and get ---
|
|
98
108
|
it("stores and retrieves a vector by key", async () => {
|
|
99
109
|
const db = await VectorDB.open({
|
|
@@ -238,7 +248,7 @@ describe("VectorDB", () => {
|
|
|
238
248
|
expect(results[2].similarity).toBeCloseTo(0.0, 2);
|
|
239
249
|
});
|
|
240
250
|
|
|
241
|
-
it("query respects
|
|
251
|
+
it("query respects limit option", async () => {
|
|
242
252
|
const db = await VectorDB.open({
|
|
243
253
|
dimensions: 4,
|
|
244
254
|
storage,
|
|
@@ -249,7 +259,7 @@ describe("VectorDB", () => {
|
|
|
249
259
|
db.set("b", [0, 1, 0, 0]);
|
|
250
260
|
db.set("c", [0, 0, 1, 0]);
|
|
251
261
|
|
|
252
|
-
const results = db.query([1, 0, 0, 0], {
|
|
262
|
+
const results = db.query([1, 0, 0, 0], { limit: 2 });
|
|
253
263
|
expect(results.length).toBe(2);
|
|
254
264
|
});
|
|
255
265
|
|
|
@@ -361,7 +371,7 @@ describe("VectorDB", () => {
|
|
|
361
371
|
expect(results[1].key).toBe("xy-axis");
|
|
362
372
|
});
|
|
363
373
|
|
|
364
|
-
it("query
|
|
374
|
+
it("query limit defaults to Infinity (returns all results)", async () => {
|
|
365
375
|
const db = await VectorDB.open({
|
|
366
376
|
dimensions: 4,
|
|
367
377
|
storage,
|
|
@@ -374,11 +384,195 @@ describe("VectorDB", () => {
|
|
|
374
384
|
db.set(`v${i}`, vec);
|
|
375
385
|
}
|
|
376
386
|
|
|
377
|
-
// Without
|
|
387
|
+
// Without limit, all 10 results should be returned
|
|
378
388
|
const results = db.query([1, 0, 0, 0]);
|
|
379
389
|
expect(results.length).toBe(10);
|
|
380
390
|
});
|
|
381
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
|
+
|
|
382
576
|
// --- flush and persistence ---
|
|
383
577
|
it("flush persists data and reopen loads it", async () => {
|
|
384
578
|
const db1 = await VectorDB.open({
|
|
@@ -838,6 +1032,284 @@ describe("VectorDB", () => {
|
|
|
838
1032
|
expect(db2.get("gamma")![2]).toBeCloseTo(1);
|
|
839
1033
|
});
|
|
840
1034
|
|
|
1035
|
+
// --- has ---
|
|
1036
|
+
it("has returns true for existing key", async () => {
|
|
1037
|
+
const db = await VectorDB.open({
|
|
1038
|
+
dimensions: 4,
|
|
1039
|
+
normalize: false,
|
|
1040
|
+
storage,
|
|
1041
|
+
wasmBinary,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1045
|
+
expect(db.has("a")).toBe(true);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it("has returns false for non-existent key", async () => {
|
|
1049
|
+
const db = await VectorDB.open({
|
|
1050
|
+
dimensions: 4,
|
|
1051
|
+
storage,
|
|
1052
|
+
wasmBinary,
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
expect(db.has("nonexistent")).toBe(false);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it("has throws on closed database", async () => {
|
|
1059
|
+
const db = await VectorDB.open({
|
|
1060
|
+
dimensions: 4,
|
|
1061
|
+
storage,
|
|
1062
|
+
wasmBinary,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
await db.close();
|
|
1066
|
+
expect(() => db.has("a")).toThrow("closed");
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// --- delete ---
|
|
1070
|
+
it("delete removes an existing entry and returns true", async () => {
|
|
1071
|
+
const db = await VectorDB.open({
|
|
1072
|
+
dimensions: 4,
|
|
1073
|
+
normalize: false,
|
|
1074
|
+
storage,
|
|
1075
|
+
wasmBinary,
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1079
|
+
db.set("b", [0, 1, 0, 0]);
|
|
1080
|
+
expect(db.size).toBe(2);
|
|
1081
|
+
|
|
1082
|
+
const result = db.delete("a");
|
|
1083
|
+
expect(result).toBe(true);
|
|
1084
|
+
expect(db.size).toBe(1);
|
|
1085
|
+
expect(db.get("a")).toBeUndefined();
|
|
1086
|
+
expect(db.has("a")).toBe(false);
|
|
1087
|
+
expect(db.get("b")).toBeDefined();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it("delete returns false for non-existent key", async () => {
|
|
1091
|
+
const db = await VectorDB.open({
|
|
1092
|
+
dimensions: 4,
|
|
1093
|
+
storage,
|
|
1094
|
+
wasmBinary,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
expect(db.delete("nonexistent")).toBe(false);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("delete last entry leaves empty database", async () => {
|
|
1101
|
+
const db = await VectorDB.open({
|
|
1102
|
+
dimensions: 4,
|
|
1103
|
+
normalize: false,
|
|
1104
|
+
storage,
|
|
1105
|
+
wasmBinary,
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
db.set("only", [1, 2, 3, 4]);
|
|
1109
|
+
db.delete("only");
|
|
1110
|
+
expect(db.size).toBe(0);
|
|
1111
|
+
expect(db.get("only")).toBeUndefined();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it("delete preserves remaining entries and query works", async () => {
|
|
1115
|
+
const db = await VectorDB.open({
|
|
1116
|
+
dimensions: 4,
|
|
1117
|
+
storage,
|
|
1118
|
+
wasmBinary,
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
db.set("x-axis", [1, 0, 0, 0]);
|
|
1122
|
+
db.set("y-axis", [0, 1, 0, 0]);
|
|
1123
|
+
db.set("z-axis", [0, 0, 1, 0]);
|
|
1124
|
+
|
|
1125
|
+
db.delete("y-axis");
|
|
1126
|
+
expect(db.size).toBe(2);
|
|
1127
|
+
|
|
1128
|
+
const results = db.query([1, 0, 0, 0]);
|
|
1129
|
+
expect(results.length).toBe(2);
|
|
1130
|
+
expect(results[0].key).toBe("x-axis");
|
|
1131
|
+
expect(results.find((r) => r.key === "y-axis")).toBeUndefined();
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
it("delete then set reuses the database correctly", async () => {
|
|
1135
|
+
const db = await VectorDB.open({
|
|
1136
|
+
dimensions: 4,
|
|
1137
|
+
normalize: false,
|
|
1138
|
+
storage,
|
|
1139
|
+
wasmBinary,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1143
|
+
db.set("b", [0, 1, 0, 0]);
|
|
1144
|
+
db.delete("a");
|
|
1145
|
+
|
|
1146
|
+
db.set("c", [0, 0, 1, 0]);
|
|
1147
|
+
expect(db.size).toBe(2);
|
|
1148
|
+
expect(db.get("a")).toBeUndefined();
|
|
1149
|
+
expect(db.get("b")![1]).toBeCloseTo(1);
|
|
1150
|
+
expect(db.get("c")![2]).toBeCloseTo(1);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it("delete persists correctly after flush", async () => {
|
|
1154
|
+
const db1 = await VectorDB.open({
|
|
1155
|
+
dimensions: 4,
|
|
1156
|
+
normalize: false,
|
|
1157
|
+
storage,
|
|
1158
|
+
wasmBinary,
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
db1.set("a", [1, 0, 0, 0]);
|
|
1162
|
+
db1.set("b", [0, 1, 0, 0]);
|
|
1163
|
+
db1.delete("a");
|
|
1164
|
+
await db1.flush();
|
|
1165
|
+
|
|
1166
|
+
const db2 = await VectorDB.open({
|
|
1167
|
+
dimensions: 4,
|
|
1168
|
+
normalize: false,
|
|
1169
|
+
storage,
|
|
1170
|
+
wasmBinary,
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
expect(db2.size).toBe(1);
|
|
1174
|
+
expect(db2.get("a")).toBeUndefined();
|
|
1175
|
+
expect(db2.get("b")![1]).toBeCloseTo(1);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it("delete throws on closed database", async () => {
|
|
1179
|
+
const db = await VectorDB.open({
|
|
1180
|
+
dimensions: 4,
|
|
1181
|
+
storage,
|
|
1182
|
+
wasmBinary,
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
await db.close();
|
|
1186
|
+
expect(() => db.delete("a")).toThrow("closed");
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// --- keys ---
|
|
1190
|
+
it("keys returns an iterable of all keys", async () => {
|
|
1191
|
+
const db = await VectorDB.open({
|
|
1192
|
+
dimensions: 4,
|
|
1193
|
+
normalize: false,
|
|
1194
|
+
storage,
|
|
1195
|
+
wasmBinary,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1199
|
+
db.set("b", [0, 1, 0, 0]);
|
|
1200
|
+
db.set("c", [0, 0, 1, 0]);
|
|
1201
|
+
|
|
1202
|
+
const keys = [...db.keys()];
|
|
1203
|
+
expect(keys).toHaveLength(3);
|
|
1204
|
+
expect(keys).toContain("a");
|
|
1205
|
+
expect(keys).toContain("b");
|
|
1206
|
+
expect(keys).toContain("c");
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it("keys returns empty iterable for empty database", async () => {
|
|
1210
|
+
const db = await VectorDB.open({
|
|
1211
|
+
dimensions: 4,
|
|
1212
|
+
storage,
|
|
1213
|
+
wasmBinary,
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
expect([...db.keys()]).toEqual([]);
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it("keys throws on closed database", async () => {
|
|
1220
|
+
const db = await VectorDB.open({
|
|
1221
|
+
dimensions: 4,
|
|
1222
|
+
storage,
|
|
1223
|
+
wasmBinary,
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
await db.close();
|
|
1227
|
+
expect(() => db.keys()).toThrow("closed");
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// --- entries ---
|
|
1231
|
+
it("entries returns an iterable of [key, value] pairs", async () => {
|
|
1232
|
+
const db = await VectorDB.open({
|
|
1233
|
+
dimensions: 4,
|
|
1234
|
+
normalize: false,
|
|
1235
|
+
storage,
|
|
1236
|
+
wasmBinary,
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1240
|
+
db.set("b", [0, 1, 0, 0]);
|
|
1241
|
+
|
|
1242
|
+
const entries = [...db.entries()];
|
|
1243
|
+
expect(entries).toHaveLength(2);
|
|
1244
|
+
|
|
1245
|
+
const aEntry = entries.find(([key]) => key === "a");
|
|
1246
|
+
expect(aEntry).toBeDefined();
|
|
1247
|
+
expect(aEntry![1][0]).toBeCloseTo(1);
|
|
1248
|
+
|
|
1249
|
+
const bEntry = entries.find(([key]) => key === "b");
|
|
1250
|
+
expect(bEntry).toBeDefined();
|
|
1251
|
+
expect(bEntry![1][1]).toBeCloseTo(1);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it("entries returns empty iterable for empty database", async () => {
|
|
1255
|
+
const db = await VectorDB.open({
|
|
1256
|
+
dimensions: 4,
|
|
1257
|
+
storage,
|
|
1258
|
+
wasmBinary,
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
expect([...db.entries()]).toEqual([]);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it("entries throws on closed database", async () => {
|
|
1265
|
+
const db = await VectorDB.open({
|
|
1266
|
+
dimensions: 4,
|
|
1267
|
+
storage,
|
|
1268
|
+
wasmBinary,
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
await db.close();
|
|
1272
|
+
expect(() => db.entries()).toThrow("closed");
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// --- Symbol.iterator ---
|
|
1276
|
+
it("supports spread operator via Symbol.iterator", async () => {
|
|
1277
|
+
const db = await VectorDB.open({
|
|
1278
|
+
dimensions: 4,
|
|
1279
|
+
normalize: false,
|
|
1280
|
+
storage,
|
|
1281
|
+
wasmBinary,
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1285
|
+
db.set("b", [0, 1, 0, 0]);
|
|
1286
|
+
|
|
1287
|
+
const spread = [...db];
|
|
1288
|
+
expect(spread).toHaveLength(2);
|
|
1289
|
+
|
|
1290
|
+
// Same as entries()
|
|
1291
|
+
const entries = [...db.entries()];
|
|
1292
|
+
expect(spread).toEqual(entries);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it("supports for-of iteration", async () => {
|
|
1296
|
+
const db = await VectorDB.open({
|
|
1297
|
+
dimensions: 4,
|
|
1298
|
+
normalize: false,
|
|
1299
|
+
storage,
|
|
1300
|
+
wasmBinary,
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
db.set("a", [1, 0, 0, 0]);
|
|
1304
|
+
db.set("b", [0, 1, 0, 0]);
|
|
1305
|
+
|
|
1306
|
+
const collected: [string, number[]][] = [];
|
|
1307
|
+
for (const entry of db) {
|
|
1308
|
+
collected.push(entry);
|
|
1309
|
+
}
|
|
1310
|
+
expect(collected).toHaveLength(2);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
841
1313
|
it("import works correctly with single-byte stream chunks", async () => {
|
|
842
1314
|
const db1 = await VectorDB.open({
|
|
843
1315
|
dimensions: 4,
|
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[]. */
|