@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.
- package/AGENTS.md +6 -5
- package/CITATION.cff +49 -0
- package/HOW_IT_WORKS.md +11 -12
- package/README.md +7 -5
- package/dist/example/train_base.js +13 -10
- package/dist/src/alu/src/index.js +1 -1
- package/dist/src/config.d.ts +16 -19
- package/dist/src/config.js +3 -8
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -6
- package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
- package/dist/src/rabitq-ivf/src/database.js +201 -0
- package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.d.ts +2 -5
- package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
- package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
- package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
- package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/prng.d.ts +1 -1
- package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/prng.js +1 -1
- package/dist/src/store-sqlite.d.ts +26 -1
- package/dist/src/store-sqlite.js +190 -8
- package/dist/src/store.d.ts +2 -9
- package/dist/src/store.js +6 -23
- package/example/train_base.ts +13 -10
- package/package.json +2 -2
- package/src/alu/README.md +1 -1
- package/src/alu/src/index.ts +1 -1
- package/src/config.ts +19 -27
- package/src/index.ts +6 -11
- package/src/rabitq-ivf/README.md +56 -0
- package/src/rabitq-ivf/src/database.ts +276 -0
- package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
- package/src/rabitq-ivf/src/ivf.ts +1330 -0
- package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
- package/src/store-sqlite.ts +196 -9
- package/src/store.ts +8 -32
- package/test/08-storage.test.mjs +3 -3
- package/test/14-scaling.test.mjs +2 -2
- package/test/35-ivf.test.mjs +263 -0
- package/test/36-bloom.test.mjs +123 -0
- package/dist/src/rabitq-hnsw/src/database.d.ts +0 -200
- package/dist/src/rabitq-hnsw/src/database.js +0 -388
- package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
- package/dist/src/rabitq-hnsw/src/heap.js +0 -89
- package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
- package/dist/src/rabitq-hnsw/src/hnsw.js +0 -474
- package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
- package/dist/src/rabitq-hnsw/src/store.js +0 -825
- package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
- package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -948
- package/src/rabitq-hnsw/README.md +0 -303
- package/src/rabitq-hnsw/src/database.ts +0 -492
- package/src/rabitq-hnsw/src/heap.ts +0 -90
- package/src/rabitq-hnsw/src/hnsw.ts +0 -514
- package/src/rabitq-hnsw/src/store.ts +0 -994
- package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
- /package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.d.ts +0 -0
- /package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.js +0 -0
- /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-
|
|
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,
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
- **
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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) │ (
|
|
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
|
|
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
|
-
- **
|
|
4128
|
-
|
|
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
|
+
[](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
|
|
227
|
-
|
|
|
228
|
-
| **`rabitq-
|
|
229
|
-
| **`derive`**
|
|
230
|
-
| **`alu`**
|
|
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'
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
|
|
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-
|
|
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.
|
package/dist/src/config.d.ts
CHANGED
|
@@ -26,18 +26,9 @@ export interface AlphabetConfig {
|
|
|
26
26
|
}
|
|
27
27
|
export interface StoreConfig {
|
|
28
28
|
minHaloMass: number;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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-
|
|
82
|
-
* (forwarded as its `cacheSizeMb`
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
package/dist/src/config.js
CHANGED
|
@@ -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 ??
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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 {
|
|
14
|
-
export type { DatabaseOptions, ExternalId,
|
|
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-
|
|
16
|
-
|
|
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
|
+
}
|