@hviana/sema 0.1.4 → 0.1.6

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 (58) hide show
  1. package/AGENTS.md +6 -5
  2. package/CITATION.cff +49 -0
  3. package/HOW_IT_WORKS.md +11 -12
  4. package/README.md +7 -5
  5. package/dist/example/train_base.js +13 -10
  6. package/dist/src/alu/src/index.js +1 -1
  7. package/dist/src/config.d.ts +16 -19
  8. package/dist/src/config.js +3 -8
  9. package/dist/src/index.d.ts +2 -2
  10. package/dist/src/index.js +2 -6
  11. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  12. package/dist/src/rabitq-ivf/src/database.js +201 -0
  13. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.d.ts +2 -5
  14. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
  15. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  16. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  17. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/prng.d.ts +1 -1
  18. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/prng.js +1 -1
  19. package/dist/src/store-sqlite.d.ts +26 -1
  20. package/dist/src/store-sqlite.js +190 -8
  21. package/dist/src/store.d.ts +2 -9
  22. package/dist/src/store.js +6 -23
  23. package/example/train_base.ts +13 -10
  24. package/package.json +2 -2
  25. package/src/alu/README.md +1 -1
  26. package/src/alu/src/index.ts +1 -1
  27. package/src/config.ts +19 -27
  28. package/src/index.ts +6 -11
  29. package/src/rabitq-ivf/README.md +56 -0
  30. package/src/rabitq-ivf/src/database.ts +276 -0
  31. package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
  32. package/src/rabitq-ivf/src/ivf.ts +1330 -0
  33. package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
  34. package/src/store-sqlite.ts +196 -9
  35. package/src/store.ts +8 -32
  36. package/test/08-storage.test.mjs +3 -3
  37. package/test/14-scaling.test.mjs +2 -2
  38. package/test/35-ivf.test.mjs +263 -0
  39. package/test/36-bloom.test.mjs +123 -0
  40. package/dist/src/rabitq-hnsw/src/database.d.ts +0 -200
  41. package/dist/src/rabitq-hnsw/src/database.js +0 -388
  42. package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
  43. package/dist/src/rabitq-hnsw/src/heap.js +0 -89
  44. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
  45. package/dist/src/rabitq-hnsw/src/hnsw.js +0 -474
  46. package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
  47. package/dist/src/rabitq-hnsw/src/store.js +0 -825
  48. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
  49. package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -948
  50. package/src/rabitq-hnsw/README.md +0 -303
  51. package/src/rabitq-hnsw/src/database.ts +0 -492
  52. package/src/rabitq-hnsw/src/heap.ts +0 -90
  53. package/src/rabitq-hnsw/src/hnsw.ts +0 -514
  54. package/src/rabitq-hnsw/src/store.ts +0 -994
  55. package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
  56. /package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.d.ts +0 -0
  57. /package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.js +0 -0
  58. /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
package/src/config.ts CHANGED
@@ -39,18 +39,9 @@ export interface AlphabetConfig {
39
39
 
40
40
  export interface StoreConfig {
41
41
  minHaloMass: number;
42
- m: number;
43
- efConstruction: number;
44
- /** Construction budget for REACH-ONLY interior gists the ~90% of content
45
- * index inserts that exist so a partial query can resonate a sub-region
46
- * and climb, never as dedup targets. Deposit roots and halo-bearing
47
- * targets always build with the full `efConstruction`. A smaller budget
48
- * here is the one deliberate speed-for-quality trade in ingestion (an
49
- * interior's layer-0 wiring is built from a narrower candidate beam);
50
- * the recall suite (partial recall, multi-topic attention, counterfactual
51
- * anchoring) is the gate for its value. Set equal to `efConstruction`
52
- * to disable the trade. */
53
- efConstructionInterior: number;
42
+ /** Query breadth of the IVF vector indices: clusters probed per query =
43
+ * ceil(efSearch / 4). Inserts have no quality knob — the partitioned
44
+ * index routes and appends, so ingestion cost is flat by construction. */
54
45
  efSearch: number;
55
46
  /** Compact the in-memory vector indices after this many vectors are written.
56
47
  * Compaction rebuilds an index from its live codes to reclaim the slots left
@@ -58,7 +49,7 @@ export interface StoreConfig {
58
49
  * (not on a flush count that goes quiet during repeat-heavy training) keeps
59
50
  * the index dense and query cost bounded. */
60
51
  compactEveryNWrites: number;
61
- /** Over-fetch factor for HNSW queries (ANN recall cushion). */
52
+ /** Over-fetch factor for vector-index queries (ANN recall cushion). */
62
53
  overfetch: number;
63
54
  /** Combined buffered-write ceiling before a flush of both vector indices
64
55
  * (content + halo). Higher ⇒ fewer, larger flushes into the in-memory
@@ -91,14 +82,20 @@ export interface StoreConfig {
91
82
  * round-trips through the quantizer. An eviction or a reopen reads the
92
83
  * 2-bit row — the fidelity every cross-session consumer already gets. */
93
84
  haloCacheBytes: number;
94
- /** Size, in MiB, of each `rabitq-hnsw` `VectorDatabase`'s memory budget
95
- * (forwarded as its `cacheSizeMb`). It is the index's SINGLE memory knob: it
96
- * sizes both SQLite's page cache and the derived immutable-code LRU. A PURE
97
- * latency optimisation the index reads codes from SQLite on demand, so its
98
- * correctness and its asymptotic per-operation storage-read count are identical
99
- * with the budget at 0. Exposed so a scaling test can set it to 0 and measure
85
+ /** Size, in MiB, of each `rabitq-ivf` `VectorDatabase`'s memory budget
86
+ * (forwarded as its `cacheSizeMb` its SQLite page cache). A PURE latency
87
+ * optimisation the index reads chunk blobs from SQLite on demand, so its
88
+ * correctness and its per-operation storage-read count are identical with
89
+ * the budget at 0. Exposed so a scaling test can set it to 0 and measure
100
90
  * the honest, cache-independent cost. */
101
91
  vectorCacheMb: number;
92
+ /** Size, in MiB, of the MAIN DAG database's SQLite page cache. The node /
93
+ * kid / edge / contain tables serve millions of point probes per training
94
+ * session (content-addressed findLeaf/findBranch, parent probes, contain
95
+ * appends); SQLite's default cache (~2 MiB) thrashes once the DB outgrows
96
+ * it, so every probe pays a file read. A PURE latency knob — correctness
97
+ * and result identical at any value. */
98
+ sqliteCacheMb: number;
102
99
  /** Max entries in the skipped-interior LRU set. Interiors that
103
100
  * {@link Store.indexSubtree} has already visited (indexed or skipped) are
104
101
  * remembered here so subsequent calls prune their subtrees. Session-local
@@ -151,9 +148,6 @@ export const DEFAULT_CONFIG: MindConfig = {
151
148
  },
152
149
  store: {
153
150
  minHaloMass: 1,
154
- m: 8,
155
- efConstruction: 64,
156
- efConstructionInterior: 16,
157
151
  efSearch: 64,
158
152
  compactEveryNWrites: 50_000,
159
153
  overfetch: 4,
@@ -165,6 +159,7 @@ export const DEFAULT_CONFIG: MindConfig = {
165
159
  pendingGistBytes: 16_000_000,
166
160
  haloCacheBytes: 16_000_000,
167
161
  vectorCacheMb: 64,
162
+ sqliteCacheMb: 64,
168
163
  coveredIdsMax: 100_000,
169
164
  chainCacheBytes: 16_000_000,
170
165
  },
@@ -194,11 +189,6 @@ export function resolveConfig(opts: Partial<MindConfig> = {}): MindConfig {
194
189
  },
195
190
  store: {
196
191
  minHaloMass: opts.store?.minHaloMass ?? DEFAULT_CONFIG.store.minHaloMass,
197
- m: opts.store?.m ?? DEFAULT_CONFIG.store.m,
198
- efConstruction: opts.store?.efConstruction ??
199
- DEFAULT_CONFIG.store.efConstruction,
200
- efConstructionInterior: opts.store?.efConstructionInterior ??
201
- DEFAULT_CONFIG.store.efConstructionInterior,
202
192
  efSearch: opts.store?.efSearch ?? DEFAULT_CONFIG.store.efSearch,
203
193
  compactEveryNWrites: opts.store?.compactEveryNWrites ??
204
194
  DEFAULT_CONFIG.store.compactEveryNWrites,
@@ -218,6 +208,8 @@ export function resolveConfig(opts: Partial<MindConfig> = {}): MindConfig {
218
208
  DEFAULT_CONFIG.store.haloCacheBytes,
219
209
  vectorCacheMb: opts.store?.vectorCacheMb ??
220
210
  DEFAULT_CONFIG.store.vectorCacheMb,
211
+ sqliteCacheMb: opts.store?.sqliteCacheMb ??
212
+ DEFAULT_CONFIG.store.sqliteCacheMb,
221
213
  coveredIdsMax: opts.store?.coveredIdsMax ??
222
214
  DEFAULT_CONFIG.store.coveredIdsMax,
223
215
  chainCacheBytes: opts.store?.chainCacheBytes ??
package/src/index.ts CHANGED
@@ -13,26 +13,21 @@ export * from "./store-sqlite.js";
13
13
  export * from "./config.js";
14
14
  export * from "./extension.js";
15
15
  export * from "./ingest-cache.js";
16
- // rabitq-hnsw is re-exported selectively: its `Store`, `NodeRec`, and
17
- // `StoreConfig` are internal names that collide with sema's own top-level
18
- // `store.js` / `config.js` exports, and sema code that needs the rabitq ones
19
- // imports them from the subpath directly. Re-export the public vector-DB
20
- // surface under the sema root, omitting the three colliding names.
16
+ // rabitq-ivf: the partitioned (IVF) vector index over 1-bit RaBitQ codes.
21
17
  export {
22
- Heap,
23
- HnswIndex,
18
+ IvfIndex,
24
19
  Prng,
25
20
  RaBitQuantizer,
26
21
  VectorDatabase,
27
- } from "./rabitq-hnsw/src/index.js";
22
+ } from "./rabitq-ivf/src/index.js";
28
23
  export type {
29
24
  DatabaseOptions,
30
25
  ExternalId,
31
- HnswParams,
32
- KnnHit,
26
+ IvfConfig,
27
+ IvfHit,
33
28
  QueryContext,
34
29
  QueryResult,
35
30
  RaBitQOptions,
36
31
  StorageStats,
37
- } from "./rabitq-hnsw/src/index.js";
32
+ } from "./rabitq-ivf/src/index.js";
38
33
  export * from "./derive/src/index.js";
@@ -0,0 +1,56 @@
1
+ # rabitq-ivf
2
+
3
+ A small TypeScript library for **approximate nearest-neighbour search** using an
4
+ **adaptive partitioned (IVF) index** over **1-bit RaBitQ codes**, with
5
+ **cosine** distance. The entire index lives in **SQLite** — the durable copy of
6
+ every code is on disk, resident memory is bounded (the pivot table plus a capped
7
+ page cache, never the whole collection), and the index survives process
8
+ restarts.
9
+
10
+ The vectors are stored **only** as 1-bit RaBitQ codes — the original vectors are
11
+ never kept — so the index is both fast _and_ tiny on disk:
12
+
13
+ ```
14
+ a 1024-d vector:
15
+ Float32 : 1024 × 4 bytes = 4096 bytes
16
+ RaBitQ 1-bit (code) : 1024 × 1 bit = 128 bytes (32× smaller)
17
+ ```
18
+
19
+ The code _is_ the entire per-vector representation. `get` returns the bare code,
20
+ and `insert`, `update`, and `query` all accept either a raw vector or a code —
21
+ detected by length.
22
+
23
+ ## The index
24
+
25
+ - **Clusters, not a graph.** The collection is partitioned into clusters, each
26
+ with a binary pivot code and its member codes packed in fixed-size chunk
27
+ blobs.
28
+ - **Insert = route + append.** Find the nearest pivot (one linear Hamming scan
29
+ of the RAM-resident pivot table) and append to that cluster's tail chunk. No
30
+ beam search, no neighbour rewiring — per-insert cost is essentially flat in
31
+ collection size.
32
+ - **Query = probe + scan.** Rank all pivots with the accurate RaBitQ estimator,
33
+ scan the `ceil(efSearch/4)` nearest clusters with the same estimator, keep the
34
+ top k. Per-query storage reads are bounded by nprobe × chunks-per-cluster —
35
+ constant once the collection has split.
36
+ - **Adaptive, deterministic splits.** A cluster reaching 4096 entries is
37
+ median-split on the margin between two farthest-point seeds (two exact halves,
38
+ cascade-proof), and both halves get fresh majority-bit pivots. There is no
39
+ RNG: the index is a pure function of the insertion sequence.
40
+ - **Same durability discipline as the rest of Sema**: WAL, batched caller-owned
41
+ transactions (`upsertMany`), 1 KiB pages, 64 MiB WAL autocheckpoint.
42
+
43
+ ## Usage
44
+
45
+ ```ts
46
+ import { VectorDatabase } from "./src/index.js";
47
+
48
+ const db = new VectorDatabase({ dbPath: "vectors.db", dim: 1024 });
49
+ db.upsertMany([{ id: 1, vector: v1 }, { id: 2, vector: v2 }]);
50
+ const hits = db.query(q, 10); // [{ id, distance }]
51
+ db.close();
52
+ ```
53
+
54
+ `lastQueryStorageReads` reports the cache-independent storage-read count of the
55
+ most recent query — the honest scalability witness the test suite asserts on
56
+ (`test/35-ivf.test.mjs` at the repo root).
@@ -0,0 +1,276 @@
1
+ import { IvfHit, IvfIndex } from "./ivf.js";
2
+
3
+ /** External ids are integers only. */
4
+ export type ExternalId = number;
5
+
6
+ export interface DatabaseOptions {
7
+ /** Path to the SQLite database file (use ":memory:" for a transient store). */
8
+ dbPath: string;
9
+ /** Vector dimensionality. Required for a new database; ignored when reopening. */
10
+ dim?: number;
11
+ /** Query breadth: clusters probed per query = ceil(efSearch / 4). Default
12
+ * 64 (16 probes). Tunable at runtime and at reopen. */
13
+ efSearch?: number;
14
+ /** Bits used to scalar-quantise the query for the RaBitQ estimator.
15
+ * Query-side only (stored codes are independent of it), so it is tunable
16
+ * at reopen, like efSearch. Default 8. */
17
+ queryBits?: number;
18
+ /** Number of sign-flip + Hadamard rounds in the random rotation. Default 3. */
19
+ rotationRounds?: number;
20
+ /** Seed for the rotation. Default fixed. */
21
+ seed?: number;
22
+ /** Optional centroid the vectors are centered by before quantisation. */
23
+ centroid?: ArrayLike<number>;
24
+ /** SQLite page-cache budget in MiB. Purely a latency knob: correctness and
25
+ * the per-operation storage-read count are identical with it at 0. */
26
+ cacheSizeMb?: number;
27
+ }
28
+
29
+ export interface QueryResult {
30
+ id: ExternalId;
31
+ /** Estimated cosine distance (1 - cosine). */
32
+ distance: number;
33
+ }
34
+
35
+ export interface StorageStats {
36
+ float32BytesPerVector: number;
37
+ codeBytesPerVector: number;
38
+ bytesPerVector: number;
39
+ compressionRatio: number;
40
+ }
41
+
42
+ /**
43
+ * A persistent vector database: an adaptive PARTITIONED (IVF) index over
44
+ * 1-bit RaBitQ codes (cosine), stored entirely in SQLite at `dbPath`. See
45
+ * ivf.ts for the index design. The original float vectors are never
46
+ * retained — only the sign codes — so a 1024-d vector costs 128 bytes.
47
+ *
48
+ * Inserting is route-and-append: cost is essentially FLAT in collection size
49
+ * (one RAM scan of the pivot table + one chunk append — no per-insert graph
50
+ * walk). Query cost is bounded by nprobe × cluster size — constant in N
51
+ * once the collection is past its first splits.
52
+ */
53
+ export class VectorDatabase {
54
+ private readonly index: IvfIndex;
55
+
56
+ constructor(options: DatabaseOptions) {
57
+ if (
58
+ !options || typeof options.dbPath !== "string" ||
59
+ options.dbPath.length === 0
60
+ ) {
61
+ throw new Error("DatabaseOptions.dbPath (string) is required");
62
+ }
63
+ this.index = new IvfIndex(options.dbPath, options);
64
+ }
65
+
66
+ get dim(): number {
67
+ return this.index.dim;
68
+ }
69
+
70
+ /** Number of live (non-deleted) vectors. */
71
+ get size(): number {
72
+ return this.index.size;
73
+ }
74
+
75
+ /** Physical slot count including tombstones from deletes/updates. */
76
+ get physicalSize(): number {
77
+ return this.index.physicalSize;
78
+ }
79
+
80
+ /** Number of clusters (partitions) currently in the index. */
81
+ get clusterCount(): number {
82
+ return this.index.clusterCount;
83
+ }
84
+
85
+ get efSearch(): number {
86
+ return this.index.efSearch;
87
+ }
88
+ set efSearch(value: number) {
89
+ this.index.efSearch = value;
90
+ }
91
+
92
+ /** Distance computations performed during the most recent query. */
93
+ get lastQueryDistanceComputations(): number {
94
+ return this.index.lastQueryDistComps;
95
+ }
96
+
97
+ /** Storage row reads issued by the most recent query — the honest,
98
+ * cache-independent scalability metric. */
99
+ get lastQueryStorageReads(): number {
100
+ return this.index.lastQueryStorageReads;
101
+ }
102
+
103
+ get storage(): StorageStats {
104
+ const f32 = this.dim * 4;
105
+ const bpv = this.index.bytesPerVector;
106
+ return {
107
+ float32BytesPerVector: f32,
108
+ codeBytesPerVector: bpv,
109
+ bytesPerVector: bpv,
110
+ compressionRatio: f32 / bpv,
111
+ };
112
+ }
113
+
114
+ private checkId(id: ExternalId): number {
115
+ // 32-bit signed range: chunk blobs store exts as int32 — a wider id
116
+ // would silently truncate, so it is rejected at the door instead.
117
+ if (!Number.isInteger(id) || id > 0x7fffffff || id < -0x80000000) {
118
+ throw new Error(`External id must be a 32-bit integer, got ${id}`);
119
+ }
120
+ return id;
121
+ }
122
+
123
+ /** Convert a value to code bytes, selecting by length: `codeWords`
124
+ * elements → an existing 1-bit code; otherwise a raw `dim`-vector. */
125
+ private toCodeBytes(value: ArrayLike<number>): Uint8Array {
126
+ if (value.length === this.index.codeWords) {
127
+ return this.index.codeToBytes(value);
128
+ }
129
+ if (value.length !== this.dim) {
130
+ throw new Error(
131
+ `Vector dimension mismatch: expected ${this.dim}, got ${value.length}`,
132
+ );
133
+ }
134
+ return this.index.encodeToBytes(value);
135
+ }
136
+
137
+ has(id: ExternalId): boolean {
138
+ return this.index.has(this.checkId(id));
139
+ }
140
+
141
+ /** Stream every live external id (bounded memory). */
142
+ *keys(): IterableIterator<ExternalId> {
143
+ yield* this.index.keys();
144
+ }
145
+
146
+ /** Stream live entries whose INTERNAL id is > `after` — a durable
147
+ * incremental watermark (internal ids are monotone at insert and preserved
148
+ * by update and compact). */
149
+ *keysSince(
150
+ after: number,
151
+ ): IterableIterator<{ ext: ExternalId; internal: number }> {
152
+ yield* this.index.keysSince(after);
153
+ }
154
+
155
+ /** Read the stored 1-bit code for an id (a copy as a Uint32Array), or null. */
156
+ get(id: ExternalId): Uint32Array | null {
157
+ const bytes = this.index.codeOf(this.checkId(id));
158
+ return bytes ? this.index.bytesToCode(bytes) : null;
159
+ }
160
+
161
+ insert(id: ExternalId, value: ArrayLike<number>): void {
162
+ this.index.begin();
163
+ try {
164
+ this.index.insert(this.checkId(id), this.toCodeBytes(value));
165
+ this.index.commit();
166
+ } catch (e) {
167
+ this.index.rollback();
168
+ throw e;
169
+ }
170
+ }
171
+
172
+ update(id: ExternalId, value: ArrayLike<number>): void {
173
+ this.index.begin();
174
+ try {
175
+ this.index.update(this.checkId(id), this.toCodeBytes(value));
176
+ this.index.commit();
177
+ } catch (e) {
178
+ this.index.rollback();
179
+ throw e;
180
+ }
181
+ }
182
+
183
+ upsert(id: ExternalId, value: ArrayLike<number>): void {
184
+ this.upsertMany([{ id, vector: value }]);
185
+ }
186
+
187
+ /**
188
+ * Upsert many vectors under ONE transaction — one WAL commit for the whole
189
+ * batch instead of one per vector, with chunk appends to the same cluster
190
+ * coalesced in the index's write-back buffer.
191
+ */
192
+ upsertMany(
193
+ entries: Array<{ id: ExternalId; vector: ArrayLike<number> }>,
194
+ ): void {
195
+ if (entries.length === 0) return;
196
+ this.index.begin();
197
+ try {
198
+ for (const e of entries) {
199
+ // One shared vmap probe decides insert vs update — the per-row point
200
+ // query is the dominant bulk-load cost, so it is never duplicated.
201
+ this.index.upsert(this.checkId(e.id), this.toCodeBytes(e.vector));
202
+ }
203
+ this.index.commit();
204
+ } catch (e) {
205
+ this.index.rollback();
206
+ throw e;
207
+ }
208
+ }
209
+
210
+ /** Delete many ids under ONE transaction. Absent ids are skipped.
211
+ * Returns the number of vectors actually removed. */
212
+ deleteMany(ids: ExternalId[]): number {
213
+ if (ids.length === 0) return 0;
214
+ let removed = 0;
215
+ this.index.begin();
216
+ try {
217
+ for (const id of ids) {
218
+ if (this.index.remove(this.checkId(id))) removed++;
219
+ }
220
+ this.index.commit();
221
+ } catch (e) {
222
+ this.index.rollback();
223
+ throw e;
224
+ }
225
+ return removed;
226
+ }
227
+
228
+ /** Delete the vector bound to an id. Returns false if absent. */
229
+ delete(id: ExternalId): boolean {
230
+ return this.deleteMany([id]) === 1;
231
+ }
232
+
233
+ /** Heat the SQLite page cache with sequential scans (latency only). */
234
+ warmCache(): number {
235
+ return this.index.warmCache();
236
+ }
237
+
238
+ /** Reclaim tombstoned slots by rewriting the clusters that carry any, then
239
+ * VACUUM. Internal ids and cluster assignment are preserved. */
240
+ compact(): void {
241
+ this.index.compact();
242
+ }
243
+
244
+ /**
245
+ * k-NN search. The argument's length selects the mode:
246
+ * - `dim` elements → a raw vector (accurate estimator)
247
+ * - `codeWords` elements → a 1-bit code, by sign-bit Hamming
248
+ */
249
+ query(
250
+ query: ArrayLike<number>,
251
+ k = 10,
252
+ opts?: { ef?: number },
253
+ ): QueryResult[] {
254
+ let hits: IvfHit[];
255
+ if (query.length === this.index.codeWords) {
256
+ hits = this.index.searchKnnByCode(
257
+ this.index.codeToBytes(query),
258
+ k,
259
+ opts?.ef,
260
+ );
261
+ } else {
262
+ if (query.length !== this.dim) {
263
+ throw new Error(
264
+ `Vector dimension mismatch: expected ${this.dim}, got ${query.length}`,
265
+ );
266
+ }
267
+ hits = this.index.searchKnn(query, k, opts?.ef);
268
+ }
269
+ return hits;
270
+ }
271
+
272
+ /** Close the underlying database. The instance must not be used afterwards. */
273
+ close(): void {
274
+ this.index.close();
275
+ }
276
+ }
@@ -5,11 +5,8 @@ export type {
5
5
  QueryResult,
6
6
  StorageStats,
7
7
  } from "./database.js";
8
- export { HnswIndex } from "./hnsw.js";
9
- export type { HnswParams, KnnHit } from "./hnsw.js";
8
+ export { IvfIndex } from "./ivf.js";
9
+ export type { IvfConfig, IvfHit } from "./ivf.js";
10
10
  export { RaBitQuantizer } from "./rabitq.js";
11
11
  export type { QueryContext, RaBitQOptions } from "./rabitq.js";
12
- export { Store } from "./store.js";
13
- export type { GlobalState, NodeRec, StoreConfig } from "./store.js";
14
- export { Heap } from "./heap.js";
15
12
  export { Prng } from "./prng.js";