@omendb/omendb 0.0.24 → 0.0.26

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.
Files changed (4) hide show
  1. package/README.md +12 -12
  2. package/index.d.ts +146 -27
  3. package/index.js +128 -25
  4. package/package.json +7 -7
package/README.md CHANGED
@@ -31,11 +31,11 @@ db.set([
31
31
  ]);
32
32
 
33
33
  // Search
34
- const results = db.search(new Float32Array(384).fill(0.15), { k: 5 });
34
+ const results = db.search(new Float32Array(384).fill(0.15), 5);
35
35
  // [{ id: 'doc1', distance: 0.05, metadata: { title: 'Hello' } }, ...]
36
36
 
37
37
  // Batch search (async, parallel)
38
- const batchResults = await db.searchBatch(queries, { k: 10 });
38
+ const batchResults = await db.searchBatch(queries, 10);
39
39
 
40
40
  // Close when done (releases file locks)
41
41
  db.close();
@@ -139,13 +139,13 @@ const deleted = db.deleteByFilter({
139
139
 
140
140
  ### Search
141
141
 
142
- #### `db.search(query, options)`
142
+ #### `db.search(query, k, options?)`
143
143
 
144
144
  Search for k nearest neighbors (sync).
145
145
 
146
146
  ```typescript
147
- const results = db.search(queryVector, {
148
- k: 10, // Number of results
147
+ const results = db.search(queryVector, 10); // Basic
148
+ const results = db.search(queryVector, 10, {
149
149
  ef: 200, // Search quality (higher = better recall)
150
150
  filter: { category: "news" }, // Metadata filter
151
151
  maxDistance: 0.5, // Distance threshold
@@ -153,12 +153,12 @@ const results = db.search(queryVector, {
153
153
  // [{ id, distance, metadata }, ...]
154
154
  ```
155
155
 
156
- #### `db.searchBatch(queries, options)`
156
+ #### `db.searchBatch(queries, k, ef?)`
157
157
 
158
158
  Batch search with parallel execution (async).
159
159
 
160
160
  ```typescript
161
- const results = await db.searchBatch(queries, { k: 10, ef: 100 });
161
+ const results = await db.searchBatch(queries, 10, 100);
162
162
  // [[{ id, distance, metadata }, ...], ...]
163
163
  ```
164
164
 
@@ -224,7 +224,7 @@ Get or create a named collection.
224
224
  ```typescript
225
225
  const users = db.collection("users");
226
226
  users.set([...]);
227
- users.search(query, { k: 5 });
227
+ users.search(query, 5);
228
228
  ```
229
229
 
230
230
  #### `db.collections()`
@@ -373,11 +373,11 @@ const merged = db.mergeFrom(otherDb);
373
373
 
374
374
  **10K vectors, 128D, M=16, ef=100. Measured 2026-01-20 (Apple M3 Max):**
375
375
 
376
- | Metric | Value |
377
- | ---------- | --------- |
378
- | Search QPS | 11,542 |
376
+ | Metric | Value |
377
+ | ---------- | ------------ |
378
+ | Search QPS | 11,542 |
379
379
  | Build | 30,826 vec/s |
380
- | Recall@10 | 89.7% |
380
+ | Recall@10 | 89.7% |
381
381
 
382
382
  ## License
383
383
 
package/index.d.ts CHANGED
@@ -4,20 +4,48 @@ export declare class VectorDatabase {
4
4
  /**
5
5
  * Insert or update vectors.
6
6
  *
7
- * Accepts an array of items with id, vector, and optional metadata.
7
+ * Works for both single-vector and multi-vector stores:
8
+ * - Single-vector: items have `vector` field
9
+ * - Multi-vector: items have `vectors` field (array of vectors)
10
+ *
11
+ * When any item includes a `text` field, text search is automatically enabled.
12
+ * This allows immediate use of searchHybrid() without calling enableTextSearch().
13
+ *
14
+ * @param items - Array of {id, vector, metadata?, text?} or {id, vectors, metadata?}
15
+ * @returns Number of vectors inserted/updated
8
16
  */
9
- set(items: Array<VectorItem>): Array<number>
17
+ set(items: Array<SetItem>): number
10
18
  /**
11
19
  * Search for k nearest neighbors.
12
20
  *
13
21
  * @param query - Query vector (number[] or Float32Array)
14
22
  * @param k - Number of results to return
15
- * @param ef - Optional search width override
16
- * @param filter - Optional metadata filter (e.g., {category: "foo"} or {price: {$gt: 10}})
17
- * @param maxDistance - Optional max distance threshold (filter out distant results)
23
+ * @param options - Optional search options: {filter?, ef?, maxDistance?}
24
+ * @returns Array of {id, distance, score, metadata}
25
+ *
26
+ * @example
27
+ * ```javascript
28
+ * // Basic search
29
+ * db.search([1, 0, 0, 0], 10);
30
+ *
31
+ * // With options
32
+ * db.search([1, 0, 0, 0], 10, { filter: { category: "A" }, ef: 200 });
33
+ * db.search([1, 0, 0, 0], 10, { maxDistance: 0.5 });
34
+ * ```
35
+ */
36
+ search(query: Array<number> | Float32Array, k: number, options?: { filter?: Record<string, unknown>; ef?: number; maxDistance?: number } | undefined): Array<SearchResult>
37
+ /**
38
+ * Search multi-vector store with query tokens.
39
+ *
40
+ * Internal method used by unified search() for multi-vector stores.
41
+ *
42
+ * @param query - Query tokens (number[][] or Float32Array[])
43
+ * @param k - Number of results to return
44
+ * @param rerank - Enable MaxSim reranking for better quality (default: true)
45
+ * @param rerankFactor - Fetch k*rerankFactor candidates before reranking (default: 32)
18
46
  * @returns Array of {id, distance, metadata}
19
47
  */
20
- search(query: Array<number> | Float32Array, k: number, ef?: number | undefined | null, filter?: Record<string, unknown> | undefined, maxDistance?: number | undefined | null): Array<SearchResult>
48
+ searchMulti(query: Array<Array<number>> | Array<Float32Array>, k: number, rerank?: boolean | undefined | null, rerankFactor?: number | undefined | null): Array<SearchResult>
21
49
  /**
22
50
  * Batch search with parallel execution (async).
23
51
  *
@@ -30,9 +58,21 @@ export declare class VectorDatabase {
30
58
  /**
31
59
  * Delete vectors by ID.
32
60
  *
61
+ * Accepts either a single ID string or an array of IDs.
62
+ *
63
+ * @param ids - Single ID string or array of IDs to delete
33
64
  * @returns Number of vectors deleted
65
+ *
66
+ * @example
67
+ * ```javascript
68
+ * // Delete single
69
+ * db.delete("doc1");
70
+ *
71
+ * // Delete multiple
72
+ * db.delete(["doc1", "doc2", "doc3"]);
73
+ * ```
34
74
  */
35
- delete(ids: Array<string>): number
75
+ delete(ids: string | Array<string>): number
36
76
  /**
37
77
  * Delete vectors matching a metadata filter.
38
78
  *
@@ -77,12 +117,34 @@ export declare class VectorDatabase {
77
117
  * ```
78
118
  */
79
119
  count(filter?: Record<string, unknown> | undefined): number
80
- /** Update a vector's data and/or metadata. */
81
- update(id: string, vector: Array<number> | Float32Array, metadata?: Record<string, unknown> | undefined): void
120
+ /**
121
+ * Update a vector's data, metadata, and/or text.
122
+ *
123
+ * @param id - Vector ID to update
124
+ * @param options - Update options: {vector?, metadata?, text?}
125
+ *
126
+ * @example
127
+ * ```javascript
128
+ * // Update vector only
129
+ * db.update("doc1", { vector: [1, 0, 0, 0] });
130
+ *
131
+ * // Update metadata only
132
+ * db.update("doc1", { metadata: { status: "active" } });
133
+ *
134
+ * // Update text (re-indexed for BM25 search)
135
+ * db.update("doc1", { text: "Updated content for search" });
136
+ *
137
+ * // Update multiple fields
138
+ * db.update("doc1", { vector: [...], metadata: {...}, text: "..." });
139
+ * ```
140
+ */
141
+ update(id: string, options: { vector?: number[] | Float32Array; metadata?: Record<string, unknown>; text?: string }): void
82
142
  /** Get number of vectors in database. */
83
143
  get length(): number
84
144
  /** Get vector dimensions of this database. */
85
145
  get dimensions(): number
146
+ /** Check if this is a multi-vector store. */
147
+ get isMultiVector(): boolean
86
148
  /** Check if database is empty. */
87
149
  isEmpty(): boolean
88
150
  /** Get database statistics. */
@@ -103,20 +165,11 @@ export declare class VectorDatabase {
103
165
  /** Delete a collection. */
104
166
  deleteCollection(name: string): void
105
167
  /**
106
- * Enable text search for hybrid (vector + text) search.
168
+ * Check if text search is enabled.
107
169
  *
108
- * Must be called before using setWithText() or hybridSearch().
170
+ * Text search is automatically enabled when using set() with text field.
109
171
  */
110
- enableTextSearch(): void
111
- /** Check if text search is enabled. */
112
172
  get hasTextSearch(): boolean
113
- /**
114
- * Set vectors with associated text for hybrid search.
115
- *
116
- * @param items - Array of {id, vector, text, metadata?}
117
- * @returns Array of internal indices
118
- */
119
- setWithText(items: Array<VectorItemWithText>): Array<number>
120
173
  /**
121
174
  * Search using text only (BM25 scoring).
122
175
  *
@@ -124,7 +177,7 @@ export declare class VectorDatabase {
124
177
  * @param k - Number of results
125
178
  * @returns Array of {id, score, metadata}
126
179
  */
127
- textSearch(query: string, k: number): Array<TextSearchResult>
180
+ searchText(query: string, k: number): Array<TextSearchResult>
128
181
  /**
129
182
  * Hybrid search combining vector similarity and text relevance.
130
183
  *
@@ -133,13 +186,24 @@ export declare class VectorDatabase {
133
186
  * @param queryVector - Query embedding
134
187
  * @param queryText - Text query for BM25
135
188
  * @param k - Number of results
136
- * @param filter - Optional metadata filter
137
- * @param alpha - Weight for vector vs text (0.0=text only, 1.0=vector only, default=0.5)
138
- * @param rrfK - RRF constant (default=60, higher reduces rank influence)
139
- * @param subscores - Return separate keyword_score and semantic_score (default: false)
140
- * @returns Array of {id, score, metadata, keyword_score?, semantic_score?}
189
+ * @param options - Optional: {filter?, alpha?, rrfK?, subscores?}
190
+ * @returns Array of {id, score, metadata, keywordScore?, semanticScore?}
191
+ *
192
+ * @example
193
+ * ```javascript
194
+ * // Basic hybrid search
195
+ * db.searchHybrid([1, 0, 0, 0], "machine learning", 10);
196
+ *
197
+ * // With options
198
+ * db.searchHybrid([1, 0, 0, 0], "query", 10, {
199
+ * filter: { type: "ml" },
200
+ * alpha: 0.7,
201
+ * rrfK: 60,
202
+ * subscores: true
203
+ * });
204
+ * ```
141
205
  */
142
- hybridSearch(queryVector: Array<number> | Float32Array, queryText: string, k: number, filter?: Record<string, unknown> | undefined, alpha?: number | undefined | null, rrfK?: number | undefined | null, subscores?: boolean | undefined | null): Array<HybridSearchResult>
206
+ searchHybrid(queryVector: Array<number> | Float32Array, queryText: string, k: number, options?: { filter?: Record<string, unknown>; alpha?: number; rrfK?: number; subscores?: boolean } | undefined): Array<HybridSearchResult>
143
207
  /**
144
208
  * Flush pending changes to disk.
145
209
  *
@@ -207,6 +271,31 @@ export declare class VectorDatabase {
207
271
  * @returns true if ID exists and is not deleted
208
272
  */
209
273
  exists(id: string): boolean
274
+ /**
275
+ * Alias for exists() - check if an ID exists in the database.
276
+ *
277
+ * @param id - Vector ID to check
278
+ * @returns true if ID exists and is not deleted
279
+ */
280
+ has(id: string): boolean
281
+ /**
282
+ * Search for the single nearest neighbor.
283
+ *
284
+ * Convenience method that returns the top result or null if no matches.
285
+ *
286
+ * @param query - Query vector (number[] or Float32Array)
287
+ * @param options - Optional search options: {filter?, ef?, maxDistance?}
288
+ * @returns Single result or null
289
+ *
290
+ * @example
291
+ * ```javascript
292
+ * const nearest = db.searchOne([1, 0, 0, 0]);
293
+ * if (nearest) {
294
+ * console.log(`Found: ${nearest.id} at distance ${nearest.distance}`);
295
+ * }
296
+ * ```
297
+ */
298
+ searchOne(query: Array<number> | Float32Array, options?: { filter?: Record<string, unknown>; ef?: number; maxDistance?: number } | undefined): SearchResult | null
210
299
  /**
211
300
  * Get multiple vectors by ID.
212
301
  *
@@ -234,6 +323,14 @@ export interface HybridSearchResult {
234
323
  semanticScore?: number
235
324
  }
236
325
 
326
+ export interface MultiVectorItem {
327
+ id: string
328
+ /** Multi-vector data as array of Float32Arrays */
329
+ vectors: Float32Array[]
330
+ /** Optional metadata */
331
+ metadata?: Record<string, unknown> | undefined
332
+ }
333
+
237
334
  /**
238
335
  * Open or create a vector database.
239
336
  *
@@ -311,15 +408,37 @@ export interface OpenOptions {
311
408
  oversample?: number
312
409
  /** Distance metric: "l2"/"euclidean" (default), "cosine", "dot"/"ip" */
313
410
  metric?: string
411
+ /**
412
+ * Enable multi-vector mode for ColBERT-style retrieval
413
+ * - true: Enable with default config (repetitions=8, partition_bits=4, dProj=16)
414
+ * - { repetitions?, partitionBits?, seed?, dProj? }: Custom config
415
+ * - dProj: Dimension projection (16 = 8x smaller FDE, null = full token dim)
416
+ * - false/null: Disabled (default, single-vector mode)
417
+ */
418
+ multiVector?: boolean | { repetitions?: number; partitionBits?: number; seed?: number; dProj?: number | null } | null | undefined
314
419
  }
315
420
 
316
421
  export interface SearchResult {
317
422
  id: string
318
423
  distance: number
424
+ /** Normalized similarity score (0-1, higher = more similar) */
425
+ score: number
319
426
  /** Metadata as JSON (using serde-json feature) */
320
427
  metadata: Record<string, unknown>
321
428
  }
322
429
 
430
+ export interface SetItem {
431
+ id: string
432
+ /** Single vector data (for regular stores) */
433
+ vector?: Float32Array
434
+ /** Multi-vector data (for multi-vector stores) */
435
+ vectors?: Float32Array[] | undefined
436
+ /** Optional metadata */
437
+ metadata?: Record<string, unknown> | undefined
438
+ /** Optional text for hybrid search (auto-enables text search, stored in metadata.text) */
439
+ text?: string
440
+ }
441
+
323
442
  export interface StatsResult {
324
443
  dimensions: number
325
444
  count: number
package/index.js CHANGED
@@ -111,12 +111,41 @@ function toFloat32Array(arr) {
111
111
 
112
112
  // Convert VectorItem to use Float32Array
113
113
  function convertVectorItem(item) {
114
+ if (item.vector === undefined || item.vector === null) {
115
+ if (Array.isArray(item.vectors)) {
116
+ throw new Error(
117
+ `Item '${item.id}' has 'vectors' field but store is single-vector. Use multiVector: true when opening the database.`,
118
+ );
119
+ }
120
+ throw new Error(`Item '${item.id}' missing required 'vector' field`);
121
+ }
114
122
  return {
115
123
  ...item,
116
124
  vector: toFloat32Array(item.vector),
117
125
  };
118
126
  }
119
127
 
128
+ // Convert MultiVectorItem to use Float32Arrays
129
+ function convertMultiVectorItem(item) {
130
+ if (!Array.isArray(item.vectors)) {
131
+ if (item.vector !== undefined) {
132
+ throw new Error(
133
+ `Item '${item.id}' has 'vector' field but store is multi-vector. Use 'vectors' field (array of vectors) instead.`,
134
+ );
135
+ }
136
+ throw new Error(`Item '${item.id}' missing required 'vectors' field`);
137
+ }
138
+ return {
139
+ ...item,
140
+ vectors: item.vectors.map(toFloat32Array),
141
+ };
142
+ }
143
+
144
+ // Check if items contain multi-vector data (vectors field must be an array)
145
+ function isMultiVectorItem(item) {
146
+ return Array.isArray(item.vectors);
147
+ }
148
+
120
149
  // Wrap VectorDatabase to handle array conversion
121
150
  const NativeVectorDatabase = nativeBinding.VectorDatabase;
122
151
 
@@ -125,12 +154,62 @@ class VectorDatabase {
125
154
  this._native = nativeDb;
126
155
  }
127
156
 
157
+ /**
158
+ * Insert or update vectors.
159
+ *
160
+ * Works for both single-vector and multi-vector stores:
161
+ * - Single-vector: items have `vector` field
162
+ * - Multi-vector: items have `vectors` field (array of vectors)
163
+ *
164
+ * When any item includes a `text` field, text search is automatically enabled.
165
+ *
166
+ * @param {Array<{id: string, vector?: Float32Array|number[], vectors?: Float32Array[]|number[][], metadata?: object, text?: string}>} items
167
+ * @returns {number} Number of vectors inserted/updated
168
+ */
128
169
  set(items) {
129
- return this._native.set(items.map(convertVectorItem));
170
+ if (!Array.isArray(items)) {
171
+ throw new Error("set() requires an array of items");
172
+ }
173
+ if (items.length === 0) {
174
+ return 0;
175
+ }
176
+ // Unified set() handles both single and multi-vector via native set()
177
+ return this._native.set(items.map(this._native.isMultiVector ? convertMultiVectorItem : convertVectorItem));
178
+ }
179
+
180
+ /**
181
+ * Search for k nearest neighbors.
182
+ *
183
+ * Works for both single-vector and multi-vector stores:
184
+ * - Single-vector: query is number[] or Float32Array
185
+ * - Multi-vector: query is number[][] or Float32Array[]
186
+ *
187
+ * @param {number[]|Float32Array|number[][]|Float32Array[]} query - Query vector(s)
188
+ * @param {number} k - Number of results to return
189
+ * @param {object} [options] - Search options: {filter?, ef?, maxDistance?}
190
+ * @returns {Array<{id: string, distance: number, score: number, metadata: object}>}
191
+ */
192
+ search(query, k, options) {
193
+ if (this._native.isMultiVector) {
194
+ // Multi-vector store
195
+ const rerank = options?.rerank;
196
+ const rerankFactor = options?.rerankFactor;
197
+ return this._native.searchMulti(query, k, rerank, rerankFactor);
198
+ } else {
199
+ // Single-vector store - pass options object directly to native
200
+ return this._native.search(query, k, options);
201
+ }
130
202
  }
131
203
 
132
- search(query, k, ef, filter, maxDistance) {
133
- return this._native.search(query, k, ef, filter, maxDistance);
204
+ /**
205
+ * Search for the single nearest neighbor.
206
+ *
207
+ * @param {number[]|Float32Array} query - Query vector
208
+ * @param {object} [options] - Search options: {filter?, ef?, maxDistance?}
209
+ * @returns {{id: string, distance: number, score: number, metadata: object}|null}
210
+ */
211
+ searchOne(query, options) {
212
+ return this._native.searchOne(query, options);
134
213
  }
135
214
 
136
215
  searchBatch(queries, k, ef) {
@@ -153,8 +232,14 @@ class VectorDatabase {
153
232
  return this._native.count(filter);
154
233
  }
155
234
 
156
- update(id, vector, metadata) {
157
- return this._native.update(id, vector, metadata);
235
+ /**
236
+ * Update a vector's data, metadata, and/or text.
237
+ *
238
+ * @param {string} id - Vector ID to update
239
+ * @param {object} options - Update options: {vector?, metadata?, text?}
240
+ */
241
+ update(id, options) {
242
+ return this._native.update(id, options);
158
243
  }
159
244
 
160
245
  get length() {
@@ -165,6 +250,10 @@ class VectorDatabase {
165
250
  return this._native.dimensions;
166
251
  }
167
252
 
253
+ get isMultiVector() {
254
+ return this._native.isMultiVector;
255
+ }
256
+
168
257
  isEmpty() {
169
258
  return this._native.isEmpty();
170
259
  }
@@ -193,32 +282,32 @@ class VectorDatabase {
193
282
  return this._native.deleteCollection(name);
194
283
  }
195
284
 
196
- enableTextSearch() {
197
- return this._native.enableTextSearch();
198
- }
199
-
200
285
  get hasTextSearch() {
201
286
  return this._native.hasTextSearch;
202
287
  }
203
288
 
204
- setWithText(items) {
205
- return this._native.setWithText(items.map(convertVectorItem));
289
+ /**
290
+ * Search using text only (BM25 scoring).
291
+ *
292
+ * @param {string} query - Text query
293
+ * @param {number} k - Number of results
294
+ * @returns {Array<{id: string, score: number, metadata: object}>}
295
+ */
296
+ searchText(query, k) {
297
+ return this._native.searchText(query, k);
206
298
  }
207
299
 
208
- textSearch(query, k) {
209
- return this._native.textSearch(query, k);
210
- }
211
-
212
- hybridSearch(queryVector, queryText, k, filter, alpha, rrfK, subscores) {
213
- return this._native.hybridSearch(
214
- queryVector,
215
- queryText,
216
- k,
217
- filter,
218
- alpha,
219
- rrfK,
220
- subscores,
221
- );
300
+ /**
301
+ * Hybrid search combining vector similarity and text relevance.
302
+ *
303
+ * @param {number[]|Float32Array} queryVector - Query embedding
304
+ * @param {string} queryText - Text query for BM25
305
+ * @param {number} k - Number of results
306
+ * @param {object} [options] - Options: {filter?, alpha?, rrfK?, subscores?}
307
+ * @returns {Array<{id: string, score: number, metadata: object, keywordScore?: number, semanticScore?: number}>}
308
+ */
309
+ searchHybrid(queryVector, queryText, k, options) {
310
+ return this._native.searchHybrid(queryVector, queryText, k, options);
222
311
  }
223
312
 
224
313
  flush() {
@@ -249,9 +338,23 @@ class VectorDatabase {
249
338
  return this._native.exists(id);
250
339
  }
251
340
 
341
+ /**
342
+ * Alias for exists() - check if an ID exists in the database.
343
+ *
344
+ * @param {string} id - Vector ID to check
345
+ * @returns {boolean}
346
+ */
347
+ has(id) {
348
+ return this._native.has(id);
349
+ }
350
+
252
351
  getBatch(ids) {
253
352
  return this._native.getBatch(ids);
254
353
  }
354
+
355
+ compact() {
356
+ return this._native.compact();
357
+ }
255
358
  }
256
359
 
257
360
  function open(path, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omendb/omendb",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Fast embedded vector database with HNSW + ACORN-1 filtered search",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -41,7 +41,7 @@
41
41
  "test": "vitest run"
42
42
  },
43
43
  "devDependencies": {
44
- "@napi-rs/cli": "^3.5.0",
44
+ "@napi-rs/cli": "^3.5.1",
45
45
  "vitest": "^2.1.0"
46
46
  },
47
47
  "files": [
@@ -50,10 +50,10 @@
50
50
  "omendb.node"
51
51
  ],
52
52
  "optionalDependencies": {
53
- "@omendb/omendb-darwin-x64": "0.0.24",
54
- "@omendb/omendb-darwin-arm64": "0.0.24",
55
- "@omendb/omendb-linux-x64-gnu": "0.0.24",
56
- "@omendb/omendb-linux-arm64-gnu": "0.0.24",
57
- "@omendb/omendb-win32-x64-msvc": "0.0.24"
53
+ "@omendb/omendb-darwin-x64": "0.0.26",
54
+ "@omendb/omendb-darwin-arm64": "0.0.26",
55
+ "@omendb/omendb-linux-x64-gnu": "0.0.26",
56
+ "@omendb/omendb-linux-arm64-gnu": "0.0.26",
57
+ "@omendb/omendb-win32-x64-msvc": "0.0.26"
58
58
  }
59
59
  }