@hviana/sema 0.1.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.
Files changed (110) hide show
  1. package/AGENTS.md +470 -0
  2. package/AUTHORS.md +12 -0
  3. package/COMMERCIAL-LICENSE.md +19 -0
  4. package/CONTRIBUTING.md +15 -0
  5. package/HOW_IT_WORKS.md +4210 -0
  6. package/LICENSE.md +117 -0
  7. package/README.md +290 -0
  8. package/TRADEMARKS.md +19 -0
  9. package/example/demo.ts +46 -0
  10. package/example/train_base.ts +2675 -0
  11. package/index.html +893 -0
  12. package/package.json +25 -0
  13. package/src/alphabet.ts +34 -0
  14. package/src/alu/README.md +332 -0
  15. package/src/alu/src/alu.ts +541 -0
  16. package/src/alu/src/expr.ts +339 -0
  17. package/src/alu/src/index.ts +115 -0
  18. package/src/alu/src/kernel-arith.ts +377 -0
  19. package/src/alu/src/kernel-bits.ts +199 -0
  20. package/src/alu/src/kernel-logic.ts +102 -0
  21. package/src/alu/src/kernel-nd.ts +235 -0
  22. package/src/alu/src/kernel-numeric.ts +466 -0
  23. package/src/alu/src/operation.ts +344 -0
  24. package/src/alu/src/parser.ts +574 -0
  25. package/src/alu/src/resonance.ts +161 -0
  26. package/src/alu/src/text.ts +83 -0
  27. package/src/alu/src/value.ts +327 -0
  28. package/src/alu/test/alu.test.ts +1004 -0
  29. package/src/bytes.ts +62 -0
  30. package/src/config.ts +227 -0
  31. package/src/derive/README.md +295 -0
  32. package/src/derive/src/deduction.ts +263 -0
  33. package/src/derive/src/index.ts +24 -0
  34. package/src/derive/src/priority-queue.ts +70 -0
  35. package/src/derive/src/rewrite.ts +127 -0
  36. package/src/derive/src/trie.ts +242 -0
  37. package/src/derive/test/derive.test.ts +137 -0
  38. package/src/extension.ts +42 -0
  39. package/src/geometry.ts +511 -0
  40. package/src/index.ts +38 -0
  41. package/src/ingest-cache.ts +224 -0
  42. package/src/mind/articulation.ts +137 -0
  43. package/src/mind/attention.ts +1061 -0
  44. package/src/mind/canonical.ts +107 -0
  45. package/src/mind/graph-search.ts +1100 -0
  46. package/src/mind/index.ts +18 -0
  47. package/src/mind/junction.ts +347 -0
  48. package/src/mind/learning.ts +246 -0
  49. package/src/mind/match.ts +507 -0
  50. package/src/mind/mechanisms/alu.ts +33 -0
  51. package/src/mind/mechanisms/cast.ts +568 -0
  52. package/src/mind/mechanisms/confluence.ts +268 -0
  53. package/src/mind/mechanisms/cover.ts +248 -0
  54. package/src/mind/mechanisms/extraction.ts +422 -0
  55. package/src/mind/mechanisms/recall.ts +241 -0
  56. package/src/mind/mind.ts +483 -0
  57. package/src/mind/pipeline-mechanism.ts +326 -0
  58. package/src/mind/pipeline.ts +300 -0
  59. package/src/mind/primitives.ts +201 -0
  60. package/src/mind/rationale.ts +275 -0
  61. package/src/mind/reasoning.ts +198 -0
  62. package/src/mind/recognition.ts +234 -0
  63. package/src/mind/resonance.ts +0 -0
  64. package/src/mind/trace.ts +108 -0
  65. package/src/mind/traverse.ts +556 -0
  66. package/src/mind/types.ts +281 -0
  67. package/src/rabitq-hnsw/README.md +303 -0
  68. package/src/rabitq-hnsw/src/database.ts +492 -0
  69. package/src/rabitq-hnsw/src/heap.ts +90 -0
  70. package/src/rabitq-hnsw/src/hnsw.ts +514 -0
  71. package/src/rabitq-hnsw/src/index.ts +15 -0
  72. package/src/rabitq-hnsw/src/prng.ts +39 -0
  73. package/src/rabitq-hnsw/src/rabitq.ts +308 -0
  74. package/src/rabitq-hnsw/src/store.ts +994 -0
  75. package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
  76. package/src/store-sqlite.ts +928 -0
  77. package/src/store.ts +2148 -0
  78. package/src/vec.ts +124 -0
  79. package/test/00-extract.test.mjs +151 -0
  80. package/test/01-floor.test.mjs +20 -0
  81. package/test/02-roundtrip.test.mjs +83 -0
  82. package/test/03-recall.test.mjs +98 -0
  83. package/test/04-think.test.mjs +627 -0
  84. package/test/05-concepts.test.mjs +84 -0
  85. package/test/08-storage.test.mjs +948 -0
  86. package/test/09-edges.test.mjs +65 -0
  87. package/test/11-universality.test.mjs +180 -0
  88. package/test/13-conversation.test.mjs +228 -0
  89. package/test/14-scaling.test.mjs +503 -0
  90. package/test/15-decomposition-gap.test.mjs +0 -0
  91. package/test/16-bridge.test.mjs +250 -0
  92. package/test/17-intelligence.test.mjs +240 -0
  93. package/test/18-alu.test.mjs +209 -0
  94. package/test/19-nd.test.mjs +254 -0
  95. package/test/20-stability.test.mjs +489 -0
  96. package/test/21-partial.test.mjs +158 -0
  97. package/test/22-multihop.test.mjs +130 -0
  98. package/test/23-rationale.test.mjs +316 -0
  99. package/test/24-generalization.test.mjs +416 -0
  100. package/test/25-embedding.test.mjs +75 -0
  101. package/test/26-cooperative.test.mjs +89 -0
  102. package/test/27-saturation-drop.test.mjs +637 -0
  103. package/test/28-unknowable.test.mjs +88 -0
  104. package/test/29-counterfactual.test.mjs +476 -0
  105. package/test/30-conflict-resolution.test.mjs +166 -0
  106. package/test/31-audit.test.mjs +288 -0
  107. package/test/32-confluence.test.mjs +173 -0
  108. package/test/33-multi-candidate.test.mjs +222 -0
  109. package/test/34-cross-region.test.mjs +252 -0
  110. package/tsconfig.json +16 -0
@@ -0,0 +1,492 @@
1
+ import { HnswIndex } from "./hnsw.js";
2
+ import { RaBitQuantizer } from "./rabitq.js";
3
+ import { Store } from "./store.js";
4
+
5
+ /** External ids are integers only. */
6
+ export type ExternalId = number;
7
+
8
+ export interface DatabaseOptions {
9
+ /** Path to the SQLite database file (use ":memory:" for a transient store). */
10
+ dbPath: string;
11
+ /** Vector dimensionality. Required for a new database; ignored when reopening. */
12
+ dim?: number;
13
+ /** Max neighbours per node on layers >= 1 (layer 0 uses 2*M). Default 16. */
14
+ M?: number;
15
+ /** Candidate list size during construction. Default 200. */
16
+ efConstruction?: number;
17
+ /** Candidate list size during search. Default 100. Tunable at runtime. */
18
+ efSearch?: number;
19
+ /** Bits used to scalar-quantise the query for the RaBitQ estimator.
20
+ * Query-side only (stored codes and the graph are independent of it), so it
21
+ * is tunable at reopen, like efSearch. Default 8: 4 bits cannot resolve a
22
+ * tight cluster's residual and costs ~18% self-recall there (test 2a); 8
23
+ * bits restores it for the price of a Uint16 per-query LUT. */
24
+ queryBits?: number;
25
+ /** Number of sign-flip + Hadamard rounds in the random rotation. Default 3. */
26
+ rotationRounds?: number;
27
+ /** Seed for the rotation and graph levels. Default fixed. */
28
+ seed?: number;
29
+ /** Optional centroid the vectors are centered by before quantisation. */
30
+ centroid?: ArrayLike<number>;
31
+ /**
32
+ * RAM cache size in MiB -- the single memory knob. It sizes SQLite's page
33
+ * cache AND the immutable-code LRU (whose entry-capacity is derived from this
34
+ * budget and the code size — there is no second knob). Both are purely speed
35
+ * enhancements: correctness and the per-operation storage-read count are
36
+ * identical with it off. Pass 0 to run with essentially no cache. Default 64.
37
+ * Not persisted; set per open.
38
+ */
39
+ cacheSizeMb?: number;
40
+ }
41
+
42
+ export interface QueryResult {
43
+ id: ExternalId;
44
+ /** Estimated cosine distance (1 - cosine). */
45
+ distance: number;
46
+ }
47
+
48
+ export interface StorageStats {
49
+ /** Bytes a single Float32 copy of the vector would take. */
50
+ float32BytesPerVector: number;
51
+ /** Bytes of 1-bit code kept per vector (on disk). */
52
+ codeBytesPerVector: number;
53
+ /** Bytes kept per vector (just the code; excludes the graph adjacency). */
54
+ bytesPerVector: number;
55
+ /** float32BytesPerVector / bytesPerVector. */
56
+ compressionRatio: number;
57
+ }
58
+
59
+ const DEFAULTS = {
60
+ M: 16,
61
+ efConstruction: 200,
62
+ efSearch: 100,
63
+ queryBits: 8,
64
+ rotationRounds: 3,
65
+ seed: 0x1234abcd,
66
+ };
67
+
68
+ /**
69
+ * A persistent vector database: an HNSW graph over 1-bit RaBitQ codes (cosine),
70
+ * stored entirely in SQLite at `dbPath`. The graph IS the database -- there is no
71
+ * load/save step and no in-RAM copy of the data, so resident memory stays flat as
72
+ * the collection grows and the store survives process restarts. Reopening the
73
+ * same path restores the exact configuration (the rotation is regenerated from
74
+ * the persisted seed), so the codes already on disk remain valid.
75
+ *
76
+ * External ids are integers. The original float vectors are never retained --
77
+ * only the sign codes -- so a 256-d vector costs 32 bytes instead of 1024. `get`
78
+ * returns the stored code, which can be fed straight back into
79
+ * `insert`/`update`/`query`.
80
+ */
81
+ export class VectorDatabase {
82
+ readonly dim: number;
83
+
84
+ private readonly store: Store;
85
+ private readonly quantizer: RaBitQuantizer;
86
+ private readonly index: HnswIndex;
87
+ private readonly codeWords: number;
88
+
89
+ constructor(options: DatabaseOptions) {
90
+ if (
91
+ !options || typeof options.dbPath !== "string" ||
92
+ options.dbPath.length === 0
93
+ ) {
94
+ throw new Error("DatabaseOptions.dbPath (string) is required");
95
+ }
96
+ this.store = new Store(options.dbPath, options.cacheSizeMb ?? 64);
97
+
98
+ let cfg = this.store.loadConfig();
99
+ if (cfg === null) {
100
+ // Fresh database: derive the configuration from the options and persist it.
101
+ if (!Number.isInteger(options.dim) || (options.dim as number) <= 0) {
102
+ throw new Error(
103
+ "DatabaseOptions.dim must be a positive integer for a new database",
104
+ );
105
+ }
106
+ const dim = options.dim as number;
107
+ const centroid = options.centroid
108
+ ? Float64Array.from(options.centroid)
109
+ : null;
110
+ const probe = new RaBitQuantizer(dim, { rounds: 1, seed: 0 });
111
+ cfg = {
112
+ dim,
113
+ m: options.M ?? DEFAULTS.M,
114
+ efConstruction: options.efConstruction ?? DEFAULTS.efConstruction,
115
+ efSearch: options.efSearch ?? DEFAULTS.efSearch,
116
+ queryBits: options.queryBits ?? DEFAULTS.queryBits,
117
+ rotationRounds: options.rotationRounds ?? DEFAULTS.rotationRounds,
118
+ seed: (options.seed ?? DEFAULTS.seed) >>> 0,
119
+ centroid,
120
+ codeWords: probe.codeWords,
121
+ paddedDim: probe.paddedDim,
122
+ };
123
+ this.store.initConfig(cfg);
124
+ } else {
125
+ // Reopened: keep the stored STRUCTURAL config, but allow tuning the two
126
+ // query-side knobs — efSearch and queryBits shape only how a query is
127
+ // executed, never what is stored, so honouring an explicit option here
128
+ // is safe and lets an existing database adopt better query settings.
129
+ if (options.efSearch !== undefined) {
130
+ cfg.efSearch = options.efSearch;
131
+ this.store.setEfSearch(options.efSearch);
132
+ }
133
+ if (
134
+ options.queryBits !== undefined && options.queryBits !== cfg.queryBits
135
+ ) {
136
+ cfg.queryBits = options.queryBits;
137
+ this.store.setQueryBits(options.queryBits);
138
+ }
139
+ }
140
+
141
+ this.dim = cfg.dim;
142
+ this.quantizer = new RaBitQuantizer(cfg.dim, {
143
+ queryBits: cfg.queryBits,
144
+ rounds: cfg.rotationRounds,
145
+ seed: cfg.seed,
146
+ centroid: cfg.centroid ?? undefined,
147
+ });
148
+ if (this.quantizer.codeWords !== cfg.codeWords) {
149
+ throw new Error(
150
+ "Stored code geometry does not match the quantizer (corrupt database?)",
151
+ );
152
+ }
153
+ this.codeWords = this.quantizer.codeWords;
154
+ this.index = new HnswIndex(this.quantizer, this.store, {
155
+ M: cfg.m,
156
+ efConstruction: cfg.efConstruction,
157
+ efSearch: cfg.efSearch,
158
+ seed: cfg.seed,
159
+ });
160
+ }
161
+
162
+ /** Number of live (non-deleted) vectors. */
163
+ get size(): number {
164
+ return this.index.size;
165
+ }
166
+
167
+ /**
168
+ * Physical node count including tombstones from deletes/updates. When it grows
169
+ * well beyond `size`, call `compact()` to reclaim the space on disk.
170
+ */
171
+ get physicalSize(): number {
172
+ return this.index.physicalSize;
173
+ }
174
+
175
+ get efSearch(): number {
176
+ return this.index.efSearch;
177
+ }
178
+ set efSearch(value: number) {
179
+ this.index.efSearch = value;
180
+ }
181
+
182
+ /** Distance computations performed during the most recent query. */
183
+ get lastQueryDistanceComputations(): number {
184
+ return this.index.lastQueryDistComps;
185
+ }
186
+
187
+ /**
188
+ * Storage row reads issued by the most recent query. This is the honest,
189
+ * cache-independent scalability metric: it counts every node/neighbour fetch
190
+ * that hit the database, so disabling the cache cannot hide a bad access pattern.
191
+ */
192
+ get lastQueryStorageReads(): number {
193
+ return this.index.lastQueryStorageReads;
194
+ }
195
+
196
+ /** Per-vector storage cost of the index versus a Float32 baseline. */
197
+ get storage(): StorageStats {
198
+ const f32 = this.dim * 4;
199
+ const bpv = this.index.bytesPerVector;
200
+ return {
201
+ float32BytesPerVector: f32,
202
+ codeBytesPerVector: bpv,
203
+ bytesPerVector: bpv,
204
+ compressionRatio: f32 / bpv,
205
+ };
206
+ }
207
+
208
+ has(id: ExternalId): boolean {
209
+ return this.store.idByExt(this.checkId(id)) !== null;
210
+ }
211
+
212
+ /** Stream every live external id (bounded memory). */
213
+ *keys(): IterableIterator<ExternalId> {
214
+ yield* this.store.liveExts();
215
+ }
216
+
217
+ /**
218
+ * Stream live entries whose INTERNAL id is > `after`, as
219
+ * {ext, internal} pairs in internal-id order. Internal ids are assigned
220
+ * monotonically at insert and preserved by {@link compact}, so the largest
221
+ * internal id a caller has seen is a durable incremental watermark: a later
222
+ * call with it yields exactly the entries added since.
223
+ */
224
+ *keysSince(
225
+ after: number,
226
+ ): IterableIterator<{ ext: ExternalId; internal: number }> {
227
+ yield* this.store.liveExtsSince(after);
228
+ }
229
+
230
+ private checkId(id: ExternalId): number {
231
+ if (!Number.isInteger(id)) {
232
+ throw new Error(`External id must be an integer, got ${id}`);
233
+ }
234
+ return id;
235
+ }
236
+
237
+ /**
238
+ * Convert a value to code bytes, selecting by length:
239
+ * - `codeWords` elements -> an existing 1-bit code (e.g. from `get()`)
240
+ * - otherwise -> a raw `dim`-vector, encoded first
241
+ * `codeWords < dim` for any dim >= 2, so the two never collide.
242
+ */
243
+ private toCodeBytes(value: ArrayLike<number>): Uint8Array {
244
+ if (value.length === this.codeWords) {
245
+ return this.quantizer.codeToBytes(value);
246
+ }
247
+ if (value.length !== this.dim) {
248
+ throw new Error(
249
+ `Vector dimension mismatch: expected ${this.dim}, got ${value.length}`,
250
+ );
251
+ }
252
+ return this.quantizer.codeToBytes(this.quantizer.encode(value));
253
+ }
254
+
255
+ // ------------------------------- CRUD ------------------------------------
256
+
257
+ /**
258
+ * Create. Accepts a raw `dim`-vector or a 1-bit code (`codeWords` words),
259
+ * detected by length. Throws if the id already exists (use `update`/`upsert`).
260
+ */
261
+ insert(id: ExternalId, value: ArrayLike<number>): void {
262
+ this.store.begin();
263
+ try {
264
+ this.insertCore(id, value);
265
+ this.store.commit();
266
+ } catch (e) {
267
+ this.store.rollback();
268
+ throw e;
269
+ }
270
+ }
271
+
272
+ /** Insert with the caller owning the transaction (used by {@link upsertMany}).
273
+ * `ef` optionally narrows the construction beam for this one vector (a
274
+ * caller-declared cheap entry, e.g. a reach-only interior); omitted means
275
+ * the index's configured efConstruction. */
276
+ private insertCore(
277
+ id: ExternalId,
278
+ value: ArrayLike<number>,
279
+ ef?: number,
280
+ ): void {
281
+ this.checkId(id);
282
+ if (this.store.idByExt(id) !== null) {
283
+ throw new Error(`External id already exists: ${id} (use update())`);
284
+ }
285
+ this.index.insert(id, this.toCodeBytes(value), ef);
286
+ }
287
+
288
+ upsert(id: ExternalId, value: ArrayLike<number>): void {
289
+ const nodeId = this.store.idByExt(this.checkId(id));
290
+ if (nodeId === null) {
291
+ this.insert(id, value);
292
+ return;
293
+ }
294
+ this.store.begin();
295
+ try {
296
+ this.updateAt(nodeId, id, value);
297
+ this.store.commit();
298
+ } catch (e) {
299
+ this.store.rollback();
300
+ throw e;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Upsert many vectors under ONE transaction. The HNSW build touches the store
306
+ * on every wired edge, so a transaction per vector is one WAL commit per vector
307
+ * — which dominates a bulk load on disk. Wrapping the whole batch in a single
308
+ * transaction coalesces those commits into one while leaving the graph and its
309
+ * result identical (reads on the connection still see the uncommitted rows).
310
+ *
311
+ * This is purely about commit batching; it changes nothing about the storage
312
+ * model. Codes still live only in SQLite and are read on demand — the
313
+ * cache-independent per-operation read count is unchanged — so a batched load
314
+ * scales exactly as the per-item path does, just with far fewer fsyncs. A throw
315
+ * rolls the whole batch back, so the caller treats it as the per-item path on
316
+ * failure.
317
+ */
318
+ upsertMany(
319
+ entries: Array<{ id: ExternalId; vector: ArrayLike<number>; ef?: number }>,
320
+ ): void {
321
+ if (entries.length === 0) return;
322
+ this.store.begin();
323
+ try {
324
+ for (const e of entries) {
325
+ const nodeId = this.store.idByExt(this.checkId(e.id));
326
+ if (nodeId !== null) this.updateAt(nodeId, e.id, e.vector, e.ef);
327
+ else this.insertCore(e.id, e.vector, e.ef);
328
+ }
329
+ this.store.commit();
330
+ } catch (e) {
331
+ this.store.rollback();
332
+ throw e;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Read the stored 1-bit code for an id (a copy as a Uint32Array), or null. The
338
+ * original vector is not retained; the code round-trips into insert/update/query.
339
+ */
340
+ get(id: ExternalId): Uint32Array | null {
341
+ const nodeId = this.store.idByExt(this.checkId(id));
342
+ if (nodeId === null) return null;
343
+ const rec = this.store.getNode(nodeId);
344
+ return rec ? this.quantizer.bytesToCode(rec.code) : null;
345
+ }
346
+
347
+ /**
348
+ * Update the vector bound to an id (raw vector or code, detected by length).
349
+ * The previous node is tombstoned and a fresh one inserted. Throws if absent.
350
+ */
351
+ update(id: ExternalId, value: ArrayLike<number>): void {
352
+ this.store.begin();
353
+ try {
354
+ this.updateCore(id, value);
355
+ this.store.commit();
356
+ } catch (e) {
357
+ this.store.rollback();
358
+ throw e;
359
+ }
360
+ }
361
+
362
+ /** Update with the caller owning the transaction (used by {@link upsertMany}). */
363
+ private updateCore(id: ExternalId, value: ArrayLike<number>): void {
364
+ const oldId = this.store.idByExt(this.checkId(id));
365
+ if (oldId === null) throw new Error(`Unknown external id: ${id}`);
366
+ this.updateAt(oldId, id, value);
367
+ }
368
+
369
+ /** Update when the live internal node id is already known.
370
+ *
371
+ * Skips the write entirely when the new code equals the stored one — the
372
+ * update would tombstone the node and replay a full graph insert for a
373
+ * byte-identical result. This is not a corner case: content-addressed
374
+ * callers re-upsert unchanged vectors wholesale after a restart (the same
375
+ * content always encodes to the same code), and each such no-op otherwise
376
+ * costs a tombstone (permanent routing/disk overhead until compaction)
377
+ * plus an O(ef·log N) reinsert. */
378
+ private updateAt(
379
+ nodeId: number,
380
+ ext: ExternalId,
381
+ value: ArrayLike<number>,
382
+ ef?: number,
383
+ ): void {
384
+ const bytes = this.toCodeBytes(value);
385
+ const rec = this.store.getNode(nodeId);
386
+ if (rec !== null && rec.code.length === bytes.length) {
387
+ let same = true;
388
+ for (let i = 0; i < bytes.length; i++) {
389
+ if (rec.code[i] !== bytes[i]) {
390
+ same = false;
391
+ break;
392
+ }
393
+ }
394
+ if (same) return; // identical code — the update is a no-op
395
+ }
396
+ this.index.remove(nodeId); // frees the ext id, then re-insert under it
397
+ this.index.insert(ext, bytes, ef);
398
+ }
399
+
400
+ /**
401
+ * Delete many ids under ONE transaction — same commit-coalescing rationale
402
+ * as {@link upsertMany}: a tombstone per implicit transaction is one WAL
403
+ * commit per id, which dominates a bulk prune. Absent ids are skipped.
404
+ * Returns the number of vectors actually removed.
405
+ */
406
+ deleteMany(ids: ExternalId[]): number {
407
+ if (ids.length === 0) return 0;
408
+ let removed = 0;
409
+ this.store.begin();
410
+ try {
411
+ for (const id of ids) {
412
+ const nodeId = this.store.idByExt(this.checkId(id));
413
+ if (nodeId !== null && this.index.remove(nodeId)) removed++;
414
+ }
415
+ this.store.commit();
416
+ } catch (e) {
417
+ this.store.rollback();
418
+ throw e;
419
+ }
420
+ return removed;
421
+ }
422
+
423
+ /** Delete the vector bound to an id. Returns false if absent. */
424
+ delete(id: ExternalId): boolean {
425
+ const nodeId = this.store.idByExt(this.checkId(id));
426
+ if (nodeId === null) return false;
427
+ this.store.begin();
428
+ try {
429
+ this.index.remove(nodeId);
430
+ this.store.commit();
431
+ } catch (e) {
432
+ this.store.rollback();
433
+ throw e;
434
+ }
435
+ return true;
436
+ }
437
+
438
+ /**
439
+ * Pre-fill the RAM caches (codes + neighbour lists) with sequential table
440
+ * scans, up to their budget-derived caps. Optional and purely a latency
441
+ * optimisation: a cold session otherwise pays the same warming through
442
+ * random point reads over its first minutes. Call once after open on a
443
+ * session that will do sustained inserts/queries. Returns rows warmed;
444
+ * a 0-budget database returns 0 immediately.
445
+ */
446
+ warmCache(): number {
447
+ return this.store.warmCache();
448
+ }
449
+
450
+ /**
451
+ * Rebuild the graph from the live codes only, dropping tombstones and returning
452
+ * the freed pages to the filesystem. Lossless -- equivalent to the original
453
+ * build -- and needs no original vectors.
454
+ */
455
+ compact(): void {
456
+ this.index.compact();
457
+ }
458
+
459
+ // ------------------------------ search -----------------------------------
460
+
461
+ /**
462
+ * k-NN search. The argument's length selects the mode:
463
+ * - `dim` elements -> a raw vector (accurate 4-bit-query estimator)
464
+ * - `codeWords` elements -> a 1-bit code (e.g. `get(id)`), by sign-bit Hamming
465
+ * Detection is by length, so a code is recognised whether it is a Uint32Array
466
+ * or a plain number[]. `codeWords < dim` for any dim >= 2, so they never collide.
467
+ */
468
+ query(
469
+ query: ArrayLike<number>,
470
+ k = 10,
471
+ opts?: { ef?: number },
472
+ ): QueryResult[] {
473
+ if (query.length === this.codeWords) {
474
+ return this.index.searchKnnByCode(
475
+ this.quantizer.codeToBytes(query),
476
+ k,
477
+ opts?.ef,
478
+ );
479
+ }
480
+ if (query.length !== this.dim) {
481
+ throw new Error(
482
+ `Vector dimension mismatch: expected ${this.dim}, got ${query.length}`,
483
+ );
484
+ }
485
+ return this.index.searchKnn(query, k, opts?.ef);
486
+ }
487
+
488
+ /** Close the underlying database. The instance must not be used afterwards. */
489
+ close(): void {
490
+ this.store.close();
491
+ }
492
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Binary heap over two parallel numeric arrays storing (key, value) pairs.
3
+ *
4
+ * When `minHeap` is true the smallest key sits at the root, otherwise the
5
+ * largest. Keys are distances, values are integer node ids. Parallel plain
6
+ * arrays are used (rather than an array of objects) to avoid per-element
7
+ * allocation in the search hot path.
8
+ */
9
+ export class Heap {
10
+ readonly keys: number[] = [];
11
+ readonly vals: number[] = [];
12
+ private readonly minHeap: boolean;
13
+
14
+ constructor(minHeap: boolean) {
15
+ this.minHeap = minHeap;
16
+ }
17
+
18
+ get size(): number {
19
+ return this.keys.length;
20
+ }
21
+
22
+ clear(): void {
23
+ this.keys.length = 0;
24
+ this.vals.length = 0;
25
+ }
26
+
27
+ topKey(): number {
28
+ return this.keys[0];
29
+ }
30
+
31
+ topVal(): number {
32
+ return this.vals[0];
33
+ }
34
+
35
+ /** true if `a` belongs closer to the root than `b`. */
36
+ private higher(a: number, b: number): boolean {
37
+ return this.minHeap ? a < b : a > b;
38
+ }
39
+
40
+ push(key: number, val: number): void {
41
+ const keys = this.keys;
42
+ const vals = this.vals;
43
+ let i = keys.length;
44
+ keys.push(key);
45
+ vals.push(val);
46
+ while (i > 0) {
47
+ const parent = (i - 1) >> 1;
48
+ if (this.higher(keys[i], keys[parent])) {
49
+ const tk = keys[i];
50
+ keys[i] = keys[parent];
51
+ keys[parent] = tk;
52
+ const tv = vals[i];
53
+ vals[i] = vals[parent];
54
+ vals[parent] = tv;
55
+ i = parent;
56
+ } else break;
57
+ }
58
+ }
59
+
60
+ pop(): void {
61
+ const keys = this.keys;
62
+ const vals = this.vals;
63
+ const n = keys.length;
64
+ if (n === 0) return;
65
+ const lastKey = keys[n - 1];
66
+ const lastVal = vals[n - 1];
67
+ keys.pop();
68
+ vals.pop();
69
+ const m = keys.length;
70
+ if (m === 0) return;
71
+ keys[0] = lastKey;
72
+ vals[0] = lastVal;
73
+ let i = 0;
74
+ while (true) {
75
+ const left = 2 * i + 1;
76
+ const right = left + 1;
77
+ let best = i;
78
+ if (left < m && this.higher(keys[left], keys[best])) best = left;
79
+ if (right < m && this.higher(keys[right], keys[best])) best = right;
80
+ if (best === i) break;
81
+ const tk = keys[i];
82
+ keys[i] = keys[best];
83
+ keys[best] = tk;
84
+ const tv = vals[i];
85
+ vals[i] = vals[best];
86
+ vals[best] = tv;
87
+ i = best;
88
+ }
89
+ }
90
+ }