@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/AGENTS.md CHANGED
@@ -44,7 +44,7 @@ store.ts AbstractStore: ALL domain logic of the DAG store
44
44
  store-sqlite.ts the one concrete backend (thin SQL wrappers)
45
45
  geometry.ts + vec/alphabet/sema
46
46
  vectors, the fold, and every derived threshold
47
- derive/ · alu/ · rabitq-hnsw/
47
+ derive/ · alu/ · rabitq-ivf/
48
48
  firewalled sublibraries with their own READMEs and tests
49
49
  ```
50
50
 
@@ -80,7 +80,8 @@ D, the perception window W, or the corpus size N, defined once in
80
80
  `src/geometry.ts` (`mergeThreshold`, `identityBar`, `reachThreshold`,
81
81
  `significanceBar`, `estimatorNoise`, `conceptThreshold`, `consensusFloor`,
82
82
  `dominates`, …). `src/config.ts` holds **capacities and budgets only** (cache
83
- byte budgets, batch sizes, HNSW parameters, query k, ALU precision, the seed).
83
+ byte budgets, batch sizes, vector-index parameters, query k, ALU precision, the
84
+ seed).
84
85
 
85
86
  _Follow it:_ if you are about to add a tunable cutoff to config, derive it in
86
87
  `geometry.ts` instead. A threshold knob is a design bug here — one was already
@@ -360,7 +361,7 @@ story of the fix.
360
361
  | Post-grounding (reason, fuse, articulate) | `src/mind/reasoning.ts`, `src/mind/articulation.ts` |
361
362
  | Rationale / trace | `src/mind/rationale.ts`, `src/mind/trace.ts` |
362
363
  | Extension host types | `src/extension.ts` |
363
- | Sublibraries (own READMEs, own tests) | `src/derive/`, `src/alu/`, `src/rabitq-hnsw/` |
364
+ | Sublibraries (own READMEs, own tests) | `src/derive/`, `src/alu/`, `src/rabitq-ivf/` |
364
365
 
365
366
  Mind functions are **free functions over `MindContext`** (`mind/types.ts`), not
366
367
  methods — `mind.ts` is a thin assembly that implements the context and
@@ -453,9 +454,9 @@ against the built `dist/` (`npm test`; one suite:
453
454
  silence). A "simplification" that fails an existing test is wrong until you
454
455
  can argue the _test_ is wrong — several guards exist precisely because a
455
456
  plausible simplification once failed a dozen suites.
456
- - Sublibraries test themselves (`src/{alu,derive,rabitq-hnsw}/test/`) with zero
457
+ - Sublibraries test themselves (`src/{alu,derive,rabitq-ivf}/test/`) with zero
457
458
  Sema dependency. Keep it so.
458
- - Performance claims are tested (the rabitq-hnsw benchmark asserts sub-linear
459
+ - Performance claims are tested (the rabitq-ivf benchmark asserts sub-linear
459
460
  scaling and compression). Changing index behaviour means running it.
460
461
 
461
462
  ---
package/CITATION.cff ADDED
@@ -0,0 +1,49 @@
1
+ cff-version: 1.2.0
2
+ message: >-
3
+ If you use Sema in academic work, please cite the accompanying preprint.
4
+ For software-specific references, you may also cite this repository.
5
+ title: "Sema"
6
+ abstract: >-
7
+ Sema is a non-parametric, instance-based reasoning system built as a
8
+ Vector Symbolic Architecture over content-addressable memory, with
9
+ inference performed by weighted automated deduction.
10
+ type: software
11
+ authors:
12
+ - family-names: "Viana"
13
+ given-names: "Henrique Emanoel"
14
+ orcid: "https://orcid.org/0000-0002-7119-5867"
15
+ affiliation: "NONE"
16
+ repository-code: "https://github.com/hviana/sema"
17
+ url: "https://github.com/hviana/sema"
18
+ license: "PolyForm-Noncommercial-1.0.0"
19
+ version: "0.1.0"
20
+ date-released: "2026-07-16"
21
+ keywords:
22
+ - artificial intelligence
23
+ - cognitive architecture
24
+ - non-parametric reasoning
25
+ - instance-based reasoning
26
+ - vector symbolic architecture
27
+ - hyperdimensional computing
28
+ - content-addressable memory
29
+ - weighted automated deduction
30
+ - symbolic reasoning
31
+ - explainable artificial intelligence
32
+ - TypeScript
33
+
34
+ preferred-citation:
35
+ type: article
36
+ title: "Sema: Reasoning by Example over a Vector-Symbolic Memory"
37
+ authors:
38
+ - family-names: "Viana"
39
+ given-names: "Henrique Emanoel"
40
+ orcid: "https://orcid.org/0000-0002-7119-5867"
41
+ affiliation: "NONE"
42
+ year: 2026
43
+ month: 7
44
+ day: 16
45
+ version: "0.1.0"
46
+ doi: "10.5281/zenodo.21385723"
47
+ url: "https://doi.org/10.5281/zenodo.21385723"
48
+ publisher:
49
+ name: "Zenodo"
package/HOW_IT_WORKS.md CHANGED
@@ -575,11 +575,13 @@ controlled amount of recall for sub-linear query time.
575
575
 
576
576
  Sema uses two established techniques in combination:
577
577
 
578
- - **HNSW** — Hierarchical Navigable Small World graphs (Malkov & Yashunin 2018).
579
- Vectors are connected into a multi-layer proximity graph; a query descends
580
- greedily from a sparse top layer to a dense bottom layer, examining only a
581
- logarithmic-ish neighbourhood of the collection. Empirically the work grows
582
- around N^0.3 in Sema's configuration — decisively sub-linear.
578
+ - **IVF (inverted-file partitioning)** — the collection is split into clusters,
579
+ each with a binary pivot code; a query ranks the pivots and scans only the few
580
+ nearest clusters. Cluster size is bounded (an oversized cluster
581
+ deterministically splits in two), so the work per query is set by the number
582
+ of probes, not by the collection — decisively sub-linear. Inserting is
583
+ route-and-append: one RAM scan of the pivot table, no graph maintenance, so
584
+ ingestion cost stays flat as the collection grows.
583
585
  - **RaBitQ 1-bit quantization** (Gao & Long 2024). Each stored vector is
584
586
  randomly rotated and reduced to one _sign bit_ per dimension — a 32×
585
587
  compression — with an unbiased, theoretically-grounded estimator of the
@@ -663,7 +665,7 @@ RELATIONS │ continuation edges (what follows what) │
663
665
  (distributional, §4) │ halos (what company each node keeps) │
664
666
  ├─────────────────────────────────────────┤
665
667
  INDEXES │ gist index + halo index │
666
- (ANN, §6) │ (HNSW over 1-bit RaBitQ codes)
668
+ (ANN, §6) │ (IVF over 1-bit RaBitQ codes)
667
669
  └──────────────────┬──────────────────────┘
668
670
  │ axioms & rule candidates
669
671
 
@@ -1696,7 +1698,7 @@ requirements:
1696
1698
 
1697
1699
  - **ANN index: 1-bit RaBitQ, irreversible.** The same halo vector, projected
1698
1700
  through a random rotation and reduced to one sign bit per dimension (32×
1699
- compression), serves as a search code in the HNSW index. This code is _never_
1701
+ compression), serves as a search code in the IVF index. This code is _never_
1700
1702
  decoded back — it only answers "which halos are near this query?" The
1701
1703
  estimator is unbiased (expected cosine is recoverable from the bit count), so
1702
1704
  ranking quality is preserved despite the loss of reversibility.
@@ -4124,8 +4126,8 @@ terms of art borrowed from the literature.
4124
4126
  §4.
4125
4127
  - **Hash-consing** — constructing structures modulo equality so equal
4126
4128
  substructures are shared. §3.2.
4127
- - **HNSW** — Hierarchical Navigable Small World graph; sub-linear ANN search
4128
- (Malkov & Yashunin 2018). §6.1.
4129
+ - **IVF** — inverted-file partitioned ANN index; bounded-probe sub-linear search
4130
+ over clustered codes. §6.1.
4129
4131
  - **Hilbert curve** — the locality-preserving space-filling curve used to
4130
4132
  linearize grids. §6.3.
4131
4133
  - **Hyperdimensional computing** — Kanerva's (2009) umbrella term for computing
@@ -4193,9 +4195,6 @@ Foundations cited in this document, in alphabetical order:
4193
4195
  Processing Letters 6(1), 1–5.
4194
4196
  - Kolodner, J. L. (1992). _An Introduction to Case-Based Reasoning._ Artificial
4195
4197
  Intelligence Review 6, 3–34.
4196
- - Malkov, Y. A. & Yashunin, D. A. (2018). _Efficient and Robust Approximate
4197
- Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs._ IEEE
4198
- TPAMI 42(4), 824–836.
4199
4198
  - Merkle, R. C. (1987). _A Digital Signature Based on a Conventional Encryption
4200
4199
  Function._ Proc. CRYPTO.
4201
4200
  - Plate, T. A. (1995). _Holographic Reduced Representations._ IEEE Transactions
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![npm version](https://img.shields.io/npm/v/@hviana/sema.svg)](https://www.npmjs.com/package/@hviana/sema)
2
+
1
3
  <div align="center">
2
4
 
3
5
  ```
@@ -223,11 +225,11 @@ Pablo Picasso co-founded the Cubist movement
223
225
  Sema is composed of three self-contained, independently documented engines. They
224
226
  are fully decoupled — Sema reaches them only through interfaces.
225
227
 
226
- | Engine | What it solves | Result |
227
- | :---------------- | :----------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- |
228
- | **`rabitq-hnsw`** | _"Given a vector, find the nodes whose gist resonates with it — fast, at scale, on disk."_ | HNSW over 1-bit RaBitQ codes · ~32× compression · bounded RAM · sub-linear queries |
229
- | **`derive`** | _"Explore a huge implicit space of derivations and return the single lightest one."_ | adapted A\*LD (adapted A\* Lightest Derivation) over a weighted deduction hypergraph — Sema's thinking _is_ one call to this |
230
- | **`alu`** | _"Compute, exactly and symbolically, the things that are rules, not facts."_ | A tiny irreducible kernel from which arithmetic, logic, and n-dimensional computation are derived |
228
+ | Engine | What it solves | Result |
229
+ | :--------------- | :----------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- |
230
+ | **`rabitq-ivf`** | _"Given a vector, find the nodes whose gist resonates with it — fast, at scale, on disk."_ | Partitioned (IVF) index over 1-bit RaBitQ codes · ~32× compression · bounded RAM · flat inserts, bounded queries |
231
+ | **`derive`** | _"Explore a huge implicit space of derivations and return the single lightest one."_ | adapted A\*LD (adapted A\* Lightest Derivation) over a weighted deduction hypergraph — Sema's thinking _is_ one call to this |
232
+ | **`alu`** | _"Compute, exactly and symbolically, the things that are rules, not facts."_ | A tiny irreducible kernel from which arithmetic, logic, and n-dimensional computation are derived |
231
233
 
232
234
  ---
233
235
 
@@ -1298,20 +1298,23 @@ async function saveProgress(store, p) {
1298
1298
  // §10 Main
1299
1299
  // ═══════════════════════════════════════════════════════════════════════
1300
1300
  async function main() {
1301
- // The vector indices' single memory knob (MiB) — sizes both the SQLite page
1302
- // cache and the decoded-code LRU of each index. Training is HNSW-insert
1303
- // bound at scale (profiled at >80% of ingest wall-clock on a 5.5M-vector
1304
- // store, nearly all of it cache-missing code reads), and inserts walk the
1305
- // graph, so a cache that covers the indexed codes cuts wall-clock directly
1306
- // (measured ~1. at 1 GB vs 64 MB on a 2M-node store). 1 GB caches ~5.8M
1307
- // D=1024 codes the whole content index of a 350 MB-corpus store — and is
1308
- // the default; override with VECTOR_CACHE_MB (e.g. 256 on a small-RAM
1309
- // machine, 64 for the library default).
1310
- const VECTOR_CACHE_MB = Math.max(0, Number(env("VECTOR_CACHE_MB", "1024")));
1301
+ // The vector indices' memory knob (MiB) — each index's SQLite page cache.
1302
+ // The IVF index routes inserts through a RAM-resident pivot table and
1303
+ // appends to chunk blobs, so this cache mostly serves query-time cluster
1304
+ // scans; 256 MiB comfortably covers the probed working set of a trained
1305
+ // store. Override with VECTOR_CACHE_MB (64 is the library default).
1306
+ const VECTOR_CACHE_MB = Math.max(0, Number(env("VECTOR_CACHE_MB", "256")));
1307
+ // Page cache for the MAIN DAG database (node/kid/edge/contain tables).
1308
+ // Training issues millions of content-addressed point probes per session
1309
+ // against a GB-scale file; the library default (64 MiB) is sized for a
1310
+ // small machine — a training box affords more. Override with
1311
+ // SQLITE_CACHE_MB.
1312
+ const SQLITE_CACHE_MB = Math.max(0, Number(env("SQLITE_CACHE_MB", "256")));
1311
1313
  const store = new SQliteStore({
1312
1314
  path: DB_PATH,
1313
1315
  D,
1314
1316
  vectorCacheMb: VECTOR_CACHE_MB,
1317
+ sqliteCacheMb: SQLITE_CACHE_MB,
1315
1318
  });
1316
1319
  // The store IS the model: memories, progress, and metadata all persist in
1317
1320
  // it, so a resumed run just reopens the same store and continues. Guard
@@ -1,7 +1,7 @@
1
1
  // alu — the ALU sub-library: a tiny irreducible kernel from which arithmetic,
2
2
  // logic, and numerical computation are all DERIVED.
3
3
  //
4
- // Self-contained in the spirit of derive/ and rabitq-hnsw/: it imports only the
4
+ // Self-contained in the spirit of derive/ and rabitq-ivf/: it imports only the
5
5
  // pure byte helpers (../../bytes.js) and nothing else from SEMA. The host
6
6
  // reaches meaning (operator synonymy, the symbolic inverse) through the injected
7
7
  // AluResonance, pre-resolved into a synchronous snapshot before the search runs.
@@ -26,18 +26,9 @@ export interface AlphabetConfig {
26
26
  }
27
27
  export interface StoreConfig {
28
28
  minHaloMass: number;
29
- m: number;
30
- efConstruction: number;
31
- /** Construction budget for REACH-ONLY interior gists the ~90% of content
32
- * index inserts that exist so a partial query can resonate a sub-region
33
- * and climb, never as dedup targets. Deposit roots and halo-bearing
34
- * targets always build with the full `efConstruction`. A smaller budget
35
- * here is the one deliberate speed-for-quality trade in ingestion (an
36
- * interior's layer-0 wiring is built from a narrower candidate beam);
37
- * the recall suite (partial recall, multi-topic attention, counterfactual
38
- * anchoring) is the gate for its value. Set equal to `efConstruction`
39
- * to disable the trade. */
40
- efConstructionInterior: number;
29
+ /** Query breadth of the IVF vector indices: clusters probed per query =
30
+ * ceil(efSearch / 4). Inserts have no quality knob — the partitioned
31
+ * index routes and appends, so ingestion cost is flat by construction. */
41
32
  efSearch: number;
42
33
  /** Compact the in-memory vector indices after this many vectors are written.
43
34
  * Compaction rebuilds an index from its live codes to reclaim the slots left
@@ -45,7 +36,7 @@ export interface StoreConfig {
45
36
  * (not on a flush count that goes quiet during repeat-heavy training) keeps
46
37
  * the index dense and query cost bounded. */
47
38
  compactEveryNWrites: number;
48
- /** Over-fetch factor for HNSW queries (ANN recall cushion). */
39
+ /** Over-fetch factor for vector-index queries (ANN recall cushion). */
49
40
  overfetch: number;
50
41
  /** Combined buffered-write ceiling before a flush of both vector indices
51
42
  * (content + halo). Higher ⇒ fewer, larger flushes into the in-memory
@@ -78,14 +69,20 @@ export interface StoreConfig {
78
69
  * round-trips through the quantizer. An eviction or a reopen reads the
79
70
  * 2-bit row — the fidelity every cross-session consumer already gets. */
80
71
  haloCacheBytes: number;
81
- /** Size, in MiB, of each `rabitq-hnsw` `VectorDatabase`'s memory budget
82
- * (forwarded as its `cacheSizeMb`). It is the index's SINGLE memory knob: it
83
- * sizes both SQLite's page cache and the derived immutable-code LRU. A PURE
84
- * latency optimisation the index reads codes from SQLite on demand, so its
85
- * correctness and its asymptotic per-operation storage-read count are identical
86
- * with the budget at 0. Exposed so a scaling test can set it to 0 and measure
72
+ /** Size, in MiB, of each `rabitq-ivf` `VectorDatabase`'s memory budget
73
+ * (forwarded as its `cacheSizeMb` its SQLite page cache). A PURE latency
74
+ * optimisation the index reads chunk blobs from SQLite on demand, so its
75
+ * correctness and its per-operation storage-read count are identical with
76
+ * the budget at 0. Exposed so a scaling test can set it to 0 and measure
87
77
  * the honest, cache-independent cost. */
88
78
  vectorCacheMb: number;
79
+ /** Size, in MiB, of the MAIN DAG database's SQLite page cache. The node /
80
+ * kid / edge / contain tables serve millions of point probes per training
81
+ * session (content-addressed findLeaf/findBranch, parent probes, contain
82
+ * appends); SQLite's default cache (~2 MiB) thrashes once the DB outgrows
83
+ * it, so every probe pays a file read. A PURE latency knob — correctness
84
+ * and result identical at any value. */
85
+ sqliteCacheMb: number;
89
86
  /** Max entries in the skipped-interior LRU set. Interiors that
90
87
  * {@link Store.indexSubtree} has already visited (indexed or skipped) are
91
88
  * remembered here so subsequent calls prune their subtrees. Session-local
@@ -22,9 +22,6 @@ export const DEFAULT_CONFIG = {
22
22
  },
23
23
  store: {
24
24
  minHaloMass: 1,
25
- m: 8,
26
- efConstruction: 64,
27
- efConstructionInterior: 16,
28
25
  efSearch: 64,
29
26
  compactEveryNWrites: 50_000,
30
27
  overfetch: 4,
@@ -36,6 +33,7 @@ export const DEFAULT_CONFIG = {
36
33
  pendingGistBytes: 16_000_000,
37
34
  haloCacheBytes: 16_000_000,
38
35
  vectorCacheMb: 64,
36
+ sqliteCacheMb: 64,
39
37
  coveredIdsMax: 100_000,
40
38
  chainCacheBytes: 16_000_000,
41
39
  },
@@ -63,11 +61,6 @@ export function resolveConfig(opts = {}) {
63
61
  },
64
62
  store: {
65
63
  minHaloMass: opts.store?.minHaloMass ?? DEFAULT_CONFIG.store.minHaloMass,
66
- m: opts.store?.m ?? DEFAULT_CONFIG.store.m,
67
- efConstruction: opts.store?.efConstruction ??
68
- DEFAULT_CONFIG.store.efConstruction,
69
- efConstructionInterior: opts.store?.efConstructionInterior ??
70
- DEFAULT_CONFIG.store.efConstructionInterior,
71
64
  efSearch: opts.store?.efSearch ?? DEFAULT_CONFIG.store.efSearch,
72
65
  compactEveryNWrites: opts.store?.compactEveryNWrites ??
73
66
  DEFAULT_CONFIG.store.compactEveryNWrites,
@@ -87,6 +80,8 @@ export function resolveConfig(opts = {}) {
87
80
  DEFAULT_CONFIG.store.haloCacheBytes,
88
81
  vectorCacheMb: opts.store?.vectorCacheMb ??
89
82
  DEFAULT_CONFIG.store.vectorCacheMb,
83
+ sqliteCacheMb: opts.store?.sqliteCacheMb ??
84
+ DEFAULT_CONFIG.store.sqliteCacheMb,
90
85
  coveredIdsMax: opts.store?.coveredIdsMax ??
91
86
  DEFAULT_CONFIG.store.coveredIdsMax,
92
87
  chainCacheBytes: opts.store?.chainCacheBytes ??
@@ -10,6 +10,6 @@ export * from "./store-sqlite.js";
10
10
  export * from "./config.js";
11
11
  export * from "./extension.js";
12
12
  export * from "./ingest-cache.js";
13
- export { Heap, HnswIndex, Prng, RaBitQuantizer, VectorDatabase, } from "./rabitq-hnsw/src/index.js";
14
- export type { DatabaseOptions, ExternalId, HnswParams, KnnHit, QueryContext, QueryResult, RaBitQOptions, StorageStats, } from "./rabitq-hnsw/src/index.js";
13
+ export { IvfIndex, Prng, RaBitQuantizer, VectorDatabase, } from "./rabitq-ivf/src/index.js";
14
+ export type { DatabaseOptions, ExternalId, IvfConfig, IvfHit, QueryContext, QueryResult, RaBitQOptions, StorageStats, } from "./rabitq-ivf/src/index.js";
15
15
  export * from "./derive/src/index.js";
package/dist/src/index.js CHANGED
@@ -12,10 +12,6 @@ export * from "./store-sqlite.js";
12
12
  export * from "./config.js";
13
13
  export * from "./extension.js";
14
14
  export * from "./ingest-cache.js";
15
- // rabitq-hnsw is re-exported selectively: its `Store`, `NodeRec`, and
16
- // `StoreConfig` are internal names that collide with sema's own top-level
17
- // `store.js` / `config.js` exports, and sema code that needs the rabitq ones
18
- // imports them from the subpath directly. Re-export the public vector-DB
19
- // surface under the sema root, omitting the three colliding names.
20
- export { Heap, HnswIndex, Prng, RaBitQuantizer, VectorDatabase, } from "./rabitq-hnsw/src/index.js";
15
+ // rabitq-ivf: the partitioned (IVF) vector index over 1-bit RaBitQ codes.
16
+ export { IvfIndex, Prng, RaBitQuantizer, VectorDatabase, } from "./rabitq-ivf/src/index.js";
21
17
  export * from "./derive/src/index.js";
@@ -0,0 +1,113 @@
1
+ /** External ids are integers only. */
2
+ export type ExternalId = number;
3
+ export interface DatabaseOptions {
4
+ /** Path to the SQLite database file (use ":memory:" for a transient store). */
5
+ dbPath: string;
6
+ /** Vector dimensionality. Required for a new database; ignored when reopening. */
7
+ dim?: number;
8
+ /** Query breadth: clusters probed per query = ceil(efSearch / 4). Default
9
+ * 64 (16 probes). Tunable at runtime and at reopen. */
10
+ efSearch?: number;
11
+ /** Bits used to scalar-quantise the query for the RaBitQ estimator.
12
+ * Query-side only (stored codes are independent of it), so it is tunable
13
+ * at reopen, like efSearch. Default 8. */
14
+ queryBits?: number;
15
+ /** Number of sign-flip + Hadamard rounds in the random rotation. Default 3. */
16
+ rotationRounds?: number;
17
+ /** Seed for the rotation. Default fixed. */
18
+ seed?: number;
19
+ /** Optional centroid the vectors are centered by before quantisation. */
20
+ centroid?: ArrayLike<number>;
21
+ /** SQLite page-cache budget in MiB. Purely a latency knob: correctness and
22
+ * the per-operation storage-read count are identical with it at 0. */
23
+ cacheSizeMb?: number;
24
+ }
25
+ export interface QueryResult {
26
+ id: ExternalId;
27
+ /** Estimated cosine distance (1 - cosine). */
28
+ distance: number;
29
+ }
30
+ export interface StorageStats {
31
+ float32BytesPerVector: number;
32
+ codeBytesPerVector: number;
33
+ bytesPerVector: number;
34
+ compressionRatio: number;
35
+ }
36
+ /**
37
+ * A persistent vector database: an adaptive PARTITIONED (IVF) index over
38
+ * 1-bit RaBitQ codes (cosine), stored entirely in SQLite at `dbPath`. See
39
+ * ivf.ts for the index design. The original float vectors are never
40
+ * retained — only the sign codes — so a 1024-d vector costs 128 bytes.
41
+ *
42
+ * Inserting is route-and-append: cost is essentially FLAT in collection size
43
+ * (one RAM scan of the pivot table + one chunk append — no per-insert graph
44
+ * walk). Query cost is bounded by nprobe × cluster size — constant in N
45
+ * once the collection is past its first splits.
46
+ */
47
+ export declare class VectorDatabase {
48
+ private readonly index;
49
+ constructor(options: DatabaseOptions);
50
+ get dim(): number;
51
+ /** Number of live (non-deleted) vectors. */
52
+ get size(): number;
53
+ /** Physical slot count including tombstones from deletes/updates. */
54
+ get physicalSize(): number;
55
+ /** Number of clusters (partitions) currently in the index. */
56
+ get clusterCount(): number;
57
+ get efSearch(): number;
58
+ set efSearch(value: number);
59
+ /** Distance computations performed during the most recent query. */
60
+ get lastQueryDistanceComputations(): number;
61
+ /** Storage row reads issued by the most recent query — the honest,
62
+ * cache-independent scalability metric. */
63
+ get lastQueryStorageReads(): number;
64
+ get storage(): StorageStats;
65
+ private checkId;
66
+ /** Convert a value to code bytes, selecting by length: `codeWords`
67
+ * elements → an existing 1-bit code; otherwise a raw `dim`-vector. */
68
+ private toCodeBytes;
69
+ has(id: ExternalId): boolean;
70
+ /** Stream every live external id (bounded memory). */
71
+ keys(): IterableIterator<ExternalId>;
72
+ /** Stream live entries whose INTERNAL id is > `after` — a durable
73
+ * incremental watermark (internal ids are monotone at insert and preserved
74
+ * by update and compact). */
75
+ keysSince(after: number): IterableIterator<{
76
+ ext: ExternalId;
77
+ internal: number;
78
+ }>;
79
+ /** Read the stored 1-bit code for an id (a copy as a Uint32Array), or null. */
80
+ get(id: ExternalId): Uint32Array | null;
81
+ insert(id: ExternalId, value: ArrayLike<number>): void;
82
+ update(id: ExternalId, value: ArrayLike<number>): void;
83
+ upsert(id: ExternalId, value: ArrayLike<number>): void;
84
+ /**
85
+ * Upsert many vectors under ONE transaction — one WAL commit for the whole
86
+ * batch instead of one per vector, with chunk appends to the same cluster
87
+ * coalesced in the index's write-back buffer.
88
+ */
89
+ upsertMany(entries: Array<{
90
+ id: ExternalId;
91
+ vector: ArrayLike<number>;
92
+ }>): void;
93
+ /** Delete many ids under ONE transaction. Absent ids are skipped.
94
+ * Returns the number of vectors actually removed. */
95
+ deleteMany(ids: ExternalId[]): number;
96
+ /** Delete the vector bound to an id. Returns false if absent. */
97
+ delete(id: ExternalId): boolean;
98
+ /** Heat the SQLite page cache with sequential scans (latency only). */
99
+ warmCache(): number;
100
+ /** Reclaim tombstoned slots by rewriting the clusters that carry any, then
101
+ * VACUUM. Internal ids and cluster assignment are preserved. */
102
+ compact(): void;
103
+ /**
104
+ * k-NN search. The argument's length selects the mode:
105
+ * - `dim` elements → a raw vector (accurate estimator)
106
+ * - `codeWords` elements → a 1-bit code, by sign-bit Hamming
107
+ */
108
+ query(query: ArrayLike<number>, k?: number, opts?: {
109
+ ef?: number;
110
+ }): QueryResult[];
111
+ /** Close the underlying database. The instance must not be used afterwards. */
112
+ close(): void;
113
+ }
@@ -0,0 +1,201 @@
1
+ import { IvfIndex } from "./ivf.js";
2
+ /**
3
+ * A persistent vector database: an adaptive PARTITIONED (IVF) index over
4
+ * 1-bit RaBitQ codes (cosine), stored entirely in SQLite at `dbPath`. See
5
+ * ivf.ts for the index design. The original float vectors are never
6
+ * retained — only the sign codes — so a 1024-d vector costs 128 bytes.
7
+ *
8
+ * Inserting is route-and-append: cost is essentially FLAT in collection size
9
+ * (one RAM scan of the pivot table + one chunk append — no per-insert graph
10
+ * walk). Query cost is bounded by nprobe × cluster size — constant in N
11
+ * once the collection is past its first splits.
12
+ */
13
+ export class VectorDatabase {
14
+ index;
15
+ constructor(options) {
16
+ if (!options || typeof options.dbPath !== "string" ||
17
+ options.dbPath.length === 0) {
18
+ throw new Error("DatabaseOptions.dbPath (string) is required");
19
+ }
20
+ this.index = new IvfIndex(options.dbPath, options);
21
+ }
22
+ get dim() {
23
+ return this.index.dim;
24
+ }
25
+ /** Number of live (non-deleted) vectors. */
26
+ get size() {
27
+ return this.index.size;
28
+ }
29
+ /** Physical slot count including tombstones from deletes/updates. */
30
+ get physicalSize() {
31
+ return this.index.physicalSize;
32
+ }
33
+ /** Number of clusters (partitions) currently in the index. */
34
+ get clusterCount() {
35
+ return this.index.clusterCount;
36
+ }
37
+ get efSearch() {
38
+ return this.index.efSearch;
39
+ }
40
+ set efSearch(value) {
41
+ this.index.efSearch = value;
42
+ }
43
+ /** Distance computations performed during the most recent query. */
44
+ get lastQueryDistanceComputations() {
45
+ return this.index.lastQueryDistComps;
46
+ }
47
+ /** Storage row reads issued by the most recent query — the honest,
48
+ * cache-independent scalability metric. */
49
+ get lastQueryStorageReads() {
50
+ return this.index.lastQueryStorageReads;
51
+ }
52
+ get storage() {
53
+ const f32 = this.dim * 4;
54
+ const bpv = this.index.bytesPerVector;
55
+ return {
56
+ float32BytesPerVector: f32,
57
+ codeBytesPerVector: bpv,
58
+ bytesPerVector: bpv,
59
+ compressionRatio: f32 / bpv,
60
+ };
61
+ }
62
+ checkId(id) {
63
+ // 32-bit signed range: chunk blobs store exts as int32 — a wider id
64
+ // would silently truncate, so it is rejected at the door instead.
65
+ if (!Number.isInteger(id) || id > 0x7fffffff || id < -0x80000000) {
66
+ throw new Error(`External id must be a 32-bit integer, got ${id}`);
67
+ }
68
+ return id;
69
+ }
70
+ /** Convert a value to code bytes, selecting by length: `codeWords`
71
+ * elements → an existing 1-bit code; otherwise a raw `dim`-vector. */
72
+ toCodeBytes(value) {
73
+ if (value.length === this.index.codeWords) {
74
+ return this.index.codeToBytes(value);
75
+ }
76
+ if (value.length !== this.dim) {
77
+ throw new Error(`Vector dimension mismatch: expected ${this.dim}, got ${value.length}`);
78
+ }
79
+ return this.index.encodeToBytes(value);
80
+ }
81
+ has(id) {
82
+ return this.index.has(this.checkId(id));
83
+ }
84
+ /** Stream every live external id (bounded memory). */
85
+ *keys() {
86
+ yield* this.index.keys();
87
+ }
88
+ /** Stream live entries whose INTERNAL id is > `after` — a durable
89
+ * incremental watermark (internal ids are monotone at insert and preserved
90
+ * by update and compact). */
91
+ *keysSince(after) {
92
+ yield* this.index.keysSince(after);
93
+ }
94
+ /** Read the stored 1-bit code for an id (a copy as a Uint32Array), or null. */
95
+ get(id) {
96
+ const bytes = this.index.codeOf(this.checkId(id));
97
+ return bytes ? this.index.bytesToCode(bytes) : null;
98
+ }
99
+ insert(id, value) {
100
+ this.index.begin();
101
+ try {
102
+ this.index.insert(this.checkId(id), this.toCodeBytes(value));
103
+ this.index.commit();
104
+ }
105
+ catch (e) {
106
+ this.index.rollback();
107
+ throw e;
108
+ }
109
+ }
110
+ update(id, value) {
111
+ this.index.begin();
112
+ try {
113
+ this.index.update(this.checkId(id), this.toCodeBytes(value));
114
+ this.index.commit();
115
+ }
116
+ catch (e) {
117
+ this.index.rollback();
118
+ throw e;
119
+ }
120
+ }
121
+ upsert(id, value) {
122
+ this.upsertMany([{ id, vector: value }]);
123
+ }
124
+ /**
125
+ * Upsert many vectors under ONE transaction — one WAL commit for the whole
126
+ * batch instead of one per vector, with chunk appends to the same cluster
127
+ * coalesced in the index's write-back buffer.
128
+ */
129
+ upsertMany(entries) {
130
+ if (entries.length === 0)
131
+ return;
132
+ this.index.begin();
133
+ try {
134
+ for (const e of entries) {
135
+ // One shared vmap probe decides insert vs update — the per-row point
136
+ // query is the dominant bulk-load cost, so it is never duplicated.
137
+ this.index.upsert(this.checkId(e.id), this.toCodeBytes(e.vector));
138
+ }
139
+ this.index.commit();
140
+ }
141
+ catch (e) {
142
+ this.index.rollback();
143
+ throw e;
144
+ }
145
+ }
146
+ /** Delete many ids under ONE transaction. Absent ids are skipped.
147
+ * Returns the number of vectors actually removed. */
148
+ deleteMany(ids) {
149
+ if (ids.length === 0)
150
+ return 0;
151
+ let removed = 0;
152
+ this.index.begin();
153
+ try {
154
+ for (const id of ids) {
155
+ if (this.index.remove(this.checkId(id)))
156
+ removed++;
157
+ }
158
+ this.index.commit();
159
+ }
160
+ catch (e) {
161
+ this.index.rollback();
162
+ throw e;
163
+ }
164
+ return removed;
165
+ }
166
+ /** Delete the vector bound to an id. Returns false if absent. */
167
+ delete(id) {
168
+ return this.deleteMany([id]) === 1;
169
+ }
170
+ /** Heat the SQLite page cache with sequential scans (latency only). */
171
+ warmCache() {
172
+ return this.index.warmCache();
173
+ }
174
+ /** Reclaim tombstoned slots by rewriting the clusters that carry any, then
175
+ * VACUUM. Internal ids and cluster assignment are preserved. */
176
+ compact() {
177
+ this.index.compact();
178
+ }
179
+ /**
180
+ * k-NN search. The argument's length selects the mode:
181
+ * - `dim` elements → a raw vector (accurate estimator)
182
+ * - `codeWords` elements → a 1-bit code, by sign-bit Hamming
183
+ */
184
+ query(query, k = 10, opts) {
185
+ let hits;
186
+ if (query.length === this.index.codeWords) {
187
+ hits = this.index.searchKnnByCode(this.index.codeToBytes(query), k, opts?.ef);
188
+ }
189
+ else {
190
+ if (query.length !== this.dim) {
191
+ throw new Error(`Vector dimension mismatch: expected ${this.dim}, got ${query.length}`);
192
+ }
193
+ hits = this.index.searchKnn(query, k, opts?.ef);
194
+ }
195
+ return hits;
196
+ }
197
+ /** Close the underlying database. The instance must not be used afterwards. */
198
+ close() {
199
+ this.index.close();
200
+ }
201
+ }