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.
@@ -0,0 +1,131 @@
1
+ /**
2
+ * VectorDB — Key-Value Vector Database
3
+ *
4
+ * Decoupled from embedding providers. Users pass pre-computed vectors
5
+ * as number arrays (or Float32Array) with string keys.
6
+ *
7
+ * Supports:
8
+ * - set/get/setMany/getMany for key-value CRUD
9
+ * - query for similarity search (dot product on normalized vectors)
10
+ * - flush to persist, close to flush+release, clear to wipe
11
+ * - Streaming export/import using Web Streams API
12
+ * - Last-write-wins semantics for duplicate keys (append-only storage)
13
+ */
14
+ import type { ResultItem } from "./result-set";
15
+ import type { OpenOptions, OpenOptionsInternal, QueryOptions, SetOptions, VectorInput } from "./types";
16
+ export declare class VectorDB {
17
+ private readonly memoryManager;
18
+ private readonly storage;
19
+ private readonly _dimensions;
20
+ private readonly shouldNormalize;
21
+ private wasmExports;
22
+ /** Maps key to its slot index in the vector array */
23
+ private keyToSlot;
24
+ /** Maps slot index back to its key */
25
+ private slotToKey;
26
+ /** Whether this instance has been closed */
27
+ private closed;
28
+ private constructor();
29
+ /**
30
+ * Opens a VectorDB instance.
31
+ * Loads existing data from storage into WASM memory.
32
+ */
33
+ static open(options: OpenOptions): Promise<VectorDB>;
34
+ static open(options: OpenOptionsInternal): Promise<VectorDB>;
35
+ /** Total number of key-value pairs in the database */
36
+ get size(): number;
37
+ /** Number of dimensions per vector */
38
+ get dimensions(): number;
39
+ /**
40
+ * Check whether a key exists in the database.
41
+ * Uses the internal key-to-slot map for O(1) lookup.
42
+ */
43
+ has(key: string): boolean;
44
+ /**
45
+ * Delete an entry by key. Returns true if the key existed, false otherwise.
46
+ * Uses swap-and-pop to avoid gaps in the underlying vector array.
47
+ */
48
+ delete(key: string): boolean;
49
+ /**
50
+ * Returns an iterable of all keys in the database.
51
+ */
52
+ keys(): IterableIterator<string>;
53
+ /**
54
+ * Returns an iterable of [key, value] pairs.
55
+ * Values are returned as plain number array copies.
56
+ */
57
+ entries(): IterableIterator<[string, number[]]>;
58
+ /**
59
+ * Implements the iterable protocol. Same as entries().
60
+ */
61
+ [Symbol.iterator](): IterableIterator<[string, number[]]>;
62
+ /**
63
+ * Set a key-value pair. If the key already exists, its vector is overwritten (last-write-wins).
64
+ * The value is a number[] or Float32Array of length equal to the configured dimensions.
65
+ */
66
+ set(key: string, value: VectorInput, options?: SetOptions): void;
67
+ /**
68
+ * Get the stored vector for a key. Returns undefined if the key does not exist.
69
+ * Returns a copy of the stored vector as a plain number array.
70
+ */
71
+ get(key: string): number[] | undefined;
72
+ /**
73
+ * Set multiple key-value pairs at once. Last-write-wins applies within the batch.
74
+ */
75
+ setMany(entries: [string, VectorInput][]): void;
76
+ /**
77
+ * Get vectors for multiple keys. Returns undefined for keys that don't exist.
78
+ */
79
+ getMany(keys: string[]): (number[] | undefined)[];
80
+ /**
81
+ * Search for the most similar vectors to the given query vector.
82
+ *
83
+ * Default: returns a plain ResultItem[] sorted by descending similarity.
84
+ * With `{ iterable: true }`: returns a lazy Iterable<ResultItem> where keys
85
+ * are resolved only as each item is consumed.
86
+ *
87
+ * Similarity is the dot product of query and stored vectors. With
88
+ * normalization (default), this equals cosine similarity: 1 = identical,
89
+ * -1 = opposite.
90
+ */
91
+ query(value: VectorInput, options: QueryOptions & {
92
+ iterable: true;
93
+ }): Iterable<ResultItem>;
94
+ query(value: VectorInput, options?: QueryOptions): ResultItem[];
95
+ /**
96
+ * Persist the current in-memory state to storage.
97
+ */
98
+ flush(): Promise<void>;
99
+ /**
100
+ * Flush data to storage and release the instance.
101
+ * The instance cannot be used after close.
102
+ */
103
+ close(): Promise<void>;
104
+ /**
105
+ * Clear all data from the database and storage.
106
+ */
107
+ clear(): Promise<void>;
108
+ /**
109
+ * Export the entire database as a ReadableStream of binary chunks.
110
+ *
111
+ * Format: [Header 24 bytes][Vector data][Keys data]
112
+ * Header: magic(4) + version(4) + dimensions(4) + vectorCount(4) + vectorDataLen(4) + keysDataLen(4)
113
+ *
114
+ * Vectors are streamed in 64KB chunks from WASM memory to avoid large
115
+ * heap allocations.
116
+ */
117
+ export(): Promise<ReadableStream<Uint8Array>>;
118
+ /**
119
+ * Import data from a ReadableStream, replacing all existing data.
120
+ * Performs a dimension check against the configured dimensions.
121
+ *
122
+ * Vectors are streamed directly into WASM memory in chunks to avoid
123
+ * large heap allocations.
124
+ */
125
+ import(stream: ReadableStream<Uint8Array>): Promise<void>;
126
+ /**
127
+ * Normalize a vector using WASM (if available) or JS fallback.
128
+ */
129
+ private normalizeVector;
130
+ private assertOpen;
131
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * WASM SIMD compute layer.
3
+ * Compiles the hand-written WAT module and provides typed wrappers
4
+ * that operate on shared WebAssembly.Memory.
5
+ */
6
+ export interface WasmExports {
7
+ normalize(ptr: number, dimensions: number): void;
8
+ search_all(queryPtr: number, dbPtr: number, scoresPtr: number, dbSize: number, dimensions: number): void;
9
+ }
10
+ /**
11
+ * Instantiates a WASM module with the given memory and returns typed exports.
12
+ */
13
+ export declare function instantiateWasm(wasmBinary: Uint8Array, memory: WebAssembly.Memory): Promise<WasmExports>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eigen-db",
3
- "version": "4.3.0",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -12,16 +12,16 @@
12
12
  "module": "./dist/eigen-db.js",
13
13
  "exports": {
14
14
  ".": {
15
- "types": "./src/lib/index.ts",
15
+ "types": "./dist/index.d.ts",
16
16
  "import": "./dist/eigen-db.js",
17
17
  "require": "./dist/eigen-db.umd.cjs"
18
18
  }
19
19
  },
20
- "types": "./src/lib/index.ts",
20
+ "types": "./dist/index.d.ts",
21
21
  "scripts": {
22
22
  "dev": "vite",
23
23
  "compile-wat": "tsx scripts/compile-wat.ts",
24
- "build": "npm run compile-wat && tsc && vite build",
24
+ "build": "npm run compile-wat && tsc && vite build && tsc -p tsconfig.build.json",
25
25
  "preview": "vite preview",
26
26
  "test": "vitest run",
27
27
  "test:watch": "vitest",
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { iterableResults, topKResults } from "../result-set";
2
+ import { iterableResults, queryResults } from "../result-set";
3
3
 
4
- describe("topKResults", () => {
4
+ describe("queryResults", () => {
5
5
  const keys = ["apple", "banana", "cherry", "date", "elderberry"];
6
6
  const resolveKey = (index: number) => keys[index];
7
7
 
8
- it("sorts results by descending similarity", () => {
8
+ it("sorts results by descending similarity (default order)", () => {
9
9
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
10
- const results = topKResults(scores, resolveKey, 5);
10
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend" });
11
11
 
12
12
  expect(results).toHaveLength(5);
13
13
  expect(results[0].key).toBe("banana");
@@ -19,9 +19,22 @@ describe("topKResults", () => {
19
19
  expect(results[4].key).toBe("cherry");
20
20
  });
21
21
 
22
- it("respects topK limit", () => {
22
+ it("sorts results by ascending similarity", () => {
23
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
24
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "ascend" });
25
+
26
+ expect(results).toHaveLength(5);
27
+ expect(results[0].key).toBe("cherry");
28
+ expect(results[0].similarity).toBeCloseTo(0.1, 4);
29
+ expect(results[1].key).toBe("apple");
30
+ expect(results[1].similarity).toBeCloseTo(0.3, 4);
31
+ expect(results[4].key).toBe("banana");
32
+ expect(results[4].similarity).toBeCloseTo(0.9, 4);
33
+ });
34
+
35
+ it("respects limit", () => {
23
36
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
24
- const results = topKResults(scores, resolveKey, 3);
37
+ const results = queryResults(scores, resolveKey, { limit: 3, order: "descend" });
25
38
 
26
39
  expect(results).toHaveLength(3);
27
40
  expect(results[0].key).toBe("banana");
@@ -31,20 +44,19 @@ describe("topKResults", () => {
31
44
 
32
45
  it("handles empty scores", () => {
33
46
  const scores = new Float32Array(0);
34
- const results = topKResults(scores, resolveKey, 10);
47
+ const results = queryResults(scores, resolveKey, { limit: 10, order: "descend" });
35
48
  expect(results).toEqual([]);
36
49
  });
37
50
 
38
- it("handles topK larger than result count", () => {
51
+ it("handles limit larger than result count", () => {
39
52
  const scores = new Float32Array([0.5, 0.8]);
40
- const results = topKResults(scores, resolveKey, 100);
53
+ const results = queryResults(scores, resolveKey, { limit: 100, order: "descend" });
41
54
  expect(results).toHaveLength(2);
42
55
  });
43
56
 
44
57
  it("filters results by minSimilarity", () => {
45
58
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
46
- // similarities: apple=0.3, banana=0.9, cherry=0.1, date=0.7, elderberry=0.5
47
- const results = topKResults(scores, resolveKey, 5, 0.5);
59
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend", minSimilarity: 0.5 });
48
60
 
49
61
  expect(results).toHaveLength(3);
50
62
  expect(results[0].key).toBe("banana");
@@ -57,15 +69,81 @@ describe("topKResults", () => {
57
69
 
58
70
  it("minSimilarity is inclusive", () => {
59
71
  const scores = new Float32Array([0.5]);
60
- // similarity = 0.5
61
- const results = topKResults(scores, resolveKey, 10, 0.5);
72
+ const results = queryResults(scores, resolveKey, { limit: 10, order: "descend", minSimilarity: 0.5 });
62
73
  expect(results).toHaveLength(1);
63
74
  });
64
75
 
65
- it("handles topK Infinity", () => {
76
+ it("filters results by maxSimilarity", () => {
66
77
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
67
- const results = topKResults(scores, resolveKey, Infinity);
78
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend", maxSimilarity: 0.7 });
79
+
80
+ expect(results).toHaveLength(4);
81
+ expect(results[0].key).toBe("date");
82
+ expect(results[0].similarity).toBeCloseTo(0.7, 4);
83
+ expect(results[1].key).toBe("elderberry");
84
+ expect(results[2].key).toBe("apple");
85
+ expect(results[3].key).toBe("cherry");
86
+ });
87
+
88
+ it("maxSimilarity is inclusive", () => {
89
+ const scores = new Float32Array([0.5]);
90
+ const results = queryResults(scores, resolveKey, { limit: 10, order: "descend", maxSimilarity: 0.5 });
91
+ expect(results).toHaveLength(1);
92
+ });
93
+
94
+ it("filters by both minSimilarity and maxSimilarity", () => {
95
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
96
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend", minSimilarity: 0.3, maxSimilarity: 0.7 });
97
+
98
+ expect(results).toHaveLength(3);
99
+ expect(results[0].key).toBe("date");
100
+ expect(results[1].key).toBe("elderberry");
101
+ expect(results[2].key).toBe("apple");
102
+ });
103
+
104
+ it("handles limit Infinity", () => {
105
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
106
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend" });
107
+ expect(results).toHaveLength(5);
108
+ });
109
+
110
+ it("supports full similarity range [-1, 1]", () => {
111
+ const scores = new Float32Array([-1.0, -0.5, 0.0, 0.5, 1.0]);
112
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend" });
113
+
68
114
  expect(results).toHaveLength(5);
115
+ expect(results[0].similarity).toBeCloseTo(1.0, 5);
116
+ expect(results[4].similarity).toBeCloseTo(-1.0, 5);
117
+ });
118
+
119
+ it("handles tiny floating point values near boundaries", () => {
120
+ const epsilon = 1e-7;
121
+ const scores = new Float32Array([0.5 - epsilon, 0.5, 0.5 + epsilon]);
122
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "descend", minSimilarity: 0.5 });
123
+
124
+ // 0.5 and 0.5 + epsilon should pass, 0.5 - epsilon should not
125
+ expect(results).toHaveLength(2);
126
+ });
127
+
128
+ it("ascending order with limit returns bottomK", () => {
129
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
130
+ const results = queryResults(scores, resolveKey, { limit: 2, order: "ascend" });
131
+
132
+ expect(results).toHaveLength(2);
133
+ expect(results[0].key).toBe("cherry");
134
+ expect(results[0].similarity).toBeCloseTo(0.1, 4);
135
+ expect(results[1].key).toBe("apple");
136
+ expect(results[1].similarity).toBeCloseTo(0.3, 4);
137
+ });
138
+
139
+ it("ascending order with minSimilarity and maxSimilarity", () => {
140
+ const scores = new Float32Array([-1.0, -0.5, 0.0, 0.5, 1.0]);
141
+ const results = queryResults(scores, resolveKey, { limit: Infinity, order: "ascend", minSimilarity: -0.5, maxSimilarity: 0.5 });
142
+
143
+ expect(results).toHaveLength(3);
144
+ expect(results[0].similarity).toBeCloseTo(-0.5, 5);
145
+ expect(results[1].similarity).toBeCloseTo(0.0, 5);
146
+ expect(results[2].similarity).toBeCloseTo(0.5, 5);
69
147
  });
70
148
  });
71
149
 
@@ -75,7 +153,7 @@ describe("iterableResults", () => {
75
153
 
76
154
  it("sorts results by descending similarity", () => {
77
155
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
78
- const results = [...iterableResults(scores, resolveKey, 5)];
156
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "descend" })];
79
157
 
80
158
  expect(results).toHaveLength(5);
81
159
  expect(results[0].key).toBe("banana");
@@ -84,16 +162,26 @@ describe("iterableResults", () => {
84
162
  expect(results[4].key).toBe("cherry");
85
163
  });
86
164
 
87
- it("respects topK limit", () => {
165
+ it("sorts results by ascending similarity", () => {
88
166
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
89
- const results = [...iterableResults(scores, resolveKey, 3)];
167
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "ascend" })];
168
+
169
+ expect(results).toHaveLength(5);
170
+ expect(results[0].key).toBe("cherry");
171
+ expect(results[0].similarity).toBeCloseTo(0.1, 4);
172
+ expect(results[4].key).toBe("banana");
173
+ });
174
+
175
+ it("respects limit", () => {
176
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
177
+ const results = [...iterableResults(scores, resolveKey, { limit: 3, order: "descend" })];
90
178
 
91
179
  expect(results).toHaveLength(3);
92
180
  expect(results[0].key).toBe("banana");
93
181
  });
94
182
 
95
183
  it("handles empty scores", () => {
96
- const results = [...iterableResults(new Float32Array(0), resolveKey, 10)];
184
+ const results = [...iterableResults(new Float32Array(0), resolveKey, { limit: 10, order: "descend" })];
97
185
  expect(results).toEqual([]);
98
186
  });
99
187
 
@@ -105,7 +193,7 @@ describe("iterableResults", () => {
105
193
  };
106
194
 
107
195
  const scores = new Float32Array([0.3, 0.9, 0.1]);
108
- const iterable = iterableResults(scores, lazyResolver, 3);
196
+ const iterable = iterableResults(scores, lazyResolver, { limit: Infinity, order: "descend" });
109
197
 
110
198
  expect(callCount).toBe(0); // no key resolved yet
111
199
 
@@ -119,7 +207,7 @@ describe("iterableResults", () => {
119
207
 
120
208
  it("is re-iterable", () => {
121
209
  const scores = new Float32Array([0.3, 0.9, 0.1]);
122
- const iterable = iterableResults(scores, resolveKey, 3);
210
+ const iterable = iterableResults(scores, resolveKey, { limit: Infinity, order: "descend" });
123
211
 
124
212
  const first = [...iterable];
125
213
  const second = [...iterable];
@@ -128,7 +216,7 @@ describe("iterableResults", () => {
128
216
 
129
217
  it("supports partial iteration (early break)", () => {
130
218
  const scores = new Float32Array([0.1, 0.2, 0.3, 0.4, 0.5]);
131
- const iterable = iterableResults(scores, resolveKey, 5);
219
+ const iterable = iterableResults(scores, resolveKey, { limit: Infinity, order: "descend" });
132
220
 
133
221
  const partial: string[] = [];
134
222
  for (const item of iterable) {
@@ -140,10 +228,9 @@ describe("iterableResults", () => {
140
228
  expect(partial[1]).toBe("date"); // similarity 0.4
141
229
  });
142
230
 
143
- it("early stops iteration by minSimilarity", () => {
231
+ it("filters by minSimilarity", () => {
144
232
  const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
145
- // similarities: apple=0.3, banana=0.9, cherry=0.1, date=0.7, elderberry=0.5
146
- const results = [...iterableResults(scores, resolveKey, 5, 0.5)];
233
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "descend", minSimilarity: 0.5 })];
147
234
 
148
235
  expect(results).toHaveLength(3);
149
236
  expect(results[0].key).toBe("banana");
@@ -153,8 +240,40 @@ describe("iterableResults", () => {
153
240
 
154
241
  it("minSimilarity is inclusive", () => {
155
242
  const scores = new Float32Array([0.5]);
156
- // similarity = 0.5
157
- const results = [...iterableResults(scores, resolveKey, 10, 0.5)];
243
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "descend", minSimilarity: 0.5 })];
158
244
  expect(results).toHaveLength(1);
159
245
  });
246
+
247
+ it("filters by maxSimilarity", () => {
248
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
249
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "descend", maxSimilarity: 0.7 })];
250
+
251
+ expect(results).toHaveLength(4);
252
+ expect(results[0].key).toBe("date");
253
+ expect(results[3].key).toBe("cherry");
254
+ });
255
+
256
+ it("maxSimilarity is inclusive", () => {
257
+ const scores = new Float32Array([0.5]);
258
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "descend", maxSimilarity: 0.5 })];
259
+ expect(results).toHaveLength(1);
260
+ });
261
+
262
+ it("ascending order with limit returns bottomK", () => {
263
+ const scores = new Float32Array([0.3, 0.9, 0.1, 0.7, 0.5]);
264
+ const results = [...iterableResults(scores, resolveKey, { limit: 2, order: "ascend" })];
265
+
266
+ expect(results).toHaveLength(2);
267
+ expect(results[0].key).toBe("cherry");
268
+ expect(results[1].key).toBe("apple");
269
+ });
270
+
271
+ it("supports full similarity range [-1, 1]", () => {
272
+ const scores = new Float32Array([-1.0, -0.5, 0.0, 0.5, 1.0]);
273
+ const results = [...iterableResults(scores, resolveKey, { limit: Infinity, order: "ascend" })];
274
+
275
+ expect(results).toHaveLength(5);
276
+ expect(results[0].similarity).toBeCloseTo(-1.0, 5);
277
+ expect(results[4].similarity).toBeCloseTo(1.0, 5);
278
+ });
160
279
  });