@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
@@ -1,474 +0,0 @@
1
- import { Heap } from "./heap.js";
2
- import { Prng } from "./prng.js";
3
- /**
4
- * Hierarchical Navigable Small World graph (Malkov & Yashunin, 2018) backed by
5
- * 1-bit RaBitQ codes that live in SQLite (see Store) rather than in RAM. The
6
- * algorithm is the textbook one; the only difference is that codes and adjacency
7
- * lists are read and written through the store on demand, so the resident set is
8
- * the working set of the current operation -- never the whole graph.
9
- *
10
- * Distances use the codes throughout:
11
- * - searching with a full-precision query uses RaBitQ's accurate estimator;
12
- * - building the graph and searching by code compare two codes by sign-bit
13
- * Hamming distance.
14
- * Building on codes alone is what lets `compact` rebuild an honest index with no
15
- * access to the original vectors.
16
- */
17
- export class HnswIndex {
18
- M;
19
- Mmax0;
20
- efConstruction;
21
- mL;
22
- quantizer;
23
- store;
24
- seed;
25
- levelRng;
26
- // tiny global scalars, cached from meta and written through on change
27
- entry;
28
- maxLevel;
29
- live;
30
- total;
31
- _efSearch;
32
- // Per-operation working set: the records the current insert/query is actively
33
- // comparing. Cleared at the start of every op and bounded by efConstruction /
34
- // efSearch (the nodes one operation can touch), never by the collection size.
35
- // This is the algorithm's working memory -- it makes each touched node cost one
36
- // storage read per op -- not a tunable cache. The only cache is SQLite's page
37
- // cache (see Store), sized in MB.
38
- working = new Map();
39
- visited = new Set();
40
- // Epoch-tagged visited marks — the Set above without per-candidate hashing:
41
- // visitedTag[id] === visitedEpoch means "seen this operation", and bumping
42
- // the epoch clears the whole set in O(1). Semantically IDENTICAL to the
43
- // Set (results never differ); used only when the store has a cache budget,
44
- // because the array is 4 B per internal id — amortized working memory that
45
- // grows with the collection, which the cacheSizeMb:0 flat-memory mode must
46
- // not pay. The epoch wrap (2³²−1 ops) refills the array once.
47
- visitedTag = new Uint32Array(1024);
48
- visitedEpoch = 0;
49
- seenTag(id, epoch) {
50
- return id < this.visitedTag.length && this.visitedTag[id] === epoch;
51
- }
52
- markVisited(id, epoch) {
53
- if (id >= this.visitedTag.length) {
54
- const grown = new Uint32Array(Math.max(id + 1, this.visitedTag.length * 2));
55
- grown.set(this.visitedTag);
56
- this.visitedTag = grown;
57
- }
58
- this.visitedTag[id] = epoch;
59
- }
60
- candHeap = new Heap(true); // min-heap: nearest first
61
- resHeap = new Heap(false); // max-heap: farthest first
62
- singleEp = [0];
63
- idxScratch = [];
64
- distScratch = [];
65
- freshScratch = [];
66
- // distance dispatch: a full-precision query sets qCtx; building or a code
67
- // query sets refBytes (code<->code). `building` only suppresses the counter.
68
- qCtx = null;
69
- refBytes = null;
70
- building = false;
71
- lastQueryDistComps = 0;
72
- /** Storage row reads issued by the most recent query (cache-independent). */
73
- lastQueryStorageReads = 0;
74
- constructor(quantizer, store, params) {
75
- this.quantizer = quantizer;
76
- this.store = store;
77
- this.M = params.M;
78
- this.Mmax0 = this.M * 2;
79
- this.efConstruction = params.efConstruction;
80
- this._efSearch = params.efSearch;
81
- this.mL = 1 / Math.log(this.M);
82
- this.seed = params.seed >>> 0;
83
- const s = store.loadState();
84
- this.entry = s.entryPoint;
85
- this.maxLevel = s.maxLevel;
86
- this.live = s.live;
87
- this.total = s.total;
88
- this.levelRng = new Prng(this.seed);
89
- this.levelRng.restore(s.rng);
90
- }
91
- get size() {
92
- return this.live;
93
- }
94
- get physicalSize() {
95
- return this.total;
96
- }
97
- get bytesPerVector() {
98
- return this.quantizer.codeWords * 4;
99
- }
100
- get efSearch() {
101
- return this._efSearch;
102
- }
103
- set efSearch(v) {
104
- this._efSearch = Math.max(1, v | 0);
105
- this.store.setEfSearch(this._efSearch);
106
- }
107
- persistState() {
108
- this.store.saveState({
109
- entryPoint: this.entry,
110
- maxLevel: this.maxLevel,
111
- live: this.live,
112
- total: this.total,
113
- rng: this.levelRng.snapshot(),
114
- });
115
- }
116
- randomLevel() {
117
- let u = this.levelRng.next();
118
- if (u < 1e-12)
119
- u = 1e-12;
120
- return Math.floor(-Math.log(u) * this.mL);
121
- }
122
- /**
123
- * Fetch a node record into the operation's working set. The first touch in an
124
- * op is a storage read; later touches in the same op reuse it. The set is
125
- * cleared per op and bounded by efConstruction / efSearch, so storage reads
126
- * per op equal the number of *distinct* nodes the op visits -- minimal, and
127
- * independent of any cache.
128
- */
129
- node(id) {
130
- const hit = this.working.get(id);
131
- if (hit !== undefined)
132
- return hit;
133
- const rec = this.store.getNode(id);
134
- if (rec === null)
135
- throw new Error(`node ${id} not found`);
136
- this.working.set(id, rec);
137
- return rec;
138
- }
139
- /** Prefetch several nodes into the working set with ONE batched storage
140
- * read for the misses. Point queries per neighbour made statement
141
- * dispatch — not distance arithmetic — the dominant cost of a large
142
- * build; the batch keeps `reads` accounting identical per row. */
143
- fetchNodes(ids, count = ids.length) {
144
- // getNodesInto skips ids already present in `working` itself — no
145
- // second pre-filter pass over the same map here.
146
- this.store.getNodesInto(ids, this.working, count);
147
- }
148
- /** Distance from the current source (query vector or reference code) to a node. */
149
- distOf(rec) {
150
- if (this.qCtx !== null) {
151
- this.lastQueryDistComps++;
152
- return this.quantizer.estimate(rec.code, 0, this.qCtx);
153
- }
154
- if (!this.building)
155
- this.lastQueryDistComps++;
156
- return this.quantizer.codeDistanceBytes(this.refBytes, rec.code);
157
- }
158
- /** Code-to-code distance between two stored nodes (graph wiring only). */
159
- codeDist(a, b) {
160
- return this.quantizer.codeDistanceBytes(this.node(a).code, this.node(b).code);
161
- }
162
- /**
163
- * SEARCH-LAYER (Algorithm 2). Frontier in `candHeap`, bounded result set in
164
- * `resHeap` (non-deleted only). Deleted nodes are traversed for routing but
165
- * never returned. `visited` is a per-call set sized by the nodes seen here.
166
- */
167
- searchLayer(entryPoints, ef, layer) {
168
- const cand = this.candHeap;
169
- const res = this.resHeap;
170
- cand.clear();
171
- res.clear();
172
- // Visited marks: epoch tags when RAM-for-speed is allowed, the plain Set
173
- // in flat-memory mode. Identical semantics either way.
174
- const useTags = this.store.cacheEnabled;
175
- let epoch = 0;
176
- const visited = this.visited;
177
- if (useTags) {
178
- epoch = ++this.visitedEpoch;
179
- if (epoch === 0xffffffff) {
180
- this.visitedTag.fill(0);
181
- epoch = this.visitedEpoch = 1;
182
- }
183
- }
184
- else {
185
- visited.clear();
186
- }
187
- for (let k = 0; k < entryPoints.length; k++) {
188
- const ep = entryPoints[k];
189
- if (useTags) {
190
- if (this.seenTag(ep, epoch))
191
- continue;
192
- this.markVisited(ep, epoch);
193
- }
194
- else {
195
- if (visited.has(ep))
196
- continue;
197
- visited.add(ep);
198
- }
199
- const rec = this.node(ep);
200
- const d = this.distOf(rec);
201
- cand.push(d, ep);
202
- if (rec.deleted === 0) {
203
- res.push(d, ep);
204
- if (res.size > ef)
205
- res.pop();
206
- }
207
- }
208
- while (cand.size > 0) {
209
- const cd = cand.topKey();
210
- if (res.size >= ef && cd > res.topKey())
211
- break;
212
- const c = cand.topVal();
213
- cand.pop();
214
- const nbrs = this.store.getNeighbors(c, layer);
215
- if (nbrs === null)
216
- continue;
217
- // Batch: mark the unvisited neighbours, fetch their codes in one
218
- // storage read, then score them in the original order.
219
- const fresh = this.freshScratch;
220
- fresh.length = 0;
221
- for (let i = 0; i < nbrs.length; i++) {
222
- const e = nbrs[i];
223
- if (useTags) {
224
- if (this.seenTag(e, epoch))
225
- continue;
226
- this.markVisited(e, epoch);
227
- }
228
- else {
229
- if (visited.has(e))
230
- continue;
231
- visited.add(e);
232
- }
233
- fresh.push(e);
234
- }
235
- if (fresh.length > 1)
236
- this.fetchNodes(fresh);
237
- for (let i = 0; i < fresh.length; i++) {
238
- const e = fresh[i];
239
- const rec = this.node(e);
240
- const d = this.distOf(rec);
241
- const worst = res.size > 0 ? res.topKey() : Infinity;
242
- if (res.size < ef || d < worst) {
243
- cand.push(d, e);
244
- if (rec.deleted === 0) {
245
- res.push(d, e);
246
- if (res.size > ef)
247
- res.pop();
248
- }
249
- }
250
- }
251
- }
252
- }
253
- /** Greedy single-best descent from `fromLayer` down to (but not into) `toLayer`. */
254
- greedyDescend(entry, fromLayer, toLayer) {
255
- let cur = entry;
256
- const ep = this.singleEp;
257
- for (let layer = fromLayer; layer > toLayer; layer--) {
258
- ep[0] = cur;
259
- this.searchLayer(ep, 1, layer);
260
- if (this.resHeap.size > 0)
261
- cur = this.resHeap.vals[0];
262
- }
263
- return cur;
264
- }
265
- /**
266
- * SELECT-NEIGHBORS-HEURISTIC (Algorithm 4) on codes. Picks up to `M` diverse
267
- * neighbours from `candIds`, preferring those closer to `base` than to any
268
- * already-chosen neighbour, then fills remaining slots with the closest
269
- * leftovers (keep-pruned connections). Appends to `out`.
270
- */
271
- selectNeighbors(base, candIds, count, M, out) {
272
- const idx = this.idxScratch;
273
- const ds = this.distScratch;
274
- this.fetchNodes(candIds, count); // one batched read for the misses
275
- idx.length = count;
276
- ds.length = count;
277
- for (let i = 0; i < count; i++) {
278
- idx[i] = i;
279
- ds[i] = this.codeDist(base, candIds[i]);
280
- }
281
- idx.sort((a, b) => ds[a] - ds[b]);
282
- for (let s = 0; s < count; s++) {
283
- if (out.length >= M)
284
- break;
285
- const i = idx[s];
286
- const cid = candIds[i];
287
- if (cid === base)
288
- continue;
289
- const cd = ds[i];
290
- let keep = true;
291
- for (let j = 0; j < out.length; j++) {
292
- if (this.codeDist(cid, out[j]) < cd) {
293
- keep = false;
294
- break;
295
- }
296
- }
297
- if (keep)
298
- out.push(cid);
299
- }
300
- if (out.length < M) {
301
- for (let s = 0; s < count && out.length < M; s++) {
302
- const cid = candIds[idx[s]];
303
- if (cid === base)
304
- continue;
305
- let dup = false;
306
- for (let j = 0; j < out.length; j++) {
307
- if (out[j] === cid) {
308
- dup = true;
309
- break;
310
- }
311
- }
312
- if (!dup)
313
- out.push(cid);
314
- }
315
- }
316
- }
317
- /**
318
- * Insert a vector's code under external id `ext`; returns the internal node id.
319
- * All graph reads/writes go through the store; the caller owns the transaction.
320
- */
321
- insert(ext, code, efC) {
322
- const ef = efC !== undefined && efC >= 1
323
- ? Math.min(efC, this.efConstruction)
324
- : this.efConstruction;
325
- this.working.clear();
326
- const level = this.randomLevel();
327
- const id = this.store.addNode(ext, level, code);
328
- this.total++;
329
- this.working.set(id, { code, deleted: 0, ext });
330
- if (this.entry === -1) {
331
- this.entry = id;
332
- this.maxLevel = level;
333
- this.live++;
334
- this.persistState();
335
- return id;
336
- }
337
- this.qCtx = null;
338
- this.refBytes = code;
339
- this.building = true;
340
- let entry = this.entry;
341
- const topLayer = this.maxLevel;
342
- if (topLayer > level)
343
- entry = this.greedyDescend(entry, topLayer, level);
344
- let entryPoints = [entry];
345
- const startLayer = Math.min(topLayer, level);
346
- for (let layer = startLayer; layer >= 0; layer--) {
347
- this.searchLayer(entryPoints, ef, layer);
348
- const res = this.resHeap;
349
- const wCount = res.size;
350
- const selected = [];
351
- this.selectNeighbors(id, res.vals, wCount, this.M, selected);
352
- if (selected.length > 0)
353
- this.store.setNeighbors(id, layer, selected);
354
- const cap = layer === 0 ? this.Mmax0 : this.M;
355
- for (let s = 0; s < selected.length; s++) {
356
- const nb = selected[s];
357
- const cur = this.store.getNeighbors(nb, layer);
358
- const list = cur ? Array.from(cur) : [];
359
- list.push(id);
360
- if (list.length > cap) {
361
- const pruned = [];
362
- this.selectNeighbors(nb, list, list.length, cap, pruned);
363
- this.store.setNeighbors(nb, layer, pruned);
364
- }
365
- else {
366
- this.store.setNeighbors(nb, layer, list);
367
- }
368
- }
369
- if (wCount > 0) {
370
- const next = new Array(wCount);
371
- for (let i = 0; i < wCount; i++)
372
- next[i] = res.vals[i];
373
- entryPoints = next;
374
- }
375
- else {
376
- entryPoints = [entry];
377
- }
378
- }
379
- this.building = false;
380
- this.refBytes = null;
381
- this.live++;
382
- if (level > this.maxLevel) {
383
- this.maxLevel = level;
384
- this.entry = id;
385
- }
386
- this.persistState();
387
- return id;
388
- }
389
- /** k-NN with a full-precision query vector (accurate estimator). */
390
- searchKnn(vec, k, ef) {
391
- this.lastQueryDistComps = 0;
392
- this.store.resetReads();
393
- this.working.clear();
394
- this.building = false;
395
- this.refBytes = null;
396
- if (this.entry === -1 || k <= 0) {
397
- this.lastQueryStorageReads = 0;
398
- return [];
399
- }
400
- this.qCtx = this.quantizer.prepareQuery(vec);
401
- const hits = this.collectKnn(k, Math.max(ef ?? this._efSearch, k));
402
- this.qCtx = null;
403
- this.lastQueryStorageReads = this.store.reads;
404
- return hits;
405
- }
406
- /** k-NN with an already-quantized code (sign-bit Hamming / angular distance). */
407
- searchKnnByCode(codeBytes, k, ef) {
408
- this.lastQueryDistComps = 0;
409
- this.store.resetReads();
410
- this.working.clear();
411
- this.building = false;
412
- this.qCtx = null;
413
- if (this.entry === -1 || k <= 0) {
414
- this.lastQueryStorageReads = 0;
415
- return [];
416
- }
417
- this.refBytes = codeBytes;
418
- const hits = this.collectKnn(k, Math.max(ef ?? this._efSearch, k));
419
- this.refBytes = null;
420
- this.lastQueryStorageReads = this.store.reads;
421
- return hits;
422
- }
423
- collectKnn(k, efs) {
424
- let entry = this.entry;
425
- if (this.maxLevel > 0)
426
- entry = this.greedyDescend(entry, this.maxLevel, 0);
427
- this.singleEp[0] = entry;
428
- this.searchLayer(this.singleEp, efs, 0);
429
- const res = this.resHeap;
430
- const n = res.size;
431
- const hits = new Array(n);
432
- for (let i = 0; i < n; i++) {
433
- let d = res.keys[i];
434
- if (d < 0)
435
- d = 0;
436
- hits[i] = { id: this.node(res.vals[i]).ext, distance: d };
437
- }
438
- hits.sort((a, b) => a.distance - b.distance);
439
- if (hits.length > k)
440
- hits.length = k;
441
- return hits;
442
- }
443
- /** Tombstone a live node by internal id. Caller owns the transaction. */
444
- remove(id) {
445
- const rec = this.store.getNode(id);
446
- if (rec === null || rec.deleted === 1)
447
- return false;
448
- this.store.tombstone(id);
449
- this.live--;
450
- this.persistState();
451
- return true;
452
- }
453
- /**
454
- * Reclaim tombstones with a graph-preserving splice (see
455
- * {@link Store.spliceCompact}): live nodes keep their internal ids and their
456
- * wiring; each dead neighbour is replaced by its own live neighbours. Codes
457
- * are untouched, so the index loses nothing beyond what deletion itself
458
- * removes. The previous implementation replayed every live code through a
459
- * full HNSW insert — O(live · ef · log N) storage reads, HOURS on a trained
460
- * multi-million-node store, inside one WAL-bloating transaction. The splice
461
- * is a streaming pass with batched commits, then a VACUUM to return the
462
- * freed pages.
463
- */
464
- compact() {
465
- const r = this.store.spliceCompact(this.M, this.Mmax0, this.entry);
466
- this.entry = r.entry;
467
- this.maxLevel = r.maxLevel;
468
- this.live = r.live;
469
- this.total = r.live;
470
- this.persistState();
471
- this.working.clear();
472
- this.store.vacuum();
473
- }
474
- }
@@ -1,162 +0,0 @@
1
- import { DatabaseSync } from "node:sqlite";
2
- /**
3
- * The on-disk structure of the index, in SQLite. This is NOT a serialization of
4
- * an in-memory graph: it IS the graph. Every code and every adjacency list lives
5
- * in these tables and is read/written on demand, so process memory stays flat as
6
- * the collection grows -- only the working set of the current operation, plus
7
- * SQLite's fixed-size page cache, is ever resident.
8
- *
9
- * Tables
10
- * meta one row of configuration + mutable global state (entry point, counts,
11
- * level-RNG state). The quantizer's rotation is fully determined by
12
- * {dim, rounds, seed, centroid}, so reopening a database reproduces it
13
- * exactly without storing any matrix.
14
- * nodes one row per inserted vector: id (rowid), external id, top level,
15
- * tombstone flag, and the 1-bit code as a BLOB. Indexed by rowid (code
16
- * lookup) and by a partial unique index on the external id (live nodes).
17
- * links one row per (node, layer) holding that node's neighbour ids packed as
18
- * a BLOB. WITHOUT ROWID makes (node, layer) the clustering key, so a
19
- * neighbour-list fetch -- the hot path of search -- is a single B-tree
20
- * descent landing on the inline BLOB.
21
- */
22
- export interface StoreConfig {
23
- dim: number;
24
- m: number;
25
- efConstruction: number;
26
- efSearch: number;
27
- queryBits: number;
28
- rotationRounds: number;
29
- seed: number;
30
- centroid: Float64Array | null;
31
- codeWords: number;
32
- paddedDim: number;
33
- }
34
- export interface GlobalState {
35
- entryPoint: number;
36
- maxLevel: number;
37
- live: number;
38
- total: number;
39
- rng: number;
40
- }
41
- /** A node as the graph algorithms need it: code bytes, tombstone, external id. */
42
- export interface NodeRec {
43
- code: Uint8Array;
44
- deleted: number;
45
- ext: number | null;
46
- }
47
- export declare class Store {
48
- readonly db: DatabaseSync;
49
- /**
50
- * Number of row reads served from the database (node and neighbour-list
51
- * fetches). This counts *storage* accesses only, so it is unaffected by any
52
- * caching layer above it -- the honest measure of how the engine scales.
53
- */
54
- reads: number;
55
- private sInsNode;
56
- private sNode;
57
- private sIdByExt;
58
- private sTombstone;
59
- private sNbrs;
60
- private sSetNbrs;
61
- private sState;
62
- private readonly cacheBudgetBytes;
63
- /** Whether the shared memory budget is non-zero — consumers that trade RAM
64
- * for speed (the graph's visited-tag array) key off this so the
65
- * `cacheSizeMb: 0` flat-memory mode stays exactly flat. */
66
- get cacheEnabled(): boolean;
67
- private slab;
68
- private slabHot;
69
- private nbrSlab;
70
- private slabsDerived;
71
- /**
72
- * @param dbPath path to the SQLite file (":memory:" for transient).
73
- * @param cacheSizeMb the ONE memory knob (MiB). It sizes BOTH SQLite's page
74
- * cache AND the immutable-code LRU above (whose entry-capacity is derived
75
- * from this budget and the code size — no second knob). Both are pure
76
- * speed enhancements: correctness and the per-operation storage-read
77
- * count do not depend on them. Pass 0 to run with essentially no cache,
78
- * which is how the tests run so a poor access pattern can never hide
79
- * behind a warm cache.
80
- */
81
- constructor(dbPath: string, cacheSizeMb?: number);
82
- private tableExists;
83
- /** (Re)compile the hot statements against the current schema. */
84
- private prepareAll;
85
- /** Load the persisted configuration, or null for a fresh database. */
86
- loadConfig(): StoreConfig | null;
87
- /** Initialise the meta row for a new database. */
88
- initConfig(c: StoreConfig): void;
89
- loadState(): GlobalState;
90
- saveState(s: GlobalState): void;
91
- setEfSearch(ef: number): void;
92
- setQueryBits(bits: number): void;
93
- resetReads(): void;
94
- /** Stream every live external id in id order, in bounded-memory batches. */
95
- liveExts(batch?: number): IterableIterator<number>;
96
- /** Size the two slabs from the shared memory budget and the actual code
97
- * size (known on the first record cached): total slots = budget / bytes
98
- * per slot, the upper-layer slab taking half. A slot is the code bytes
99
- * plus 17 B of typed-array bookkeeping (key + ext + deleted). */
100
- private deriveSlabs;
101
- /** Record a node in the slab cache. Upper-layer nodes (level ≥ 1) go to
102
- * the pinned section layer-0 traffic never touches; a colliding slot is
103
- * simply overwritten (direct-mapped eviction — no bookkeeping). */
104
- private cacheRec;
105
- /** Insert a node, returning its assigned id (rowid). */
106
- addNode(ext: number | null, level: number, code: Uint8Array): number;
107
- /** Fetch a node's code (copied), tombstone flag and external id, or null.
108
- * Served from the slab cache when present; a miss reads SQLite, counts
109
- * one `reads` (the cache-independent scalability witness), and caches the
110
- * result. The cache is a latency layer ONLY — `reads` does not count hits, so
111
- * the index's storage-read scaling is identical with the cache off. */
112
- getNode(id: number): NodeRec | null;
113
- idByExt(ext: number): number | null;
114
- private readonly batchStmts;
115
- private static readonly BATCH_MAX;
116
- private batchStmt;
117
- /** Fetch several nodes into `out` (skipping ids already present). Serves
118
- * from the code LRU first; the misses are read with one IN query per
119
- * chunk. Ids with no row are simply absent from `out`. Each id that
120
- * reaches SQLite counts one `reads`, exactly like a getNode miss. */
121
- getNodesInto(ids: number[], out: Map<number, NodeRec>, count?: number): void;
122
- /** Pre-fill the code and neighbour-list caches with ONE sequential table
123
- * scan each, up to their existing budget-derived caps. A cold session
124
- * otherwise warms the caches through hundreds of thousands of RANDOM point
125
- * reads spread across its first minutes of inserts/queries; a sequential
126
- * scan streams the same rows at C speed in seconds. A pure latency
127
- * optimisation with the exact same caps and coherence rules as demand
128
- * filling — nothing about results, `reads` discipline, or memory ceilings
129
- * changes. Two passes over `nodes` so the upper-layer pinned section is
130
- * filled before layer-0 rows compete for the LRU. Returns rows warmed. */
131
- warmCache(): number;
132
- tombstone(id: number): void;
133
- /** A node's neighbour ids at a layer, or null if it has no list there. */
134
- getNeighbors(node: number, layer: number): Uint32Array | null;
135
- setNeighbors(node: number, layer: number, ids: number[]): void;
136
- begin(): void;
137
- commit(): void;
138
- rollback(): void;
139
- /** Encode a neighbour id list to the delta-varint blob format. */
140
- private encodeNbrs;
141
- /** Decode a delta-varint neighbour blob into a number[]. */
142
- private decodeNbrs;
143
- /** Tombstone-splice compaction. `M`/`Mmax0` cap a rewritten list on upper
144
- * layers / layer 0. Returns the new global state scalars (internal ids are
145
- * preserved, so `entry` survives unless it was itself dead). The caller
146
- * persists state and vacuums. */
147
- spliceCompact(M: number, Mmax0: number, entry: number): {
148
- entry: number;
149
- maxLevel: number;
150
- live: number;
151
- };
152
- /** Stream live external ids with internal id > `after`, in internal-id
153
- * order, in bounded batches. Internal ids are assigned monotonically and
154
- * PRESERVED by {@link spliceCompact}, so a caller can use the largest
155
- * internal id it has seen as a durable incremental watermark. */
156
- liveExtsSince(after: number, batch?: number): IterableIterator<{
157
- ext: number;
158
- internal: number;
159
- }>;
160
- vacuum(): void;
161
- close(): void;
162
- }