eigen-db 4.1.0 → 4.3.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 +79 -27
- package/dist/eigen-db.js +317 -195
- 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 +19 -19
- package/src/lib/__tests__/vector-db.test.ts +429 -16
- package/src/lib/memory-manager.ts +8 -0
- package/src/lib/result-set.ts +16 -15
- package/src/lib/simd-binary.ts +1 -1
- package/src/lib/simd-optimized.wat +362 -0
- package/src/lib/simd.wat +42 -248
- package/src/lib/types.ts +4 -6
- package/src/lib/vector-db.ts +241 -9
|
@@ -5,15 +5,15 @@ describe("topKResults", () => {
|
|
|
5
5
|
const keys = ["apple", "banana", "cherry", "date", "elderberry"];
|
|
6
6
|
const resolveKey = (index: number) => keys[index];
|
|
7
7
|
|
|
8
|
-
it("sorts results by
|
|
8
|
+
it("sorts results by descending similarity", () => {
|
|
9
9
|
const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
|
|
10
10
|
const results = topKResults(scores, resolveKey, 5);
|
|
11
11
|
|
|
12
12
|
expect(results).toHaveLength(5);
|
|
13
13
|
expect(results[0].key).toBe("banana");
|
|
14
|
-
expect(results[0].
|
|
14
|
+
expect(results[0].similarity).toBeCloseTo(0.9, 4);
|
|
15
15
|
expect(results[1].key).toBe("date");
|
|
16
|
-
expect(results[1].
|
|
16
|
+
expect(results[1].similarity).toBeCloseTo(0.7, 4);
|
|
17
17
|
expect(results[2].key).toBe("elderberry");
|
|
18
18
|
expect(results[3].key).toBe("apple");
|
|
19
19
|
expect(results[4].key).toBe("cherry");
|
|
@@ -25,7 +25,7 @@ describe("topKResults", () => {
|
|
|
25
25
|
|
|
26
26
|
expect(results).toHaveLength(3);
|
|
27
27
|
expect(results[0].key).toBe("banana");
|
|
28
|
-
expect(results[0].
|
|
28
|
+
expect(results[0].similarity).toBeCloseTo(0.9, 4);
|
|
29
29
|
expect(results[2].key).toBe("elderberry");
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -41,23 +41,23 @@ describe("topKResults", () => {
|
|
|
41
41
|
expect(results).toHaveLength(2);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it("filters results by
|
|
44
|
+
it("filters results by minSimilarity", () => {
|
|
45
45
|
const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
|
|
46
|
-
//
|
|
46
|
+
// similarities: apple=0.3, banana=0.9, cherry=0.1, date=0.7, elderberry=0.5
|
|
47
47
|
const results = topKResults(scores, resolveKey, 5, 0.5);
|
|
48
48
|
|
|
49
49
|
expect(results).toHaveLength(3);
|
|
50
50
|
expect(results[0].key).toBe("banana");
|
|
51
|
-
expect(results[0].
|
|
51
|
+
expect(results[0].similarity).toBeCloseTo(0.9, 4);
|
|
52
52
|
expect(results[1].key).toBe("date");
|
|
53
|
-
expect(results[1].
|
|
53
|
+
expect(results[1].similarity).toBeCloseTo(0.7, 4);
|
|
54
54
|
expect(results[2].key).toBe("elderberry");
|
|
55
|
-
expect(results[2].
|
|
55
|
+
expect(results[2].similarity).toBeCloseTo(0.5, 4);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("
|
|
58
|
+
it("minSimilarity is inclusive", () => {
|
|
59
59
|
const scores = new Float32Array([0.5]);
|
|
60
|
-
//
|
|
60
|
+
// similarity = 0.5
|
|
61
61
|
const results = topKResults(scores, resolveKey, 10, 0.5);
|
|
62
62
|
expect(results).toHaveLength(1);
|
|
63
63
|
});
|
|
@@ -73,13 +73,13 @@ describe("iterableResults", () => {
|
|
|
73
73
|
const keys = ["apple", "banana", "cherry", "date", "elderberry"];
|
|
74
74
|
const resolveKey = (index: number) => keys[index];
|
|
75
75
|
|
|
76
|
-
it("sorts results by
|
|
76
|
+
it("sorts results by descending similarity", () => {
|
|
77
77
|
const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
|
|
78
78
|
const results = [...iterableResults(scores, resolveKey, 5)];
|
|
79
79
|
|
|
80
80
|
expect(results).toHaveLength(5);
|
|
81
81
|
expect(results[0].key).toBe("banana");
|
|
82
|
-
expect(results[0].
|
|
82
|
+
expect(results[0].similarity).toBeCloseTo(0.9, 4);
|
|
83
83
|
expect(results[1].key).toBe("date");
|
|
84
84
|
expect(results[4].key).toBe("cherry");
|
|
85
85
|
});
|
|
@@ -136,13 +136,13 @@ describe("iterableResults", () => {
|
|
|
136
136
|
if (partial.length === 2) break;
|
|
137
137
|
}
|
|
138
138
|
expect(partial).toHaveLength(2);
|
|
139
|
-
expect(partial[0]).toBe("elderberry"); //
|
|
140
|
-
expect(partial[1]).toBe("date"); //
|
|
139
|
+
expect(partial[0]).toBe("elderberry"); // similarity 0.5 (highest)
|
|
140
|
+
expect(partial[1]).toBe("date"); // similarity 0.4
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
it("early stops iteration by
|
|
143
|
+
it("early stops iteration by minSimilarity", () => {
|
|
144
144
|
const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
|
|
145
|
-
//
|
|
145
|
+
// similarities: apple=0.3, banana=0.9, cherry=0.1, date=0.7, elderberry=0.5
|
|
146
146
|
const results = [...iterableResults(scores, resolveKey, 5, 0.5)];
|
|
147
147
|
|
|
148
148
|
expect(results).toHaveLength(3);
|
|
@@ -151,9 +151,9 @@ describe("iterableResults", () => {
|
|
|
151
151
|
expect(results[2].key).toBe("elderberry");
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
it("
|
|
154
|
+
it("minSimilarity is inclusive", () => {
|
|
155
155
|
const scores = new Float32Array([0.5]);
|
|
156
|
-
//
|
|
156
|
+
// similarity = 0.5
|
|
157
157
|
const results = [...iterableResults(scores, resolveKey, 10, 0.5)];
|
|
158
158
|
expect(results).toHaveLength(1);
|
|
159
159
|
});
|
|
@@ -16,6 +16,51 @@ function getWasmBinary(): Promise<Uint8Array> {
|
|
|
16
16
|
return wasmBinaryPromise;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Collect a ReadableStream into a single Uint8Array. */
|
|
20
|
+
async function streamToBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
|
|
21
|
+
const chunks: Uint8Array[] = [];
|
|
22
|
+
const reader = stream.getReader();
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done) break;
|
|
26
|
+
chunks.push(value);
|
|
27
|
+
}
|
|
28
|
+
const totalSize = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
29
|
+
const result = new Uint8Array(totalSize);
|
|
30
|
+
let offset = 0;
|
|
31
|
+
for (const chunk of chunks) {
|
|
32
|
+
result.set(chunk, offset);
|
|
33
|
+
offset += chunk.byteLength;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Create a ReadableStream from a Uint8Array (single chunk). */
|
|
39
|
+
function bytesToStream(data: Uint8Array): ReadableStream<Uint8Array> {
|
|
40
|
+
return new ReadableStream({
|
|
41
|
+
start(controller) {
|
|
42
|
+
controller.enqueue(data);
|
|
43
|
+
controller.close();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Create a ReadableStream from a Uint8Array, delivering it in small chunks. */
|
|
49
|
+
function bytesToChunkedStream(data: Uint8Array, chunkSize: number): ReadableStream<Uint8Array> {
|
|
50
|
+
let offset = 0;
|
|
51
|
+
return new ReadableStream({
|
|
52
|
+
pull(controller) {
|
|
53
|
+
if (offset >= data.byteLength) {
|
|
54
|
+
controller.close();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const end = Math.min(offset + chunkSize, data.byteLength);
|
|
58
|
+
controller.enqueue(data.subarray(offset, end));
|
|
59
|
+
offset = end;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
19
64
|
describe("VectorDB", () => {
|
|
20
65
|
let storage: InMemoryStorageProvider;
|
|
21
66
|
|
|
@@ -180,17 +225,17 @@ describe("VectorDB", () => {
|
|
|
180
225
|
const results = db.query([1, 0, 0, 0]);
|
|
181
226
|
expect(results.length).toBe(3);
|
|
182
227
|
|
|
183
|
-
// x-axis should be the best match (identical direction,
|
|
228
|
+
// x-axis should be the best match (identical direction, similarity ≈ 1)
|
|
184
229
|
expect(results[0].key).toBe("x-axis");
|
|
185
|
-
expect(results[0].
|
|
230
|
+
expect(results[0].similarity).toBeCloseTo(1.0, 2);
|
|
186
231
|
|
|
187
232
|
// xy-axis should be second (partially aligned)
|
|
188
233
|
expect(results[1].key).toBe("xy-axis");
|
|
189
|
-
expect(results[1].
|
|
234
|
+
expect(results[1].similarity).toBeGreaterThan(0);
|
|
190
235
|
|
|
191
|
-
// y-axis should be last (orthogonal,
|
|
236
|
+
// y-axis should be last (orthogonal, similarity ≈ 0)
|
|
192
237
|
expect(results[2].key).toBe("y-axis");
|
|
193
|
-
expect(results[2].
|
|
238
|
+
expect(results[2].similarity).toBeCloseTo(0.0, 2);
|
|
194
239
|
});
|
|
195
240
|
|
|
196
241
|
it("query respects topK option", async () => {
|
|
@@ -251,7 +296,7 @@ describe("VectorDB", () => {
|
|
|
251
296
|
expect(all).toHaveLength(5);
|
|
252
297
|
|
|
253
298
|
// Partial iteration (simulate pagination)
|
|
254
|
-
const page: { key: string;
|
|
299
|
+
const page: { key: string; similarity: number }[] = [];
|
|
255
300
|
for (const item of results) {
|
|
256
301
|
page.push(item);
|
|
257
302
|
if (page.length === 2) break;
|
|
@@ -273,13 +318,13 @@ describe("VectorDB", () => {
|
|
|
273
318
|
db.set("point", [0, 1, 0, 0]);
|
|
274
319
|
|
|
275
320
|
const results = db.query([0, 1, 0, 0]);
|
|
276
|
-
// Both 'point' and 'other' are now along y-axis, so both should have
|
|
277
|
-
expect(results[0].
|
|
278
|
-
expect(results[1].
|
|
321
|
+
// Both 'point' and 'other' are now along y-axis, so both should have similarity ≈ 1
|
|
322
|
+
expect(results[0].similarity).toBeCloseTo(1.0, 2);
|
|
323
|
+
expect(results[1].similarity).toBeCloseTo(1.0, 2);
|
|
279
324
|
expect(db.size).toBe(2);
|
|
280
325
|
});
|
|
281
326
|
|
|
282
|
-
it("query respects
|
|
327
|
+
it("query respects minSimilarity option", async () => {
|
|
283
328
|
const db = await VectorDB.open({
|
|
284
329
|
dimensions: 4,
|
|
285
330
|
storage,
|
|
@@ -290,16 +335,16 @@ describe("VectorDB", () => {
|
|
|
290
335
|
db.set("y-axis", [0, 1, 0, 0]);
|
|
291
336
|
db.set("xy-axis", [1, 1, 0, 0]);
|
|
292
337
|
|
|
293
|
-
// Only return results with
|
|
294
|
-
const results = db.query([1, 0, 0, 0], {
|
|
295
|
-
// x-axis:
|
|
296
|
-
// y-axis:
|
|
338
|
+
// Only return results with similarity ≥ 0.5 from the x-axis query
|
|
339
|
+
const results = db.query([1, 0, 0, 0], { minSimilarity: 0.5 });
|
|
340
|
+
// x-axis: similarity ≈ 1, xy-axis: similarity ≈ 0.71
|
|
341
|
+
// y-axis: similarity ≈ 0 (excluded)
|
|
297
342
|
expect(results.length).toBe(2);
|
|
298
343
|
expect(results[0].key).toBe("x-axis");
|
|
299
344
|
expect(results[1].key).toBe("xy-axis");
|
|
300
345
|
});
|
|
301
346
|
|
|
302
|
-
it("query
|
|
347
|
+
it("query minSimilarity works with iterable mode", async () => {
|
|
303
348
|
const db = await VectorDB.open({
|
|
304
349
|
dimensions: 4,
|
|
305
350
|
storage,
|
|
@@ -310,7 +355,7 @@ describe("VectorDB", () => {
|
|
|
310
355
|
db.set("y-axis", [0, 1, 0, 0]);
|
|
311
356
|
db.set("xy-axis", [1, 1, 0, 0]);
|
|
312
357
|
|
|
313
|
-
const results = [...db.query([1, 0, 0, 0], {
|
|
358
|
+
const results = [...db.query([1, 0, 0, 0], { minSimilarity: 0.5, iterable: true })];
|
|
314
359
|
expect(results.length).toBe(2);
|
|
315
360
|
expect(results[0].key).toBe("x-axis");
|
|
316
361
|
expect(results[1].key).toBe("xy-axis");
|
|
@@ -491,6 +536,334 @@ describe("VectorDB", () => {
|
|
|
491
536
|
const result = db.get("a");
|
|
492
537
|
expect(result![0]).toBeCloseTo(1.0);
|
|
493
538
|
});
|
|
539
|
+
|
|
540
|
+
// --- export and import (streaming) ---
|
|
541
|
+
it("export returns a readable stream of the database", async () => {
|
|
542
|
+
const db = await VectorDB.open({
|
|
543
|
+
dimensions: 4,
|
|
544
|
+
normalize: false,
|
|
545
|
+
storage,
|
|
546
|
+
wasmBinary,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
db.set("a", [1, 2, 3, 4]);
|
|
550
|
+
db.set("b", [5, 6, 7, 8]);
|
|
551
|
+
|
|
552
|
+
const stream = await db.export();
|
|
553
|
+
expect(stream).toBeInstanceOf(ReadableStream);
|
|
554
|
+
|
|
555
|
+
const bytes = await streamToBytes(stream);
|
|
556
|
+
expect(bytes.byteLength).toBeGreaterThan(0);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("export of empty database returns a valid stream", async () => {
|
|
560
|
+
const db = await VectorDB.open({
|
|
561
|
+
dimensions: 4,
|
|
562
|
+
normalize: false,
|
|
563
|
+
storage,
|
|
564
|
+
wasmBinary,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const stream = await db.export();
|
|
568
|
+
const bytes = await streamToBytes(stream);
|
|
569
|
+
expect(bytes.byteLength).toBeGreaterThan(0);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("import restores data from an exported stream", async () => {
|
|
573
|
+
const db1 = await VectorDB.open({
|
|
574
|
+
dimensions: 4,
|
|
575
|
+
normalize: false,
|
|
576
|
+
storage,
|
|
577
|
+
wasmBinary,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
db1.set("alpha", [1, 0, 0, 0]);
|
|
581
|
+
db1.set("beta", [0, 1, 0, 0]);
|
|
582
|
+
|
|
583
|
+
const stream = await db1.export();
|
|
584
|
+
|
|
585
|
+
// Import into a fresh database
|
|
586
|
+
const storage2 = new InMemoryStorageProvider();
|
|
587
|
+
const db2 = await VectorDB.open({
|
|
588
|
+
dimensions: 4,
|
|
589
|
+
normalize: false,
|
|
590
|
+
storage: storage2,
|
|
591
|
+
wasmBinary,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
await db2.import(stream);
|
|
595
|
+
expect(db2.size).toBe(2);
|
|
596
|
+
expect(db2.get("alpha")![0]).toBeCloseTo(1);
|
|
597
|
+
expect(db2.get("beta")![1]).toBeCloseTo(1);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("import overrides existing data", async () => {
|
|
601
|
+
const db1 = await VectorDB.open({
|
|
602
|
+
dimensions: 4,
|
|
603
|
+
normalize: false,
|
|
604
|
+
storage,
|
|
605
|
+
wasmBinary,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
db1.set("a", [1, 0, 0, 0]);
|
|
609
|
+
const stream = await db1.export();
|
|
610
|
+
|
|
611
|
+
// Create db2 with different data
|
|
612
|
+
const storage2 = new InMemoryStorageProvider();
|
|
613
|
+
const db2 = await VectorDB.open({
|
|
614
|
+
dimensions: 4,
|
|
615
|
+
normalize: false,
|
|
616
|
+
storage: storage2,
|
|
617
|
+
wasmBinary,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
db2.set("x", [0, 0, 0, 1]);
|
|
621
|
+
db2.set("y", [0, 0, 1, 0]);
|
|
622
|
+
expect(db2.size).toBe(2);
|
|
623
|
+
|
|
624
|
+
await db2.import(stream);
|
|
625
|
+
expect(db2.size).toBe(1);
|
|
626
|
+
expect(db2.get("a")![0]).toBeCloseTo(1);
|
|
627
|
+
expect(db2.get("x")).toBeUndefined();
|
|
628
|
+
expect(db2.get("y")).toBeUndefined();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("import throws on dimension mismatch", async () => {
|
|
632
|
+
const db1 = await VectorDB.open({
|
|
633
|
+
dimensions: 4,
|
|
634
|
+
normalize: false,
|
|
635
|
+
storage,
|
|
636
|
+
wasmBinary,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
db1.set("a", [1, 0, 0, 0]);
|
|
640
|
+
const bytes = await streamToBytes(await db1.export());
|
|
641
|
+
|
|
642
|
+
const storage2 = new InMemoryStorageProvider();
|
|
643
|
+
const db2 = await VectorDB.open({
|
|
644
|
+
dimensions: 8,
|
|
645
|
+
normalize: false,
|
|
646
|
+
storage: storage2,
|
|
647
|
+
wasmBinary,
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await expect(db2.import(bytesToStream(bytes))).rejects.toThrow("dimension");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("import of empty export clears the database", async () => {
|
|
654
|
+
const db1 = await VectorDB.open({
|
|
655
|
+
dimensions: 4,
|
|
656
|
+
normalize: false,
|
|
657
|
+
storage,
|
|
658
|
+
wasmBinary,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const stream = await db1.export();
|
|
662
|
+
|
|
663
|
+
const storage2 = new InMemoryStorageProvider();
|
|
664
|
+
const db2 = await VectorDB.open({
|
|
665
|
+
dimensions: 4,
|
|
666
|
+
normalize: false,
|
|
667
|
+
storage: storage2,
|
|
668
|
+
wasmBinary,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
db2.set("existing", [1, 0, 0, 0]);
|
|
672
|
+
await db2.import(stream);
|
|
673
|
+
expect(db2.size).toBe(0);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("imported data is queryable", async () => {
|
|
677
|
+
const db1 = await VectorDB.open({
|
|
678
|
+
dimensions: 4,
|
|
679
|
+
storage,
|
|
680
|
+
wasmBinary,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
db1.set("x-axis", [1, 0, 0, 0]);
|
|
684
|
+
db1.set("y-axis", [0, 1, 0, 0]);
|
|
685
|
+
db1.set("xy-axis", [1, 1, 0, 0]);
|
|
686
|
+
|
|
687
|
+
const stream = await db1.export();
|
|
688
|
+
|
|
689
|
+
const storage2 = new InMemoryStorageProvider();
|
|
690
|
+
const db2 = await VectorDB.open({
|
|
691
|
+
dimensions: 4,
|
|
692
|
+
storage: storage2,
|
|
693
|
+
wasmBinary,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await db2.import(stream);
|
|
697
|
+
|
|
698
|
+
const results = db2.query([1, 0, 0, 0]);
|
|
699
|
+
expect(results.length).toBe(3);
|
|
700
|
+
expect(results[0].key).toBe("x-axis");
|
|
701
|
+
expect(results[0].similarity).toBeCloseTo(1.0, 2);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("export and import preserve data after set operations on imported db", async () => {
|
|
705
|
+
const db1 = await VectorDB.open({
|
|
706
|
+
dimensions: 4,
|
|
707
|
+
normalize: false,
|
|
708
|
+
storage,
|
|
709
|
+
wasmBinary,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
db1.set("a", [1, 0, 0, 0]);
|
|
713
|
+
const stream = await db1.export();
|
|
714
|
+
|
|
715
|
+
const storage2 = new InMemoryStorageProvider();
|
|
716
|
+
const db2 = await VectorDB.open({
|
|
717
|
+
dimensions: 4,
|
|
718
|
+
normalize: false,
|
|
719
|
+
storage: storage2,
|
|
720
|
+
wasmBinary,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
await db2.import(stream);
|
|
724
|
+
db2.set("b", [0, 1, 0, 0]);
|
|
725
|
+
|
|
726
|
+
expect(db2.size).toBe(2);
|
|
727
|
+
expect(db2.get("a")![0]).toBeCloseTo(1);
|
|
728
|
+
expect(db2.get("b")![1]).toBeCloseTo(1);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("export throws on closed database", async () => {
|
|
732
|
+
const db = await VectorDB.open({
|
|
733
|
+
dimensions: 4,
|
|
734
|
+
storage,
|
|
735
|
+
wasmBinary,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
await db.close();
|
|
739
|
+
await expect(db.export()).rejects.toThrow("closed");
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("import throws on closed database", async () => {
|
|
743
|
+
const db1 = await VectorDB.open({
|
|
744
|
+
dimensions: 4,
|
|
745
|
+
storage,
|
|
746
|
+
wasmBinary,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const stream = await db1.export();
|
|
750
|
+
|
|
751
|
+
const storage2 = new InMemoryStorageProvider();
|
|
752
|
+
const db2 = await VectorDB.open({
|
|
753
|
+
dimensions: 4,
|
|
754
|
+
storage: storage2,
|
|
755
|
+
wasmBinary,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
await db2.close();
|
|
759
|
+
await expect(db2.import(stream)).rejects.toThrow("closed");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("import throws on invalid stream (bad magic)", async () => {
|
|
763
|
+
const db = await VectorDB.open({
|
|
764
|
+
dimensions: 4,
|
|
765
|
+
storage,
|
|
766
|
+
wasmBinary,
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const badBlob = new Uint8Array(24);
|
|
770
|
+
await expect(db.import(bytesToStream(badBlob))).rejects.toThrow();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("import throws on stream too short for header", async () => {
|
|
774
|
+
const db = await VectorDB.open({
|
|
775
|
+
dimensions: 4,
|
|
776
|
+
storage,
|
|
777
|
+
wasmBinary,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
const tooShort = new Uint8Array(10);
|
|
781
|
+
await expect(db.import(bytesToStream(tooShort))).rejects.toThrow();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("import throws on truncated stream body", async () => {
|
|
785
|
+
const db1 = await VectorDB.open({
|
|
786
|
+
dimensions: 4,
|
|
787
|
+
normalize: false,
|
|
788
|
+
storage,
|
|
789
|
+
wasmBinary,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
db1.set("a", [1, 0, 0, 0]);
|
|
793
|
+
const bytes = await streamToBytes(await db1.export());
|
|
794
|
+
|
|
795
|
+
// Truncate the blob to have valid header but incomplete body
|
|
796
|
+
const truncated = bytes.slice(0, 25);
|
|
797
|
+
|
|
798
|
+
const storage2 = new InMemoryStorageProvider();
|
|
799
|
+
const db2 = await VectorDB.open({
|
|
800
|
+
dimensions: 4,
|
|
801
|
+
normalize: false,
|
|
802
|
+
storage: storage2,
|
|
803
|
+
wasmBinary,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
await expect(db2.import(bytesToStream(truncated))).rejects.toThrow("unexpected end of stream");
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("import works correctly with chunked stream (small chunks)", async () => {
|
|
810
|
+
const db1 = await VectorDB.open({
|
|
811
|
+
dimensions: 4,
|
|
812
|
+
normalize: false,
|
|
813
|
+
storage,
|
|
814
|
+
wasmBinary,
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
db1.set("alpha", [1, 0, 0, 0]);
|
|
818
|
+
db1.set("beta", [0, 1, 0, 0]);
|
|
819
|
+
db1.set("gamma", [0, 0, 1, 0]);
|
|
820
|
+
|
|
821
|
+
const bytes = await streamToBytes(await db1.export());
|
|
822
|
+
|
|
823
|
+
// Feed it back as a stream with tiny 7-byte chunks (crosses header/vector/key boundaries)
|
|
824
|
+
const chunkedStream = bytesToChunkedStream(bytes, 7);
|
|
825
|
+
|
|
826
|
+
const storage2 = new InMemoryStorageProvider();
|
|
827
|
+
const db2 = await VectorDB.open({
|
|
828
|
+
dimensions: 4,
|
|
829
|
+
normalize: false,
|
|
830
|
+
storage: storage2,
|
|
831
|
+
wasmBinary,
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
await db2.import(chunkedStream);
|
|
835
|
+
expect(db2.size).toBe(3);
|
|
836
|
+
expect(db2.get("alpha")![0]).toBeCloseTo(1);
|
|
837
|
+
expect(db2.get("beta")![1]).toBeCloseTo(1);
|
|
838
|
+
expect(db2.get("gamma")![2]).toBeCloseTo(1);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it("import works correctly with single-byte stream chunks", async () => {
|
|
842
|
+
const db1 = await VectorDB.open({
|
|
843
|
+
dimensions: 4,
|
|
844
|
+
normalize: false,
|
|
845
|
+
storage,
|
|
846
|
+
wasmBinary,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
db1.set("k", [1, 2, 3, 4]);
|
|
850
|
+
|
|
851
|
+
const bytes = await streamToBytes(await db1.export());
|
|
852
|
+
const chunkedStream = bytesToChunkedStream(bytes, 1);
|
|
853
|
+
|
|
854
|
+
const storage2 = new InMemoryStorageProvider();
|
|
855
|
+
const db2 = await VectorDB.open({
|
|
856
|
+
dimensions: 4,
|
|
857
|
+
normalize: false,
|
|
858
|
+
storage: storage2,
|
|
859
|
+
wasmBinary,
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
await db2.import(chunkedStream);
|
|
863
|
+
expect(db2.size).toBe(1);
|
|
864
|
+
expect(db2.get("k")![0]).toBeCloseTo(1);
|
|
865
|
+
expect(db2.get("k")![3]).toBeCloseTo(4);
|
|
866
|
+
});
|
|
494
867
|
}
|
|
495
868
|
|
|
496
869
|
it("throws VectorCapacityExceededError when full", async () => {
|
|
@@ -498,4 +871,44 @@ describe("VectorDB", () => {
|
|
|
498
871
|
expect(err).toBeInstanceOf(VectorCapacityExceededError);
|
|
499
872
|
expect(err.message).toContain("100");
|
|
500
873
|
});
|
|
874
|
+
|
|
875
|
+
// --- storage decoupling ---
|
|
876
|
+
it("defaults to in-memory storage when no storage is provided", async () => {
|
|
877
|
+
const db = await VectorDB.open({
|
|
878
|
+
dimensions: 4,
|
|
879
|
+
wasmBinary: null,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
db.set("a", [1, 0, 0, 0]);
|
|
883
|
+
expect(db.size).toBe(1);
|
|
884
|
+
expect(db.get("a")).toBeDefined();
|
|
885
|
+
|
|
886
|
+
// Flush should succeed with default in-memory storage
|
|
887
|
+
await db.flush();
|
|
888
|
+
await db.close();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("accepts an explicit storage provider via options", async () => {
|
|
892
|
+
const customStorage = new InMemoryStorageProvider();
|
|
893
|
+
const db = await VectorDB.open({
|
|
894
|
+
dimensions: 4,
|
|
895
|
+
normalize: false,
|
|
896
|
+
storage: customStorage,
|
|
897
|
+
wasmBinary: null,
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
db.set("key1", [1, 2, 3, 4]);
|
|
901
|
+
await db.flush();
|
|
902
|
+
|
|
903
|
+
// Reopen with the same storage to verify persistence
|
|
904
|
+
const db2 = await VectorDB.open({
|
|
905
|
+
dimensions: 4,
|
|
906
|
+
normalize: false,
|
|
907
|
+
storage: customStorage,
|
|
908
|
+
wasmBinary: null,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
expect(db2.size).toBe(1);
|
|
912
|
+
expect(db2.get("key1")![0]).toBeCloseTo(1);
|
|
913
|
+
});
|
|
501
914
|
});
|
|
@@ -144,4 +144,12 @@ export class MemoryManager {
|
|
|
144
144
|
reset(): void {
|
|
145
145
|
this._vectorCount = 0;
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set the vector count after bulk-loading data directly into the DB region.
|
|
150
|
+
* Use when writing raw bytes to the DB region externally (e.g., streaming import).
|
|
151
|
+
*/
|
|
152
|
+
setVectorCount(count: number): void {
|
|
153
|
+
this._vectorCount = count;
|
|
154
|
+
}
|
|
147
155
|
}
|