@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Small deterministic pseudo-random number generator (mulberry32).
|
|
3
3
|
*
|
|
4
4
|
* Pure ECMAScript. It is used so that the random orthogonal rotation of the
|
|
5
|
-
* RaBitQ quantizer
|
|
5
|
+
* RaBitQ quantizer rotation is reproducible and can be
|
|
6
6
|
* regenerated after (de)serialization without storing large matrices.
|
|
7
7
|
*/
|
|
8
8
|
export declare class Prng {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Small deterministic pseudo-random number generator (mulberry32).
|
|
3
3
|
*
|
|
4
4
|
* Pure ECMAScript. It is used so that the random orthogonal rotation of the
|
|
5
|
-
* RaBitQ quantizer
|
|
5
|
+
* RaBitQ quantizer rotation is reproducible and can be
|
|
6
6
|
* regenerated after (de)serialization without storing large matrices.
|
|
7
7
|
*/
|
|
8
8
|
export class Prng {
|
|
@@ -13,11 +13,21 @@ export interface SQliteStoreOptions extends Partial<StoreConfig> {
|
|
|
13
13
|
export declare class SQliteStore extends AbstractStore implements Store {
|
|
14
14
|
private readonly opts;
|
|
15
15
|
private readonly vectorCacheMb;
|
|
16
|
+
private readonly sqliteCacheMb;
|
|
16
17
|
private readonly vectorSeed;
|
|
17
18
|
private content;
|
|
18
19
|
private halos;
|
|
19
20
|
private sqlite;
|
|
20
21
|
private _inTx;
|
|
22
|
+
/** Bloom filter over every node row's content hash `h`. The dedup probes
|
|
23
|
+
* consult it first: "definitely absent" answers the training hot path's
|
|
24
|
+
* dominant miss case in RAM (profiled: the SQLite probes were 38.7% of
|
|
25
|
+
* ingest wall at 2.9M nodes). INVARIANT: every inserted h is added
|
|
26
|
+
* before the probe that could see it — a false negative would mint a
|
|
27
|
+
* duplicate node and silently break hash-consing. */
|
|
28
|
+
private _bloom;
|
|
29
|
+
/** Dedup probes answered by the filter alone this session (observability). */
|
|
30
|
+
bloomSkips: number;
|
|
21
31
|
private _insertNode;
|
|
22
32
|
private _insertKid;
|
|
23
33
|
private _selContain;
|
|
@@ -40,6 +50,22 @@ export declare class SQliteStore extends AbstractStore implements Store {
|
|
|
40
50
|
private vectorDbPath;
|
|
41
51
|
private openVectorDB;
|
|
42
52
|
protected _dbOpen(): Promise<void>;
|
|
53
|
+
/** Restore the persisted filter and top up the rows a crash left uncovered
|
|
54
|
+
* (id ≥ stored `upto`); a missing/undersized blob forces a full rebuild.
|
|
55
|
+
* Clean-close sessions restart with a 0-row top-up. */
|
|
56
|
+
private _bloomLoad;
|
|
57
|
+
/** Re-add the hashes of rows with id ≥ `from` (the tail a crash cut off).
|
|
58
|
+
* Re-adding a hash twice only re-sets its bits — never a correctness
|
|
59
|
+
* issue — so overlap with the persisted coverage is harmless. */
|
|
60
|
+
private _bloomTopUp;
|
|
61
|
+
/** Build a fresh filter sized for the current node count from one
|
|
62
|
+
* sequential scan of the h column. Called at open when no usable blob is
|
|
63
|
+
* stored, and again whenever growth saturates the bits-per-key budget. */
|
|
64
|
+
private _bloomRebuild;
|
|
65
|
+
/** Persist the filter with its coverage watermark. Runs inside the
|
|
66
|
+
* caller's transaction when one is open (checkpoint/close both flush
|
|
67
|
+
* first). */
|
|
68
|
+
private _bloomPersist;
|
|
43
69
|
protected _dbClose(): void;
|
|
44
70
|
protected _dbBeginTx(): void;
|
|
45
71
|
protected _dbCommitTx(): void;
|
|
@@ -109,7 +135,6 @@ export declare class SQliteStore extends AbstractStore implements Store {
|
|
|
109
135
|
protected _vecContentUpsert(entries: Array<{
|
|
110
136
|
id: NodeId;
|
|
111
137
|
vector: Float32Array;
|
|
112
|
-
ef?: number;
|
|
113
138
|
}>): void;
|
|
114
139
|
protected _vecContentQuery(v: Float32Array, k: number, ef: number): Array<{
|
|
115
140
|
id: number;
|
package/dist/src/store-sqlite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// store-sqlite.ts — SQLite + rabitq-
|
|
1
|
+
// store-sqlite.ts — SQLite + rabitq-ivf persistence adapter.
|
|
2
2
|
//
|
|
3
3
|
// SQliteStore extends AbstractStore and implements only the bare essentials
|
|
4
4
|
// required for database communication: SQL schema, prepared statements, and
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { DatabaseSync } from "node:sqlite";
|
|
30
30
|
import { AbstractStore, flatBytesKids, packKids, unpackKids, } from "./store.js";
|
|
31
31
|
import { DEFAULT_CONFIG } from "./config.js";
|
|
32
|
-
import { VectorDatabase } from "./rabitq-
|
|
32
|
+
import { VectorDatabase } from "./rabitq-ivf/src/index.js";
|
|
33
33
|
const SCHEMA = `
|
|
34
34
|
-- Content-addressed lookup goes through ONE small integer index: h is the
|
|
35
35
|
-- FNV-1a hash of the row's content key (leaf bytes, or the packed kid ids of a
|
|
@@ -44,6 +44,21 @@ CREATE TABLE IF NOT EXISTS node (
|
|
|
44
44
|
h INTEGER NOT NULL
|
|
45
45
|
);
|
|
46
46
|
CREATE INDEX IF NOT EXISTS idx_node_h ON node(h);
|
|
47
|
+
-- Negative-lookup BLOOM FILTER over node.h — the RAM structure that lets the
|
|
48
|
+
-- content-addressed dedup probes (findLeaf/findBranch, the training hot path)
|
|
49
|
+
-- answer "definitely new content" without an idx_node_h descent. Persisted
|
|
50
|
+
-- here at checkpoint/close with upto = the next node id NOT covered; on
|
|
51
|
+
-- open, rows with id >= upto (a crash's tail) are re-added by an incremental
|
|
52
|
+
-- rowid-range scan, so the filter can NEVER be missing an inserted hash — a
|
|
53
|
+
-- false negative would silently break hash-consing (duplicate mints), while
|
|
54
|
+
-- a false positive only costs the SQLite probe that was paid on every call
|
|
55
|
+
-- before the filter existed.
|
|
56
|
+
CREATE TABLE IF NOT EXISTS bloom (
|
|
57
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
58
|
+
bits BLOB NOT NULL,
|
|
59
|
+
n INTEGER NOT NULL,
|
|
60
|
+
upto INTEGER NOT NULL
|
|
61
|
+
);
|
|
47
62
|
-- The reverse structural edge child→parent. A WITHOUT ROWID table clustered on
|
|
48
63
|
-- (child, parent) IS the lookup index: parents(child) is one B-tree descent to a
|
|
49
64
|
-- contiguous run, with no separate rowid heap and no secondary index to maintain
|
|
@@ -106,9 +121,65 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
106
121
|
val TEXT NOT NULL
|
|
107
122
|
);
|
|
108
123
|
`;
|
|
124
|
+
/** Blocked Bloom filter over 32-bit content hashes. k=4 bit positions per
|
|
125
|
+
* key via double hashing; at the maintained ≥12 bits/key the false-positive
|
|
126
|
+
* rate stays under ~1%, so ≥99% of miss-probes skip SQLite entirely.
|
|
127
|
+
* No deletions (nodes are never deleted), so growth is a rebuild. */
|
|
128
|
+
class NodeBloom {
|
|
129
|
+
bits;
|
|
130
|
+
mask;
|
|
131
|
+
n = 0;
|
|
132
|
+
constructor(log2bits) {
|
|
133
|
+
this.bits = new Uint8Array(1 << (log2bits - 3));
|
|
134
|
+
this.mask = (1 << log2bits) - 1 >>> 0;
|
|
135
|
+
}
|
|
136
|
+
add(h) {
|
|
137
|
+
const h2 = (Math.imul(h, 0x9e3779b1) | 1) >>> 0;
|
|
138
|
+
let x = h >>> 0;
|
|
139
|
+
const bits = this.bits;
|
|
140
|
+
const mask = this.mask;
|
|
141
|
+
for (let i = 0; i < 4; i++) {
|
|
142
|
+
const b = x & mask;
|
|
143
|
+
bits[b >> 3] |= 1 << (b & 7);
|
|
144
|
+
x = (x + h2) >>> 0;
|
|
145
|
+
}
|
|
146
|
+
this.n++;
|
|
147
|
+
}
|
|
148
|
+
mightContain(h) {
|
|
149
|
+
const h2 = (Math.imul(h, 0x9e3779b1) | 1) >>> 0;
|
|
150
|
+
let x = h >>> 0;
|
|
151
|
+
const bits = this.bits;
|
|
152
|
+
const mask = this.mask;
|
|
153
|
+
for (let i = 0; i < 4; i++) {
|
|
154
|
+
const b = x & mask;
|
|
155
|
+
if ((bits[b >> 3] & (1 << (b & 7))) === 0)
|
|
156
|
+
return false;
|
|
157
|
+
x = (x + h2) >>> 0;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
/** Whether the filter is due for a bigger rebuild (< 12 bits/key). */
|
|
162
|
+
get saturated() {
|
|
163
|
+
return this.n * 12 > this.bits.length * 8;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** log2 of the bloom bit count sized for `n` keys at 16 bits/key, clamped to
|
|
167
|
+
* [17, 30] — 16 KiB floor, 128 MiB ceiling (past ~90M nodes the FPR is
|
|
168
|
+
* allowed to drift up rather than RAM). */
|
|
169
|
+
function bloomLog2For(n) {
|
|
170
|
+
let log2 = 17;
|
|
171
|
+
while (log2 < 30 && (1 << log2) < n * 16)
|
|
172
|
+
log2++;
|
|
173
|
+
return log2;
|
|
174
|
+
}
|
|
175
|
+
/** Below this node count the filter is NOT persisted: a rebuild scan at open
|
|
176
|
+
* is instant at this size, and the blob would dominate a small store's
|
|
177
|
+
* on-disk footprint. */
|
|
178
|
+
const BLOOM_PERSIST_MIN_NODES = 65_536;
|
|
109
179
|
export class SQliteStore extends AbstractStore {
|
|
110
180
|
opts;
|
|
111
181
|
vectorCacheMb;
|
|
182
|
+
sqliteCacheMb;
|
|
112
183
|
vectorSeed;
|
|
113
184
|
// ── the two SEPARATE vector indices ───────────────────────────────────
|
|
114
185
|
content = null; // STORAGE: node gists
|
|
@@ -116,6 +187,16 @@ export class SQliteStore extends AbstractStore {
|
|
|
116
187
|
sqlite = null;
|
|
117
188
|
// Deferred write transaction guard.
|
|
118
189
|
_inTx = false;
|
|
190
|
+
// ── negative dedup filter ──────────────────────────────────────────────
|
|
191
|
+
/** Bloom filter over every node row's content hash `h`. The dedup probes
|
|
192
|
+
* consult it first: "definitely absent" answers the training hot path's
|
|
193
|
+
* dominant miss case in RAM (profiled: the SQLite probes were 38.7% of
|
|
194
|
+
* ingest wall at 2.9M nodes). INVARIANT: every inserted h is added
|
|
195
|
+
* before the probe that could see it — a false negative would mint a
|
|
196
|
+
* duplicate node and silently break hash-consing. */
|
|
197
|
+
_bloom = null;
|
|
198
|
+
/** Dedup probes answered by the filter alone this session (observability). */
|
|
199
|
+
bloomSkips = 0;
|
|
119
200
|
_insertNode = null;
|
|
120
201
|
_insertKid = null;
|
|
121
202
|
_selContain = null;
|
|
@@ -139,10 +220,6 @@ export class SQliteStore extends AbstractStore {
|
|
|
139
220
|
// Resolve config from opts, falling back to defaults.
|
|
140
221
|
const config = {
|
|
141
222
|
minHaloMass: opts.minHaloMass ?? d.minHaloMass,
|
|
142
|
-
m: opts.m ?? d.m,
|
|
143
|
-
efConstruction: opts.efConstruction ?? d.efConstruction,
|
|
144
|
-
efConstructionInterior: opts.efConstructionInterior ??
|
|
145
|
-
d.efConstructionInterior,
|
|
146
223
|
efSearch: opts.efSearch ?? d.efSearch,
|
|
147
224
|
compactEveryNWrites: opts.compactEveryNWrites ?? d.compactEveryNWrites,
|
|
148
225
|
overfetch: opts.overfetch ?? d.overfetch,
|
|
@@ -154,6 +231,7 @@ export class SQliteStore extends AbstractStore {
|
|
|
154
231
|
pendingGistBytes: opts.pendingGistBytes ?? d.pendingGistBytes,
|
|
155
232
|
haloCacheBytes: opts.haloCacheBytes ?? d.haloCacheBytes,
|
|
156
233
|
vectorCacheMb: opts.vectorCacheMb ?? d.vectorCacheMb,
|
|
234
|
+
sqliteCacheMb: opts.sqliteCacheMb ?? d.sqliteCacheMb,
|
|
157
235
|
coveredIdsMax: opts.coveredIdsMax ?? d.coveredIdsMax,
|
|
158
236
|
chainCacheBytes: opts.chainCacheBytes ?? d.chainCacheBytes,
|
|
159
237
|
};
|
|
@@ -162,6 +240,7 @@ export class SQliteStore extends AbstractStore {
|
|
|
162
240
|
super(config, D, maxGroup);
|
|
163
241
|
this.opts = opts;
|
|
164
242
|
this.vectorCacheMb = opts.vectorCacheMb ?? d.vectorCacheMb;
|
|
243
|
+
this.sqliteCacheMb = opts.sqliteCacheMb ?? d.sqliteCacheMb;
|
|
165
244
|
this.vectorSeed = (0x51f15e ^ 0x9e3779b9) >>> 0;
|
|
166
245
|
this._ready = this._dbOpen();
|
|
167
246
|
}
|
|
@@ -176,8 +255,6 @@ export class SQliteStore extends AbstractStore {
|
|
|
176
255
|
return new VectorDatabase({
|
|
177
256
|
dbPath: this.vectorDbPath(name),
|
|
178
257
|
dim: this.D,
|
|
179
|
-
M: this.m,
|
|
180
|
-
efConstruction: this.efConstruction,
|
|
181
258
|
efSearch: this.efSearch,
|
|
182
259
|
// Query-side estimator precision. 8 bits: 4 bits measurably misranks
|
|
183
260
|
// tight gist clusters (mixture recall@10 37.5% vs 39.0%, self-recall
|
|
@@ -207,6 +284,26 @@ export class SQliteStore extends AbstractStore {
|
|
|
207
284
|
// WAL + NORMAL sync: crash-safe (WAL commits are atomic) without a full
|
|
208
285
|
// fsync per commit — the same discipline the vector databases use.
|
|
209
286
|
this.sqlite.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;");
|
|
287
|
+
// SQLite's autocheckpoint default is 1000 PAGES — 1 MiB at this store's
|
|
288
|
+
// 1 KiB page size — so at training write rates nearly every batch commit
|
|
289
|
+
// crossed the threshold and paid a synchronous checkpoint: random writes
|
|
290
|
+
// into a GB-scale main file, with the hottest B-tree pages copied out on
|
|
291
|
+
// every commit instead of being coalesced across commits in the WAL.
|
|
292
|
+
// Profiled on a 7.5M-node store: ~114 ms PER COMMIT, ~19% of ingest
|
|
293
|
+
// wall-clock. 16 MiB of WAL between checkpoints coalesces hot-page
|
|
294
|
+
// rewrites and amortizes the checkpoint's fixed cost; crash-safety is
|
|
295
|
+
// unchanged (WAL recovery replays it), the only trade is a bounded,
|
|
296
|
+
// fixed-size WAL file.
|
|
297
|
+
this.sqlite.exec("PRAGMA wal_autocheckpoint = 65536;");
|
|
298
|
+
// Page cache for the DAG tables. Content-addressed dedup (findLeaf /
|
|
299
|
+
// findBranch), parent probes and contain appends are millions of point
|
|
300
|
+
// queries per session against a file that outgrows SQLite's ~2 MiB
|
|
301
|
+
// default cache almost immediately; every miss is a file read on the
|
|
302
|
+
// ingest hot path. Negative = KiB budget (size-based, page-size
|
|
303
|
+
// independent). Latency only — results identical at any value.
|
|
304
|
+
const cacheKb = Math.max(0, Math.floor(this.sqliteCacheMb * 1024));
|
|
305
|
+
if (cacheKb > 0)
|
|
306
|
+
this.sqlite.exec(`PRAGMA cache_size = -${cacheKb};`);
|
|
210
307
|
this.sqlite.exec(SCHEMA);
|
|
211
308
|
// Recover D from a previous run so loadFromStore() can bootstrap with any
|
|
212
309
|
// default — the real value is read from meta before vector indices load.
|
|
@@ -244,8 +341,71 @@ export class SQliteStore extends AbstractStore {
|
|
|
244
341
|
// the same value but scans every leaf page, so open time grew linearly
|
|
245
342
|
// with the store; a per-row scan into a Set was worse still.)
|
|
246
343
|
this._nextId = (this.sqlite.prepare("SELECT MAX(id) AS m FROM node").get().m ?? -1) + 1;
|
|
344
|
+
this._bloomLoad();
|
|
345
|
+
}
|
|
346
|
+
// ── negative dedup filter: load / rebuild / persist ────────────────────
|
|
347
|
+
/** Restore the persisted filter and top up the rows a crash left uncovered
|
|
348
|
+
* (id ≥ stored `upto`); a missing/undersized blob forces a full rebuild.
|
|
349
|
+
* Clean-close sessions restart with a 0-row top-up. */
|
|
350
|
+
_bloomLoad() {
|
|
351
|
+
const row = this.sqlite.prepare("SELECT bits, n, upto FROM bloom WHERE id = 1").get();
|
|
352
|
+
const wantLog2 = bloomLog2For(this._nextId);
|
|
353
|
+
if (row && row.bits.length * 8 >= (1 << wantLog2)) {
|
|
354
|
+
const log2 = 31 - Math.clz32(row.bits.length * 8);
|
|
355
|
+
const b = new NodeBloom(log2);
|
|
356
|
+
b.bits.set(row.bits);
|
|
357
|
+
b.n = row.n;
|
|
358
|
+
this._bloom = b;
|
|
359
|
+
this._bloomTopUp(row.upto);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
this._bloomRebuild();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** Re-add the hashes of rows with id ≥ `from` (the tail a crash cut off).
|
|
366
|
+
* Re-adding a hash twice only re-sets its bits — never a correctness
|
|
367
|
+
* issue — so overlap with the persisted coverage is harmless. */
|
|
368
|
+
_bloomTopUp(from) {
|
|
369
|
+
if (from >= this._nextId)
|
|
370
|
+
return;
|
|
371
|
+
const b = this._bloom;
|
|
372
|
+
const scan = this.sqlite.prepare("SELECT h FROM node WHERE id >= ?");
|
|
373
|
+
scan.setReturnArrays(true);
|
|
374
|
+
for (const r of scan.iterate(from)) {
|
|
375
|
+
b.add(r[0]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/** Build a fresh filter sized for the current node count from one
|
|
379
|
+
* sequential scan of the h column. Called at open when no usable blob is
|
|
380
|
+
* stored, and again whenever growth saturates the bits-per-key budget. */
|
|
381
|
+
_bloomRebuild() {
|
|
382
|
+
const b = new NodeBloom(bloomLog2For(this._nextId));
|
|
383
|
+
b.n = 0;
|
|
384
|
+
if (this._nextId > 0) {
|
|
385
|
+
const scan = this.sqlite.prepare("SELECT h FROM node");
|
|
386
|
+
scan.setReturnArrays(true);
|
|
387
|
+
for (const r of scan.iterate()) {
|
|
388
|
+
// add() counts n itself.
|
|
389
|
+
b.add(r[0]);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
this._bloom = b;
|
|
393
|
+
}
|
|
394
|
+
/** Persist the filter with its coverage watermark. Runs inside the
|
|
395
|
+
* caller's transaction when one is open (checkpoint/close both flush
|
|
396
|
+
* first). */
|
|
397
|
+
_bloomPersist() {
|
|
398
|
+
if (!this._bloom || !this.sqlite)
|
|
399
|
+
return;
|
|
400
|
+
if (this._nextId < BLOOM_PERSIST_MIN_NODES)
|
|
401
|
+
return;
|
|
402
|
+
this.sqlite.prepare("INSERT INTO bloom (id, bits, n, upto) VALUES (1, ?, ?, ?) " +
|
|
403
|
+
"ON CONFLICT(id) DO UPDATE SET bits = excluded.bits, " +
|
|
404
|
+
"n = excluded.n, upto = excluded.upto").run(this._bloom.bits, this._bloom.n, this._nextId);
|
|
247
405
|
}
|
|
248
406
|
_dbClose() {
|
|
407
|
+
if (this.sqlite)
|
|
408
|
+
this._bloomPersist();
|
|
249
409
|
if (this.content) {
|
|
250
410
|
this.content.close();
|
|
251
411
|
this.content = null;
|
|
@@ -278,6 +438,12 @@ export class SQliteStore extends AbstractStore {
|
|
|
278
438
|
this._insertNode = this.sqlite.prepare("INSERT INTO node (id, leaf, kids, h) VALUES (?, ?, ?, ?)");
|
|
279
439
|
}
|
|
280
440
|
this._insertNode.run(id, leaf, kids, h);
|
|
441
|
+
// The filter must see every inserted hash BEFORE any probe could — the
|
|
442
|
+
// add sits in the same synchronous call as the row insert.
|
|
443
|
+
const b = this._bloom;
|
|
444
|
+
b.add(h);
|
|
445
|
+
if (b.saturated)
|
|
446
|
+
this._bloomRebuild();
|
|
281
447
|
}
|
|
282
448
|
_dbGetNode(id) {
|
|
283
449
|
if (!this._selNode) {
|
|
@@ -298,6 +464,10 @@ export class SQliteStore extends AbstractStore {
|
|
|
298
464
|
};
|
|
299
465
|
}
|
|
300
466
|
_dbFindLeaf(h, bytes) {
|
|
467
|
+
if (this._bloom && !this._bloom.mightContain(h)) {
|
|
468
|
+
this.bloomSkips++;
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
301
471
|
if (!this._selLeaf) {
|
|
302
472
|
this._selLeaf = this.sqlite.prepare("SELECT id FROM node WHERE h = ? AND leaf = ? AND kids IS NULL LIMIT 1");
|
|
303
473
|
}
|
|
@@ -305,6 +475,10 @@ export class SQliteStore extends AbstractStore {
|
|
|
305
475
|
return row ? row.id : null;
|
|
306
476
|
}
|
|
307
477
|
_dbFindBranchByLeaf(h, bytes) {
|
|
478
|
+
if (this._bloom && !this._bloom.mightContain(h)) {
|
|
479
|
+
this.bloomSkips++;
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
308
482
|
if (!this._selFlat) {
|
|
309
483
|
this._selFlat = this.sqlite.prepare("SELECT id FROM node WHERE h = ? AND leaf = ? AND kids IS NOT NULL LIMIT 1");
|
|
310
484
|
}
|
|
@@ -312,6 +486,10 @@ export class SQliteStore extends AbstractStore {
|
|
|
312
486
|
return row ? row.id : null;
|
|
313
487
|
}
|
|
314
488
|
_dbFindBranchByKids(h, packed) {
|
|
489
|
+
if (this._bloom && !this._bloom.mightContain(h)) {
|
|
490
|
+
this.bloomSkips++;
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
315
493
|
if (!this._selKids) {
|
|
316
494
|
this._selKids = this.sqlite.prepare("SELECT id FROM node WHERE h = ? AND kids = ? LIMIT 1");
|
|
317
495
|
}
|
|
@@ -626,6 +804,10 @@ export class SQliteStore extends AbstractStore {
|
|
|
626
804
|
"ON CONFLICT(id) DO UPDATE SET data = excluded.data");
|
|
627
805
|
}
|
|
628
806
|
this._insSnapshot.run(bytes);
|
|
807
|
+
// Checkpoint cadence: persisting the dedup filter here keeps the crash
|
|
808
|
+
// top-up (the id ≥ upto rescan at next open) bounded by one checkpoint
|
|
809
|
+
// interval instead of the whole session.
|
|
810
|
+
this._bloomPersist();
|
|
629
811
|
}
|
|
630
812
|
_dbLoadSnapshot() {
|
|
631
813
|
if (!this._selSnapshot) {
|
package/dist/src/store.d.ts
CHANGED
|
@@ -324,7 +324,6 @@ export declare abstract class AbstractStore implements Store {
|
|
|
324
324
|
protected abstract _vecContentUpsert(entries: Array<{
|
|
325
325
|
id: NodeId;
|
|
326
326
|
vector: Float32Array;
|
|
327
|
-
ef?: number;
|
|
328
327
|
}>): void;
|
|
329
328
|
protected abstract _vecContentQuery(v: Float32Array, k: number, ef: number): Array<{
|
|
330
329
|
id: number;
|
|
@@ -376,9 +375,6 @@ export declare abstract class AbstractStore implements Store {
|
|
|
376
375
|
protected _maxGroup: number;
|
|
377
376
|
protected readonly minHaloMass: number;
|
|
378
377
|
protected readonly efSearch: number;
|
|
379
|
-
protected readonly m: number;
|
|
380
|
-
protected readonly efConstruction: number;
|
|
381
|
-
protected readonly efConstructionInterior: number;
|
|
382
378
|
protected readonly overfetch: number;
|
|
383
379
|
protected readonly batchSize: number;
|
|
384
380
|
protected readonly compactEveryNWrites: number;
|
|
@@ -429,20 +425,17 @@ export declare abstract class AbstractStore implements Store {
|
|
|
429
425
|
protected _coveredIds: BoundedMap<NodeId, true>;
|
|
430
426
|
/** Live content-index id set, LRU-bounded so a massive ingest never leaks
|
|
431
427
|
* memory; an evicted entry is still indexed (the row is durable), so the
|
|
432
|
-
* only cost of an eviction is a duplicate
|
|
428
|
+
* only cost of an eviction is a duplicate index probe on next visit. */
|
|
433
429
|
protected _indexedIds: BoundedMap<NodeId, true>;
|
|
434
430
|
/** ANN read cache for {@link resonate} — keyed by vecKey(v) + ":" + k;
|
|
435
431
|
* lazily initialised, dropped on any index mutation. */
|
|
436
432
|
protected _resonateCache: Map<string, Hit[]> | null;
|
|
437
433
|
/** ANN read cache for {@link resonateHalo} — same scheme. */
|
|
438
434
|
protected _resonateHaloCache: Map<string, Hit[]> | null;
|
|
439
|
-
/** Content (gist) index write buffer.
|
|
440
|
-
* construction budget: reach-only interiors carry the reduced
|
|
441
|
-
* `efConstructionInterior`; dedup targets omit it (full budget). */
|
|
435
|
+
/** Content (gist) index write buffer. */
|
|
442
436
|
protected _contentBuffer: Array<{
|
|
443
437
|
id: NodeId;
|
|
444
438
|
vector: Float32Array;
|
|
445
|
-
ef?: number;
|
|
446
439
|
}>;
|
|
447
440
|
/** Halo index write buffer — keyed by id so repeats within a batch coalesce. */
|
|
448
441
|
protected _haloBuffer: Map<number, Float32Array<ArrayBufferLike>>;
|
package/dist/src/store.js
CHANGED
|
@@ -364,9 +364,6 @@ export class AbstractStore {
|
|
|
364
364
|
_maxGroup;
|
|
365
365
|
minHaloMass;
|
|
366
366
|
efSearch;
|
|
367
|
-
m;
|
|
368
|
-
efConstruction;
|
|
369
|
-
efConstructionInterior;
|
|
370
367
|
overfetch;
|
|
371
368
|
batchSize;
|
|
372
369
|
compactEveryNWrites;
|
|
@@ -419,7 +416,7 @@ export class AbstractStore {
|
|
|
419
416
|
_coveredIds;
|
|
420
417
|
/** Live content-index id set, LRU-bounded so a massive ingest never leaks
|
|
421
418
|
* memory; an evicted entry is still indexed (the row is durable), so the
|
|
422
|
-
* only cost of an eviction is a duplicate
|
|
419
|
+
* only cost of an eviction is a duplicate index probe on next visit. */
|
|
423
420
|
_indexedIds;
|
|
424
421
|
// ── ANN read cache ─────────────────────────────────────────────────────
|
|
425
422
|
// The index is read-only between writes, so the same (v,k) always returns
|
|
@@ -432,9 +429,7 @@ export class AbstractStore {
|
|
|
432
429
|
/** ANN read cache for {@link resonateHalo} — same scheme. */
|
|
433
430
|
_resonateHaloCache = null;
|
|
434
431
|
// ── Write buffers ──────────────────────────────────────────────────────
|
|
435
|
-
/** Content (gist) index write buffer.
|
|
436
|
-
* construction budget: reach-only interiors carry the reduced
|
|
437
|
-
* `efConstructionInterior`; dedup targets omit it (full budget). */
|
|
432
|
+
/** Content (gist) index write buffer. */
|
|
438
433
|
_contentBuffer = [];
|
|
439
434
|
/** Halo index write buffer — keyed by id so repeats within a batch coalesce. */
|
|
440
435
|
_haloBuffer = new Map();
|
|
@@ -466,9 +461,6 @@ export class AbstractStore {
|
|
|
466
461
|
this._maxGroup = maxGroup;
|
|
467
462
|
this.minHaloMass = config.minHaloMass;
|
|
468
463
|
this.efSearch = config.efSearch;
|
|
469
|
-
this.m = config.m;
|
|
470
|
-
this.efConstruction = config.efConstruction;
|
|
471
|
-
this.efConstructionInterior = Math.max(1, Math.min(config.efConstructionInterior, config.efConstruction));
|
|
472
464
|
this.overfetch = config.overfetch;
|
|
473
465
|
this.batchSize = config.batchSize;
|
|
474
466
|
this.compactEveryNWrites = config.compactEveryNWrites;
|
|
@@ -875,7 +867,7 @@ export class AbstractStore {
|
|
|
875
867
|
// leaf, and near-merging distinct leaves only corrupts bytes for no real
|
|
876
868
|
// saving. Real near-dedup compression lives in subtree (branch) fusion.
|
|
877
869
|
//
|
|
878
|
-
// There is deliberately NO
|
|
870
|
+
// There is deliberately NO ANN probe of the FLUSHED index here. It used
|
|
879
871
|
// to fire for EVERY new branch that the buffer scan didn't settle — i.e.
|
|
880
872
|
// ~every interior branch, since interiors are never dedup targets —
|
|
881
873
|
// making one ANN query per branch the dominant training cost (it dwarfed
|
|
@@ -981,7 +973,7 @@ export class AbstractStore {
|
|
|
981
973
|
// Capture the gist; it is pushed into the content index lazily, the first
|
|
982
974
|
// time this node becomes a resonance target (link / pourHalo). A node that
|
|
983
975
|
// never does (a pure intermediate DAG node — ~99.5% of them) is never
|
|
984
|
-
// indexed: it costs one persistence row, no
|
|
976
|
+
// indexed: it costs one persistence row, no vector-index slot and no merge probe.
|
|
985
977
|
this._pendingGist.set(id, normalize(copy(gist)));
|
|
986
978
|
await this.maybeFlush();
|
|
987
979
|
return id;
|
|
@@ -1056,12 +1048,7 @@ export class AbstractStore {
|
|
|
1056
1048
|
return;
|
|
1057
1049
|
}
|
|
1058
1050
|
this._indexedIds.set(id, true);
|
|
1059
|
-
|
|
1060
|
-
// one deliberate speed-for-quality trade of ingestion (see
|
|
1061
|
-
// {@link StoreConfig.efConstructionInterior}); targets keep the full one.
|
|
1062
|
-
this._contentBuffer.push(dedupTarget
|
|
1063
|
-
? { id, vector: v }
|
|
1064
|
-
: { id, vector: v, ef: this.efConstructionInterior });
|
|
1051
|
+
this._contentBuffer.push({ id, vector: v });
|
|
1065
1052
|
this._bufferedIds.add(id);
|
|
1066
1053
|
// A node indexed AS a dedup target enters the candidate set immediately.
|
|
1067
1054
|
if (dedupTarget)
|
|
@@ -1344,11 +1331,7 @@ export class AbstractStore {
|
|
|
1344
1331
|
// Index it — same code path as indexGist, but the vector is injected
|
|
1345
1332
|
// directly rather than read from the (empty) pending-gist cache.
|
|
1346
1333
|
this._indexedIds.set(id, true);
|
|
1347
|
-
this._contentBuffer.push({
|
|
1348
|
-
id,
|
|
1349
|
-
vector: gist,
|
|
1350
|
-
ef: this.efConstructionInterior,
|
|
1351
|
-
});
|
|
1334
|
+
this._contentBuffer.push({ id, vector: gist });
|
|
1352
1335
|
this._bufferedIds.add(id);
|
|
1353
1336
|
// Repaired nodes are reach-indexed, never dedup targets: their gist is
|
|
1354
1337
|
// regenerated (may differ numerically from the original) and they are
|
package/example/train_base.ts
CHANGED
|
@@ -1610,20 +1610,23 @@ async function saveProgress(store: Store, p: SavedProgress): Promise<void> {
|
|
|
1610
1610
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1611
1611
|
|
|
1612
1612
|
async function main(): Promise<void> {
|
|
1613
|
-
// The vector indices'
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1617
|
-
//
|
|
1618
|
-
|
|
1619
|
-
//
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1613
|
+
// The vector indices' memory knob (MiB) — each index's SQLite page cache.
|
|
1614
|
+
// The IVF index routes inserts through a RAM-resident pivot table and
|
|
1615
|
+
// appends to chunk blobs, so this cache mostly serves query-time cluster
|
|
1616
|
+
// scans; 256 MiB comfortably covers the probed working set of a trained
|
|
1617
|
+
// store. Override with VECTOR_CACHE_MB (64 is the library default).
|
|
1618
|
+
const VECTOR_CACHE_MB = Math.max(0, Number(env("VECTOR_CACHE_MB", "256")));
|
|
1619
|
+
// Page cache for the MAIN DAG database (node/kid/edge/contain tables).
|
|
1620
|
+
// Training issues millions of content-addressed point probes per session
|
|
1621
|
+
// against a GB-scale file; the library default (64 MiB) is sized for a
|
|
1622
|
+
// small machine — a training box affords more. Override with
|
|
1623
|
+
// SQLITE_CACHE_MB.
|
|
1624
|
+
const SQLITE_CACHE_MB = Math.max(0, Number(env("SQLITE_CACHE_MB", "256")));
|
|
1623
1625
|
const store = new SQliteStore({
|
|
1624
1626
|
path: DB_PATH,
|
|
1625
1627
|
D,
|
|
1626
1628
|
vectorCacheMb: VECTOR_CACHE_MB,
|
|
1629
|
+
sqliteCacheMb: SQLITE_CACHE_MB,
|
|
1627
1630
|
});
|
|
1628
1631
|
|
|
1629
1632
|
// The store IS the model: memories, progress, and metadata all persist in
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hviana/sema",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Sema: a non-parametric, instance-based reasoning system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
7
7
|
"types": "dist/src/index.d.ts",
|
package/src/alu/README.md
CHANGED
|
@@ -15,7 +15,7 @@ search as authoritative axioms (at `STEP` cost, like a learned edge).
|
|
|
15
15
|
|
|
16
16
|
It has no dependency on the rest of the codebase except the pure byte helpers in
|
|
17
17
|
`../bytes.ts`, and is intended to be reused as a self-contained sublibrary in
|
|
18
|
-
the spirit of `derive/` and `rabitq-
|
|
18
|
+
the spirit of `derive/` and `rabitq-ivf/`.
|
|
19
19
|
|
|
20
20
|
## The thesis: one tiny kernel, everything else is a rewrite
|
|
21
21
|
|
package/src/alu/src/index.ts
CHANGED
|
@@ -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.
|