@hviana/sema 0.1.8 → 0.1.9
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/package.json +1 -1
- package/src/geometry.ts +14 -30
- package/src/mind/mechanisms/cast.ts +12 -7
- package/dist/example/demo.d.ts +0 -1
- package/dist/example/demo.js +0 -39
- package/dist/example/train_base.d.ts +0 -87
- package/dist/example/train_base.js +0 -2252
- package/dist/src/alphabet.d.ts +0 -7
- package/dist/src/alphabet.js +0 -33
- package/dist/src/alu/src/alu.d.ts +0 -185
- package/dist/src/alu/src/alu.js +0 -440
- package/dist/src/alu/src/expr.d.ts +0 -61
- package/dist/src/alu/src/expr.js +0 -318
- package/dist/src/alu/src/index.d.ts +0 -11
- package/dist/src/alu/src/index.js +0 -19
- package/dist/src/alu/src/kernel-arith.d.ts +0 -16
- package/dist/src/alu/src/kernel-arith.js +0 -264
- package/dist/src/alu/src/kernel-bits.d.ts +0 -19
- package/dist/src/alu/src/kernel-bits.js +0 -152
- package/dist/src/alu/src/kernel-logic.d.ts +0 -4
- package/dist/src/alu/src/kernel-logic.js +0 -60
- package/dist/src/alu/src/kernel-nd.d.ts +0 -3
- package/dist/src/alu/src/kernel-nd.js +0 -208
- package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
- package/dist/src/alu/src/kernel-numeric.js +0 -366
- package/dist/src/alu/src/operation.d.ts +0 -168
- package/dist/src/alu/src/operation.js +0 -189
- package/dist/src/alu/src/parser.d.ts +0 -221
- package/dist/src/alu/src/parser.js +0 -577
- package/dist/src/alu/src/resonance.d.ts +0 -55
- package/dist/src/alu/src/resonance.js +0 -126
- package/dist/src/alu/src/text.d.ts +0 -31
- package/dist/src/alu/src/text.js +0 -73
- package/dist/src/alu/src/value.d.ts +0 -109
- package/dist/src/alu/src/value.js +0 -300
- package/dist/src/alu/test/alu.test.d.ts +0 -1
- package/dist/src/alu/test/alu.test.js +0 -764
- package/dist/src/bytes.d.ts +0 -14
- package/dist/src/bytes.js +0 -59
- package/dist/src/canon.d.ts +0 -26
- package/dist/src/canon.js +0 -57
- package/dist/src/config.d.ts +0 -111
- package/dist/src/config.js +0 -91
- package/dist/src/derive/src/deduction.d.ts +0 -125
- package/dist/src/derive/src/deduction.js +0 -155
- package/dist/src/derive/src/index.d.ts +0 -7
- package/dist/src/derive/src/index.js +0 -11
- package/dist/src/derive/src/priority-queue.d.ts +0 -20
- package/dist/src/derive/src/priority-queue.js +0 -73
- package/dist/src/derive/src/rewrite.d.ts +0 -56
- package/dist/src/derive/src/rewrite.js +0 -100
- package/dist/src/derive/src/trie.d.ts +0 -90
- package/dist/src/derive/src/trie.js +0 -217
- package/dist/src/derive/test/derive.test.d.ts +0 -1
- package/dist/src/derive/test/derive.test.js +0 -122
- package/dist/src/extension.d.ts +0 -37
- package/dist/src/extension.js +0 -7
- package/dist/src/geometry.d.ts +0 -148
- package/dist/src/geometry.js +0 -529
- package/dist/src/index.d.ts +0 -16
- package/dist/src/index.js +0 -18
- package/dist/src/ingest-cache.d.ts +0 -41
- package/dist/src/ingest-cache.js +0 -161
- package/dist/src/mind/articulation.d.ts +0 -6
- package/dist/src/mind/articulation.js +0 -99
- package/dist/src/mind/attention.d.ts +0 -72
- package/dist/src/mind/attention.js +0 -898
- package/dist/src/mind/canonical.d.ts +0 -29
- package/dist/src/mind/canonical.js +0 -88
- package/dist/src/mind/graph-search.d.ts +0 -270
- package/dist/src/mind/graph-search.js +0 -847
- package/dist/src/mind/index.d.ts +0 -5
- package/dist/src/mind/index.js +0 -5
- package/dist/src/mind/junction.d.ts +0 -95
- package/dist/src/mind/junction.js +0 -262
- package/dist/src/mind/learning.d.ts +0 -47
- package/dist/src/mind/learning.js +0 -233
- package/dist/src/mind/match.d.ts +0 -113
- package/dist/src/mind/match.js +0 -446
- package/dist/src/mind/mechanisms/alu.d.ts +0 -4
- package/dist/src/mind/mechanisms/alu.js +0 -29
- package/dist/src/mind/mechanisms/cast.d.ts +0 -35
- package/dist/src/mind/mechanisms/cast.js +0 -461
- package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
- package/dist/src/mind/mechanisms/confluence.js +0 -225
- package/dist/src/mind/mechanisms/cover.d.ts +0 -6
- package/dist/src/mind/mechanisms/cover.js +0 -179
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
- package/dist/src/mind/mechanisms/extraction.js +0 -342
- package/dist/src/mind/mechanisms/recall.d.ts +0 -13
- package/dist/src/mind/mechanisms/recall.js +0 -235
- package/dist/src/mind/mind.d.ts +0 -273
- package/dist/src/mind/mind.js +0 -562
- package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
- package/dist/src/mind/pipeline-mechanism.js +0 -213
- package/dist/src/mind/pipeline.d.ts +0 -20
- package/dist/src/mind/pipeline.js +0 -185
- package/dist/src/mind/primitives.d.ts +0 -63
- package/dist/src/mind/primitives.js +0 -251
- package/dist/src/mind/rationale.d.ts +0 -134
- package/dist/src/mind/rationale.js +0 -162
- package/dist/src/mind/reasoning.d.ts +0 -15
- package/dist/src/mind/reasoning.js +0 -162
- package/dist/src/mind/recognition.d.ts +0 -20
- package/dist/src/mind/recognition.js +0 -246
- package/dist/src/mind/resonance.d.ts +0 -23
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +0 -15
- package/dist/src/mind/trace.js +0 -73
- package/dist/src/mind/traverse.d.ts +0 -113
- package/dist/src/mind/traverse.js +0 -488
- package/dist/src/mind/types.d.ts +0 -186
- package/dist/src/mind/types.js +0 -84
- package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
- package/dist/src/rabitq-ivf/src/database.js +0 -201
- package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
- package/dist/src/rabitq-ivf/src/index.js +0 -4
- package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
- package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
- package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
- package/dist/src/rabitq-ivf/src/prng.js +0 -36
- package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
- package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
- package/dist/src/sema.d.ts +0 -31
- package/dist/src/sema.js +0 -63
- package/dist/src/store-sqlite.d.ts +0 -184
- package/dist/src/store-sqlite.js +0 -942
- package/dist/src/store.d.ts +0 -654
- package/dist/src/store.js +0 -1612
- package/dist/src/vec.d.ts +0 -31
- package/dist/src/vec.js +0 -109
package/dist/src/store-sqlite.js
DELETED
|
@@ -1,942 +0,0 @@
|
|
|
1
|
-
// store-sqlite.ts — SQLite + rabitq-ivf persistence adapter.
|
|
2
|
-
//
|
|
3
|
-
// SQliteStore extends AbstractStore and implements only the bare essentials
|
|
4
|
-
// required for database communication: SQL schema, prepared statements, and
|
|
5
|
-
// VectorDatabase management. All domain logic — caching, dedup/merge
|
|
6
|
-
// decisions, interior-node indexing, geometric halo scheduling, buffer
|
|
7
|
-
// management — lives in the AbstractStore base class (store.ts).
|
|
8
|
-
//
|
|
9
|
-
// Two clearly separated concerns, each with its OWN vector index:
|
|
10
|
-
//
|
|
11
|
-
// • STORAGE — the content-addressed DAG (nodes, edges, metadata) in SQLite,
|
|
12
|
-
// plus a VectorDatabase for node gists (resonant lookup), in its OWN
|
|
13
|
-
// SQLite file (`<path>.content.vec`).
|
|
14
|
-
// • CACHE — the distributional "company" of each node (its halo), a
|
|
15
|
-
// derived/regenerable concept layer, in a SEPARATE VectorDatabase,
|
|
16
|
-
// in its own SQLite file (`<path>.halo.vec`).
|
|
17
|
-
//
|
|
18
|
-
// Write discipline:
|
|
19
|
-
//
|
|
20
|
-
// • Integer node ids: a dense 0,1,2,… counter. SQLite stores them inline in
|
|
21
|
-
// the rowid B-tree (no secondary index), child lists pack to 4 bytes each,
|
|
22
|
-
// and the vector index keys on them as a 4-byte int32.
|
|
23
|
-
// • One deferred WRITE TRANSACTION: node rows, edges and halo updates are
|
|
24
|
-
// committed in batches on the flush cadence (and on close/snapshot) rather
|
|
25
|
-
// than one implicit transaction per node.
|
|
26
|
-
// • Each of the three SQLite databases (main DAG, content vectors, halo
|
|
27
|
-
// vectors) is an independent file. The vector databases persist to their
|
|
28
|
-
// own files on every write — no separate serialisation step is needed.
|
|
29
|
-
import { DatabaseSync } from "node:sqlite";
|
|
30
|
-
import { AbstractStore, flatBytesKids, packKids, unpackKids, } from "./store.js";
|
|
31
|
-
import { DEFAULT_CONFIG } from "./config.js";
|
|
32
|
-
import { VectorDatabase } from "./rabitq-ivf/src/index.js";
|
|
33
|
-
const SCHEMA = `
|
|
34
|
-
-- Content-addressed lookup goes through ONE small integer index: h is the
|
|
35
|
-
-- FNV-1a hash of the row's content key (leaf bytes, or the packed kid ids of a
|
|
36
|
-
-- mixed branch). Indexing the hash instead of the blob itself means the lookup
|
|
37
|
-
-- index stores ~9 bytes per node rather than a full copy of every leaf and kid
|
|
38
|
-
-- blob (which doubled the table); the point query fetches the h-candidates and
|
|
39
|
-
-- verifies the actual blob, so collisions cost a fetch, never a wrong id.
|
|
40
|
-
CREATE TABLE IF NOT EXISTS node (
|
|
41
|
-
id INTEGER PRIMARY KEY,
|
|
42
|
-
leaf BLOB,
|
|
43
|
-
kids BLOB,
|
|
44
|
-
h INTEGER NOT NULL
|
|
45
|
-
);
|
|
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
|
-
);
|
|
62
|
-
-- The reverse structural edge child→parent. A WITHOUT ROWID table clustered on
|
|
63
|
-
-- (child, parent) IS the lookup index: parents(child) is one B-tree descent to a
|
|
64
|
-
-- contiguous run, with no separate rowid heap and no secondary index to maintain
|
|
65
|
-
-- (~2x smaller than a heap table + idx_kid_child). The composite primary key also
|
|
66
|
-
-- gives free (child,parent) dedup, so a child repeated within one branch's kids
|
|
67
|
-
-- never writes a duplicate row.
|
|
68
|
-
CREATE TABLE IF NOT EXISTS kid (
|
|
69
|
-
child INTEGER NOT NULL,
|
|
70
|
-
parent INTEGER NOT NULL,
|
|
71
|
-
PRIMARY KEY (child, parent)
|
|
72
|
-
) WITHOUT ROWID;
|
|
73
|
-
-- seq is a plain INTEGER PRIMARY KEY: rowids are assigned max+1 and edges are
|
|
74
|
-
-- never deleted, so insertion order is preserved without AUTOINCREMENT's
|
|
75
|
-
-- sqlite_sequence bookkeeping table.
|
|
76
|
-
CREATE TABLE IF NOT EXISTS edge (
|
|
77
|
-
src INTEGER NOT NULL,
|
|
78
|
-
dst INTEGER NOT NULL,
|
|
79
|
-
seq INTEGER PRIMARY KEY
|
|
80
|
-
);
|
|
81
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_edge ON edge(src, dst);
|
|
82
|
-
CREATE INDEX IF NOT EXISTS idx_edge_dst ON edge(dst);
|
|
83
|
-
CREATE TABLE IF NOT EXISTS halo (
|
|
84
|
-
id INTEGER PRIMARY KEY,
|
|
85
|
-
vec BLOB NOT NULL,
|
|
86
|
-
mass REAL NOT NULL DEFAULT 0
|
|
87
|
-
);
|
|
88
|
-
-- CONTAINMENT: parent's byte content contains child's, for children that are
|
|
89
|
-
-- not among any parent's kids list (sub-span flat branches inside leaf-parent
|
|
90
|
-
-- chunks). Kept SEPARATE from kid so parents() — the structural climb — is
|
|
91
|
-
-- untouched; containers() serves the climbing surface of orphan flat branches
|
|
92
|
-
-- durably (a session-local map would be lost on restart or ingest-cache replay).
|
|
93
|
-
-- LOG-STRUCTURED PACKED PAGES: a child's parents are int32-packed blobs split
|
|
94
|
-
-- across (child, seq) rows. A flush APPENDS one page holding only the NEW
|
|
95
|
-
-- parents (never reading the list back), then geometrically merges adjacent
|
|
96
|
-
-- pages (byte concat; full dedup when a merge reaches the seq-0 base page), so
|
|
97
|
-
-- page count stays O(log fan-in) and total merge I/O is amortized linear.
|
|
98
|
-
-- This keeps the packed format's ~4 B/parent density (contain is ~11% of a
|
|
99
|
-
-- trained store; one B-tree cell per pair measured 2.3× larger) while fixing
|
|
100
|
-
-- the single-blob row's flaw: merging new parents into ONE row re-wrote the
|
|
101
|
-
-- child's WHOLE list per flush — quadratic in a hot window's corpus-sized
|
|
102
|
-
-- fan-in. Transient duplicates may exist across pages until a base merge;
|
|
103
|
-
-- readers dedup (containers()) or tolerate repeats by contract (slices).
|
|
104
|
-
-- The (child, seq) key is packed into the INTEGER PRIMARY KEY (rowid) as
|
|
105
|
-
-- child·2^8 + seq — exact in a JS double for every valid id, and a child's
|
|
106
|
-
-- pages are one contiguous rowid range. The geometric merge bounds live page
|
|
107
|
-
-- counts (and thus seq) to O(log fan-in) ≪ 2^8, and a narrow band keeps the
|
|
108
|
-
-- rowid varint within ~1 byte of the bare child id. A rowid table, NOT a WITHOUT ROWID one: index B-tree cells cap at
|
|
109
|
-
-- ~¼ page, so at 1 KiB pages any child beyond ~55 parents would spill each
|
|
110
|
-
-- page row onto a mostly-empty overflow page (measured +26% table size).
|
|
111
|
-
CREATE TABLE IF NOT EXISTS contain (
|
|
112
|
-
id INTEGER PRIMARY KEY,
|
|
113
|
-
parents BLOB NOT NULL
|
|
114
|
-
);
|
|
115
|
-
-- CANONICAL-FORM index (Store.canonAdd/canonFind): h is the 32-bit hash of a
|
|
116
|
-
-- node's CANONICAL content key (the modality's canonicalizer output — case-
|
|
117
|
-
-- folded, whitespace-collapsed text, etc.; the store never sees the key
|
|
118
|
-
-- itself, only its hash). Same hash-then-verify discipline as idx_node_h:
|
|
119
|
-
-- the caller re-canonicalizes each candidate's bytes before trusting it, so
|
|
120
|
-
-- a collision costs a read, never a wrong id. WITHOUT ROWID on (h, id):
|
|
121
|
-
-- the composite key IS the lookup index and gives free dedup.
|
|
122
|
-
CREATE TABLE IF NOT EXISTS canon (
|
|
123
|
-
h INTEGER NOT NULL,
|
|
124
|
-
id INTEGER NOT NULL,
|
|
125
|
-
PRIMARY KEY (h, id)
|
|
126
|
-
) WITHOUT ROWID;
|
|
127
|
-
CREATE TABLE IF NOT EXISTS snapshot (
|
|
128
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
129
|
-
data BLOB NOT NULL
|
|
130
|
-
);
|
|
131
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
132
|
-
key TEXT PRIMARY KEY,
|
|
133
|
-
val TEXT NOT NULL
|
|
134
|
-
);
|
|
135
|
-
`;
|
|
136
|
-
/** Blocked Bloom filter over 32-bit content hashes. k=4 bit positions per
|
|
137
|
-
* key via double hashing; at the maintained ≥12 bits/key the false-positive
|
|
138
|
-
* rate stays under ~1%, so ≥99% of miss-probes skip SQLite entirely.
|
|
139
|
-
* No deletions (nodes are never deleted), so growth is a rebuild. */
|
|
140
|
-
class NodeBloom {
|
|
141
|
-
bits;
|
|
142
|
-
mask;
|
|
143
|
-
n = 0;
|
|
144
|
-
constructor(log2bits) {
|
|
145
|
-
this.bits = new Uint8Array(1 << (log2bits - 3));
|
|
146
|
-
this.mask = (1 << log2bits) - 1 >>> 0;
|
|
147
|
-
}
|
|
148
|
-
add(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
|
-
bits[b >> 3] |= 1 << (b & 7);
|
|
156
|
-
x = (x + h2) >>> 0;
|
|
157
|
-
}
|
|
158
|
-
this.n++;
|
|
159
|
-
}
|
|
160
|
-
mightContain(h) {
|
|
161
|
-
const h2 = (Math.imul(h, 0x9e3779b1) | 1) >>> 0;
|
|
162
|
-
let x = h >>> 0;
|
|
163
|
-
const bits = this.bits;
|
|
164
|
-
const mask = this.mask;
|
|
165
|
-
for (let i = 0; i < 4; i++) {
|
|
166
|
-
const b = x & mask;
|
|
167
|
-
if ((bits[b >> 3] & (1 << (b & 7))) === 0)
|
|
168
|
-
return false;
|
|
169
|
-
x = (x + h2) >>> 0;
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
/** Whether the filter is due for a bigger rebuild (< 12 bits/key). */
|
|
174
|
-
get saturated() {
|
|
175
|
-
return this.n * 12 > this.bits.length * 8;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/** log2 of the bloom bit count sized for `n` keys at 16 bits/key, clamped to
|
|
179
|
-
* [17, 30] — 16 KiB floor, 128 MiB ceiling (past ~90M nodes the FPR is
|
|
180
|
-
* allowed to drift up rather than RAM). */
|
|
181
|
-
function bloomLog2For(n) {
|
|
182
|
-
let log2 = 17;
|
|
183
|
-
while (log2 < 30 && (1 << log2) < n * 16)
|
|
184
|
-
log2++;
|
|
185
|
-
return log2;
|
|
186
|
-
}
|
|
187
|
-
/** Below this node count the filter is NOT persisted: a rebuild scan at open
|
|
188
|
-
* is instant at this size, and the blob would dominate a small store's
|
|
189
|
-
* on-disk footprint. */
|
|
190
|
-
const BLOOM_PERSIST_MIN_NODES = 65_536;
|
|
191
|
-
export class SQliteStore extends AbstractStore {
|
|
192
|
-
opts;
|
|
193
|
-
vectorCacheMb;
|
|
194
|
-
sqliteCacheMb;
|
|
195
|
-
vectorSeed;
|
|
196
|
-
// ── the two SEPARATE vector indices ───────────────────────────────────
|
|
197
|
-
content = null; // STORAGE: node gists
|
|
198
|
-
halos = null; // CACHE: halo gists
|
|
199
|
-
sqlite = null;
|
|
200
|
-
// Deferred write transaction guard.
|
|
201
|
-
_inTx = false;
|
|
202
|
-
// ── negative dedup filter ──────────────────────────────────────────────
|
|
203
|
-
/** Bloom filter over every node row's content hash `h`. The dedup probes
|
|
204
|
-
* consult it first: "definitely absent" answers the training hot path's
|
|
205
|
-
* dominant miss case in RAM (profiled: the SQLite probes were 38.7% of
|
|
206
|
-
* ingest wall at 2.9M nodes). INVARIANT: every inserted h is added
|
|
207
|
-
* before the probe that could see it — a false negative would mint a
|
|
208
|
-
* duplicate node and silently break hash-consing. */
|
|
209
|
-
_bloom = null;
|
|
210
|
-
/** Dedup probes answered by the filter alone this session (observability). */
|
|
211
|
-
bloomSkips = 0;
|
|
212
|
-
_insertNode = null;
|
|
213
|
-
_insertKid = null;
|
|
214
|
-
_selContain = null;
|
|
215
|
-
_selParents = null;
|
|
216
|
-
_selHalo = null;
|
|
217
|
-
_upHalo = null;
|
|
218
|
-
_selLeaf = null;
|
|
219
|
-
_selFlat = null;
|
|
220
|
-
_selKids = null;
|
|
221
|
-
_selNode = null;
|
|
222
|
-
_insertEdge = null;
|
|
223
|
-
_selNext = null;
|
|
224
|
-
_selPrev = null;
|
|
225
|
-
_setMeta = null;
|
|
226
|
-
_getMeta = null;
|
|
227
|
-
_insCanon = null;
|
|
228
|
-
_selCanon = null;
|
|
229
|
-
_cntCanon = null;
|
|
230
|
-
_selContentFrom = null;
|
|
231
|
-
_delMeta = null;
|
|
232
|
-
_insSnapshot = null;
|
|
233
|
-
_selSnapshot = null;
|
|
234
|
-
constructor(opts = {}) {
|
|
235
|
-
const d = DEFAULT_CONFIG.store;
|
|
236
|
-
// Resolve config from opts, falling back to defaults.
|
|
237
|
-
const config = {
|
|
238
|
-
minHaloMass: opts.minHaloMass ?? d.minHaloMass,
|
|
239
|
-
efSearch: opts.efSearch ?? d.efSearch,
|
|
240
|
-
compactEveryNWrites: opts.compactEveryNWrites ?? d.compactEveryNWrites,
|
|
241
|
-
overfetch: opts.overfetch ?? d.overfetch,
|
|
242
|
-
batchSize: opts.batchSize ?? d.batchSize,
|
|
243
|
-
dedupCacheMax: opts.dedupCacheMax ?? d.dedupCacheMax,
|
|
244
|
-
bytesCacheMax: opts.bytesCacheMax ?? d.bytesCacheMax,
|
|
245
|
-
recCacheBytes: opts.recCacheBytes ?? d.recCacheBytes,
|
|
246
|
-
ingestCacheBytes: opts.ingestCacheBytes ?? d.ingestCacheBytes,
|
|
247
|
-
pendingGistBytes: opts.pendingGistBytes ?? d.pendingGistBytes,
|
|
248
|
-
haloCacheBytes: opts.haloCacheBytes ?? d.haloCacheBytes,
|
|
249
|
-
vectorCacheMb: opts.vectorCacheMb ?? d.vectorCacheMb,
|
|
250
|
-
sqliteCacheMb: opts.sqliteCacheMb ?? d.sqliteCacheMb,
|
|
251
|
-
coveredIdsMax: opts.coveredIdsMax ?? d.coveredIdsMax,
|
|
252
|
-
chainCacheBytes: opts.chainCacheBytes ?? d.chainCacheBytes,
|
|
253
|
-
};
|
|
254
|
-
const D = opts.D ?? 1024;
|
|
255
|
-
const maxGroup = opts.maxGroup ?? DEFAULT_CONFIG.geometry.maxGroup;
|
|
256
|
-
super(config, D, maxGroup);
|
|
257
|
-
this.opts = opts;
|
|
258
|
-
this.vectorCacheMb = opts.vectorCacheMb ?? d.vectorCacheMb;
|
|
259
|
-
this.sqliteCacheMb = opts.sqliteCacheMb ?? d.sqliteCacheMb;
|
|
260
|
-
this.vectorSeed = (0x51f15e ^ 0x9e3779b9) >>> 0;
|
|
261
|
-
this._ready = this._dbOpen();
|
|
262
|
-
}
|
|
263
|
-
// ── Vector-DB paths ───────────────────────────────────────────────────
|
|
264
|
-
vectorDbPath(name) {
|
|
265
|
-
const stem = this.opts.path ?? ":memory:";
|
|
266
|
-
if (stem === ":memory:")
|
|
267
|
-
return ":memory:";
|
|
268
|
-
return `${stem}.${name}.vec`;
|
|
269
|
-
}
|
|
270
|
-
openVectorDB(name) {
|
|
271
|
-
return new VectorDatabase({
|
|
272
|
-
dbPath: this.vectorDbPath(name),
|
|
273
|
-
dim: this.D,
|
|
274
|
-
efSearch: this.efSearch,
|
|
275
|
-
// Query-side estimator precision. 8 bits: 4 bits measurably misranks
|
|
276
|
-
// tight gist clusters (mixture recall@10 37.5% vs 39.0%, self-recall
|
|
277
|
-
// 69% vs 76%; rabitq test 2a), and the codes on disk are independent of
|
|
278
|
-
// it, so existing stores adopt the sharper setting on reopen. The one
|
|
279
|
-
// decision the sharper ranking exposed — a saturated hub winning a
|
|
280
|
-
// sub-noise rank tie and silencing its region — is handled by the
|
|
281
|
-
// tie-band saturation fallback in attention.ts, which is derived from
|
|
282
|
-
// the estimator's own margin noise rather than tuned to a bit width.
|
|
283
|
-
queryBits: this.opts.queryBits ?? 8,
|
|
284
|
-
seed: this.vectorSeed,
|
|
285
|
-
cacheSizeMb: this.vectorCacheMb,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
// ── Abstract method implementations ───────────────────────────────────
|
|
289
|
-
// -- Lifecycle --
|
|
290
|
-
async _dbOpen() {
|
|
291
|
-
const stem = this.opts.path ?? ":memory:";
|
|
292
|
-
const sp = stem === ":memory:" ? ":memory:" : `${stem}.sqlite`;
|
|
293
|
-
this.sqlite = new DatabaseSync(sp);
|
|
294
|
-
// Page size matched to the store's row grain BEFORE any table exists (a
|
|
295
|
-
// no-op on a non-empty database). Every table here holds small rows —
|
|
296
|
-
// packed kid/contain pairs, byte-packed flat branches, quantized halos —
|
|
297
|
-
// so SQLite's default 4 KiB pages carry mostly slack; 1 KiB pages keep
|
|
298
|
-
// leaf-page fill high at a negligible extra tree depth.
|
|
299
|
-
this.sqlite.exec("PRAGMA page_size = 1024;");
|
|
300
|
-
// WAL + NORMAL sync: crash-safe (WAL commits are atomic) without a full
|
|
301
|
-
// fsync per commit — the same discipline the vector databases use.
|
|
302
|
-
this.sqlite.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;");
|
|
303
|
-
// SQLite's autocheckpoint default is 1000 PAGES — 1 MiB at this store's
|
|
304
|
-
// 1 KiB page size — so at training write rates nearly every batch commit
|
|
305
|
-
// crossed the threshold and paid a synchronous checkpoint: random writes
|
|
306
|
-
// into a GB-scale main file, with the hottest B-tree pages copied out on
|
|
307
|
-
// every commit instead of being coalesced across commits in the WAL.
|
|
308
|
-
// Profiled on a 7.5M-node store: ~114 ms PER COMMIT, ~19% of ingest
|
|
309
|
-
// wall-clock. 16 MiB of WAL between checkpoints coalesces hot-page
|
|
310
|
-
// rewrites and amortizes the checkpoint's fixed cost; crash-safety is
|
|
311
|
-
// unchanged (WAL recovery replays it), the only trade is a bounded,
|
|
312
|
-
// fixed-size WAL file.
|
|
313
|
-
this.sqlite.exec("PRAGMA wal_autocheckpoint = 65536;");
|
|
314
|
-
// Page cache for the DAG tables. Content-addressed dedup (findLeaf /
|
|
315
|
-
// findBranch), parent probes and contain appends are millions of point
|
|
316
|
-
// queries per session against a file that outgrows SQLite's ~2 MiB
|
|
317
|
-
// default cache almost immediately; every miss is a file read on the
|
|
318
|
-
// ingest hot path. Negative = KiB budget (size-based, page-size
|
|
319
|
-
// independent). Latency only — results identical at any value.
|
|
320
|
-
const cacheKb = Math.max(0, Math.floor(this.sqliteCacheMb * 1024));
|
|
321
|
-
if (cacheKb > 0)
|
|
322
|
-
this.sqlite.exec(`PRAGMA cache_size = -${cacheKb};`);
|
|
323
|
-
this.sqlite.exec(SCHEMA);
|
|
324
|
-
// Recover D from a previous run so loadFromStore() can bootstrap with any
|
|
325
|
-
// default — the real value is read from meta before vector indices load.
|
|
326
|
-
// The meta table always exists here (SCHEMA above creates it), so these
|
|
327
|
-
// reads run bare: a throw is a genuine storage fault, and swallowing it
|
|
328
|
-
// would let the store proceed with a default D/maxGroup that silently
|
|
329
|
-
// disagrees with what training persisted.
|
|
330
|
-
{
|
|
331
|
-
const row = this.sqlite.prepare("SELECT val FROM meta WHERE key = 'train.D'").get();
|
|
332
|
-
if (row) {
|
|
333
|
-
const d = Number(row.val);
|
|
334
|
-
if (Number.isInteger(d) && d > 0)
|
|
335
|
-
this._D = d;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Recover maxGroup from meta so indexSubtree uses the training-time value.
|
|
339
|
-
{
|
|
340
|
-
const row = this.sqlite.prepare("SELECT val FROM meta WHERE key = 'geometry.maxGroup'").get();
|
|
341
|
-
if (row) {
|
|
342
|
-
const g = Number(row.val);
|
|
343
|
-
if (Number.isInteger(g) && g > 0)
|
|
344
|
-
this._maxGroup = g;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// Persist maxGroup to meta when opening a FRESH store (no rows yet) so
|
|
348
|
-
// indexSubtree always sees the training-time value even when the store is
|
|
349
|
-
// accessed without a Mind / full snapshot.
|
|
350
|
-
if (this._nextId === 0) {
|
|
351
|
-
this.sqlite.prepare("INSERT OR IGNORE INTO meta (key, val) VALUES ('geometry.maxGroup', ?)").run(String(this._maxGroup));
|
|
352
|
-
}
|
|
353
|
-
this.content = this.openVectorDB("content");
|
|
354
|
-
this.halos = this.openVectorDB("halo");
|
|
355
|
-
// Ids are dense (0,1,2,…) and never deleted, so MAX(id)+1 IS the next id
|
|
356
|
-
// — one rightmost B-tree descent on the rowid, O(log n). (COUNT(*) gives
|
|
357
|
-
// the same value but scans every leaf page, so open time grew linearly
|
|
358
|
-
// with the store; a per-row scan into a Set was worse still.)
|
|
359
|
-
this._nextId = (this.sqlite.prepare("SELECT MAX(id) AS m FROM node").get().m ?? -1) + 1;
|
|
360
|
-
this._bloomLoad();
|
|
361
|
-
}
|
|
362
|
-
// ── negative dedup filter: load / rebuild / persist ────────────────────
|
|
363
|
-
/** Restore the persisted filter and top up the rows a crash left uncovered
|
|
364
|
-
* (id ≥ stored `upto`); a missing/undersized blob forces a full rebuild.
|
|
365
|
-
* Clean-close sessions restart with a 0-row top-up. */
|
|
366
|
-
_bloomLoad() {
|
|
367
|
-
const row = this.sqlite.prepare("SELECT bits, n, upto FROM bloom WHERE id = 1").get();
|
|
368
|
-
const wantLog2 = bloomLog2For(this._nextId);
|
|
369
|
-
if (row && row.bits.length * 8 >= (1 << wantLog2)) {
|
|
370
|
-
const log2 = 31 - Math.clz32(row.bits.length * 8);
|
|
371
|
-
const b = new NodeBloom(log2);
|
|
372
|
-
b.bits.set(row.bits);
|
|
373
|
-
b.n = row.n;
|
|
374
|
-
this._bloom = b;
|
|
375
|
-
this._bloomTopUp(row.upto);
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
this._bloomRebuild();
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
/** Re-add the hashes of rows with id ≥ `from` (the tail a crash cut off).
|
|
382
|
-
* Re-adding a hash twice only re-sets its bits — never a correctness
|
|
383
|
-
* issue — so overlap with the persisted coverage is harmless. */
|
|
384
|
-
_bloomTopUp(from) {
|
|
385
|
-
if (from >= this._nextId)
|
|
386
|
-
return;
|
|
387
|
-
const b = this._bloom;
|
|
388
|
-
const scan = this.sqlite.prepare("SELECT h FROM node WHERE id >= ?");
|
|
389
|
-
scan.setReturnArrays(true);
|
|
390
|
-
for (const r of scan.iterate(from)) {
|
|
391
|
-
b.add(r[0]);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/** Build a fresh filter sized for the current node count from one
|
|
395
|
-
* sequential scan of the h column. Called at open when no usable blob is
|
|
396
|
-
* stored, and again whenever growth saturates the bits-per-key budget. */
|
|
397
|
-
_bloomRebuild() {
|
|
398
|
-
const b = new NodeBloom(bloomLog2For(this._nextId));
|
|
399
|
-
b.n = 0;
|
|
400
|
-
if (this._nextId > 0) {
|
|
401
|
-
const scan = this.sqlite.prepare("SELECT h FROM node");
|
|
402
|
-
scan.setReturnArrays(true);
|
|
403
|
-
for (const r of scan.iterate()) {
|
|
404
|
-
// add() counts n itself.
|
|
405
|
-
b.add(r[0]);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
this._bloom = b;
|
|
409
|
-
}
|
|
410
|
-
/** Persist the filter with its coverage watermark. Runs inside the
|
|
411
|
-
* caller's transaction when one is open (checkpoint/close both flush
|
|
412
|
-
* first). */
|
|
413
|
-
_bloomPersist() {
|
|
414
|
-
if (!this._bloom || !this.sqlite)
|
|
415
|
-
return;
|
|
416
|
-
if (this._nextId < BLOOM_PERSIST_MIN_NODES)
|
|
417
|
-
return;
|
|
418
|
-
this.sqlite.prepare("INSERT INTO bloom (id, bits, n, upto) VALUES (1, ?, ?, ?) " +
|
|
419
|
-
"ON CONFLICT(id) DO UPDATE SET bits = excluded.bits, " +
|
|
420
|
-
"n = excluded.n, upto = excluded.upto").run(this._bloom.bits, this._bloom.n, this._nextId);
|
|
421
|
-
}
|
|
422
|
-
_dbClose() {
|
|
423
|
-
if (this.sqlite)
|
|
424
|
-
this._bloomPersist();
|
|
425
|
-
if (this.content) {
|
|
426
|
-
this.content.close();
|
|
427
|
-
this.content = null;
|
|
428
|
-
}
|
|
429
|
-
if (this.halos) {
|
|
430
|
-
this.halos.close();
|
|
431
|
-
this.halos = null;
|
|
432
|
-
}
|
|
433
|
-
if (this.sqlite) {
|
|
434
|
-
this.sqlite.close();
|
|
435
|
-
this.sqlite = null;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// -- Transaction --
|
|
439
|
-
_dbBeginTx() {
|
|
440
|
-
if (this._inTx || !this.sqlite)
|
|
441
|
-
return;
|
|
442
|
-
this.sqlite.exec("BEGIN");
|
|
443
|
-
this._inTx = true;
|
|
444
|
-
}
|
|
445
|
-
_dbCommitTx() {
|
|
446
|
-
if (!this._inTx || !this.sqlite)
|
|
447
|
-
return;
|
|
448
|
-
this._inTx = false; // clear first so a throw can't wedge us mid-commit
|
|
449
|
-
this.sqlite.exec("COMMIT");
|
|
450
|
-
}
|
|
451
|
-
// -- Node CRUD --
|
|
452
|
-
_dbInsertNode(id, leaf, kids, h) {
|
|
453
|
-
if (!this._insertNode) {
|
|
454
|
-
this._insertNode = this.sqlite.prepare("INSERT INTO node (id, leaf, kids, h) VALUES (?, ?, ?, ?)");
|
|
455
|
-
}
|
|
456
|
-
this._insertNode.run(id, leaf, kids, h);
|
|
457
|
-
// The filter must see every inserted hash BEFORE any probe could — the
|
|
458
|
-
// add sits in the same synchronous call as the row insert.
|
|
459
|
-
const b = this._bloom;
|
|
460
|
-
b.add(h);
|
|
461
|
-
if (b.saturated)
|
|
462
|
-
this._bloomRebuild();
|
|
463
|
-
}
|
|
464
|
-
_dbGetNode(id) {
|
|
465
|
-
if (!this._selNode) {
|
|
466
|
-
this._selNode = this.sqlite.prepare("SELECT id, leaf, kids FROM node WHERE id = ?");
|
|
467
|
-
}
|
|
468
|
-
const r = this._selNode.get(id);
|
|
469
|
-
if (!r)
|
|
470
|
-
return null;
|
|
471
|
-
// A zero-length kids blob marks a FLAT branch: the leaf column holds its
|
|
472
|
-
// bytes and the kid list is derived, one implicit leaf per byte.
|
|
473
|
-
const flat = r.kids !== null && r.kids.byteLength === 0;
|
|
474
|
-
return {
|
|
475
|
-
id: r.id,
|
|
476
|
-
leaf: r.leaf && !flat ? new Uint8Array(r.leaf) : null,
|
|
477
|
-
kids: flat
|
|
478
|
-
? flatBytesKids(r.leaf)
|
|
479
|
-
: (r.kids ? unpackKids(r.kids) : null),
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
_dbFindLeaf(h, bytes) {
|
|
483
|
-
if (this._bloom && !this._bloom.mightContain(h)) {
|
|
484
|
-
this.bloomSkips++;
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
if (!this._selLeaf) {
|
|
488
|
-
this._selLeaf = this.sqlite.prepare("SELECT id FROM node WHERE h = ? AND leaf = ? AND kids IS NULL LIMIT 1");
|
|
489
|
-
}
|
|
490
|
-
const row = this._selLeaf.get(h, bytes);
|
|
491
|
-
return row ? row.id : null;
|
|
492
|
-
}
|
|
493
|
-
_dbFindBranchByLeaf(h, bytes) {
|
|
494
|
-
if (this._bloom && !this._bloom.mightContain(h)) {
|
|
495
|
-
this.bloomSkips++;
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
if (!this._selFlat) {
|
|
499
|
-
this._selFlat = this.sqlite.prepare("SELECT id FROM node WHERE h = ? AND leaf = ? AND kids IS NOT NULL LIMIT 1");
|
|
500
|
-
}
|
|
501
|
-
const row = this._selFlat.get(h, bytes);
|
|
502
|
-
return row ? row.id : null;
|
|
503
|
-
}
|
|
504
|
-
_dbFindBranchByKids(h, packed) {
|
|
505
|
-
if (this._bloom && !this._bloom.mightContain(h)) {
|
|
506
|
-
this.bloomSkips++;
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
if (!this._selKids) {
|
|
510
|
-
this._selKids = this.sqlite.prepare("SELECT id FROM node WHERE h = ? AND kids = ? LIMIT 1");
|
|
511
|
-
}
|
|
512
|
-
const row = this._selKids.get(h, packed);
|
|
513
|
-
return row ? row.id : null;
|
|
514
|
-
}
|
|
515
|
-
// -- Kid (structural parent) edges --
|
|
516
|
-
_dbInsertKid(child, parent) {
|
|
517
|
-
if (!this._insertKid) {
|
|
518
|
-
this._insertKid = this.sqlite.prepare("INSERT OR IGNORE INTO kid (child, parent) VALUES (?, ?)");
|
|
519
|
-
}
|
|
520
|
-
this._insertKid.run(child, parent);
|
|
521
|
-
}
|
|
522
|
-
_dbGetParents(id) {
|
|
523
|
-
if (!this._selParents) {
|
|
524
|
-
this._selParents = this.sqlite.prepare("SELECT parent FROM kid WHERE child = ?");
|
|
525
|
-
}
|
|
526
|
-
return this._selParents.all(id).map((r) => r.parent);
|
|
527
|
-
}
|
|
528
|
-
// -- Containment --
|
|
529
|
-
_selParentsFirst = null;
|
|
530
|
-
_dbGetParentsFirst(id, limit) {
|
|
531
|
-
if (!this._selParentsFirst) {
|
|
532
|
-
this._selParentsFirst = this.sqlite.prepare("SELECT parent FROM kid WHERE child = ? LIMIT ?");
|
|
533
|
-
}
|
|
534
|
-
return this._selParentsFirst.all(id, limit)
|
|
535
|
-
.map((r) => r.parent);
|
|
536
|
-
}
|
|
537
|
-
_selChain = null;
|
|
538
|
-
/** {@link Store.chainRun}'s walk as ONE recursive CTE: the whole
|
|
539
|
-
* transparent chain (no edge in or out, exactly one parent) is descended
|
|
540
|
-
* inside SQLite — per-node work is the same three indexed probes as the
|
|
541
|
-
* base class's loop, but without a JS↔SQLite round trip per node, which
|
|
542
|
-
* dominates on the deep single-structure scaffolding this read exists
|
|
543
|
-
* for. */
|
|
544
|
-
_chainWalk(id, cap) {
|
|
545
|
-
if (!this._selChain) {
|
|
546
|
-
this._selChain = this.sqlite.prepare(`WITH RECURSIVE chain(n, d) AS (
|
|
547
|
-
SELECT ?, 0
|
|
548
|
-
UNION ALL
|
|
549
|
-
SELECT (SELECT parent FROM kid WHERE child = chain.n LIMIT 1),
|
|
550
|
-
chain.d + 1
|
|
551
|
-
FROM chain
|
|
552
|
-
WHERE chain.d < ?
|
|
553
|
-
AND NOT EXISTS (SELECT 1 FROM edge WHERE src = chain.n)
|
|
554
|
-
AND NOT EXISTS (SELECT 1 FROM edge WHERE dst = chain.n)
|
|
555
|
-
AND (SELECT count(*)
|
|
556
|
-
FROM (SELECT 1 FROM kid WHERE child = chain.n LIMIT 2)
|
|
557
|
-
) = 1
|
|
558
|
-
)
|
|
559
|
-
SELECT n FROM chain ORDER BY d`);
|
|
560
|
-
}
|
|
561
|
-
return this._selChain.all(id, cap - 1)
|
|
562
|
-
.map((r) => r.n);
|
|
563
|
-
}
|
|
564
|
-
/** Width of the seq band inside the packed contain rowid: rowid =
|
|
565
|
-
* child·SEQ_SPAN + seq. Page sizes are geometric, so a chain of k live
|
|
566
|
-
* pages needs a base of ≥ 2^k · 4 bytes — seq stays ≤ ~50 for any
|
|
567
|
-
* physically possible list; the append path guards the band anyway. */
|
|
568
|
-
static SEQ_SPAN = 1 << 8;
|
|
569
|
-
_selContainExists = null;
|
|
570
|
-
_dbContainExists(child) {
|
|
571
|
-
if (!this._selContainExists) {
|
|
572
|
-
// A probe of the child's rowid range — one descent.
|
|
573
|
-
this._selContainExists = this.sqlite.prepare("SELECT 1 AS one FROM contain WHERE id >= ? AND id < ? LIMIT 1");
|
|
574
|
-
}
|
|
575
|
-
const base = child * SQliteStore.SEQ_SPAN;
|
|
576
|
-
return this._selContainExists.get(base, base + SQliteStore.SEQ_SPAN) !==
|
|
577
|
-
undefined;
|
|
578
|
-
}
|
|
579
|
-
_selContainPages = null;
|
|
580
|
-
/** The child's page directory — (seq, byte length) per page, O(log fan-in)
|
|
581
|
-
* rows, each read from the cell header without loading the blob. */
|
|
582
|
-
containPages(child) {
|
|
583
|
-
if (!this._selContainPages) {
|
|
584
|
-
this._selContainPages = this.sqlite.prepare("SELECT id, length(parents) AS len FROM contain " +
|
|
585
|
-
"WHERE id >= ? AND id < ? ORDER BY id");
|
|
586
|
-
}
|
|
587
|
-
const base = child * SQliteStore.SEQ_SPAN;
|
|
588
|
-
return this._selContainPages.all(base, base + SQliteStore.SEQ_SPAN)
|
|
589
|
-
.map((r) => ({ seq: r.id - base, len: r.len }));
|
|
590
|
-
}
|
|
591
|
-
_selContainPageSub = null;
|
|
592
|
-
_selContainPage = null;
|
|
593
|
-
containPage(child, seq) {
|
|
594
|
-
if (!this._selContainPage) {
|
|
595
|
-
this._selContainPage = this.sqlite.prepare("SELECT parents FROM contain WHERE id = ?");
|
|
596
|
-
}
|
|
597
|
-
return this._selContainPage.get(child * SQliteStore.SEQ_SPAN + seq).parents;
|
|
598
|
-
}
|
|
599
|
-
_dbGetContainParentsSlice(child, offset, limit) {
|
|
600
|
-
if (!this._selContainPageSub) {
|
|
601
|
-
// substr on a BLOB returns bytes: unpack only the requested span.
|
|
602
|
-
this._selContainPageSub = this.sqlite.prepare("SELECT substr(parents, 1 + ? * 4, ? * 4) AS page " +
|
|
603
|
-
"FROM contain WHERE id = ?");
|
|
604
|
-
}
|
|
605
|
-
const out = [];
|
|
606
|
-
let skip = offset;
|
|
607
|
-
for (const seg of this.containPages(child)) {
|
|
608
|
-
const n = seg.len >>> 2;
|
|
609
|
-
if (skip >= n) {
|
|
610
|
-
skip -= n;
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
const take = Math.min(limit - out.length, n - skip);
|
|
614
|
-
const row = this._selContainPageSub.get(skip, take, child * SQliteStore.SEQ_SPAN + seg.seq);
|
|
615
|
-
if (row && row.page.length > 0)
|
|
616
|
-
out.push(...unpackKids(row.page));
|
|
617
|
-
skip = 0;
|
|
618
|
-
if (out.length >= limit)
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
return out;
|
|
622
|
-
}
|
|
623
|
-
_selContainCount = null;
|
|
624
|
-
_dbGetContainCount(child) {
|
|
625
|
-
if (!this._selContainCount) {
|
|
626
|
-
// Stored ENTRY count (transient duplicates included) — consistent with
|
|
627
|
-
// what the slice streams, which is all the seam math needs.
|
|
628
|
-
this._selContainCount = this.sqlite.prepare("SELECT COALESCE(SUM(length(parents)), 0) / 4 AS n " +
|
|
629
|
-
"FROM contain WHERE id >= ? AND id < ?");
|
|
630
|
-
}
|
|
631
|
-
const base = child * SQliteStore.SEQ_SPAN;
|
|
632
|
-
return this._selContainCount.get(base, base + SQliteStore.SEQ_SPAN).n;
|
|
633
|
-
}
|
|
634
|
-
_dbGetContainParents(child) {
|
|
635
|
-
if (!this._selContain) {
|
|
636
|
-
this._selContain = this.sqlite.prepare("SELECT parents FROM contain WHERE id >= ? AND id < ? ORDER BY id");
|
|
637
|
-
}
|
|
638
|
-
const base = child * SQliteStore.SEQ_SPAN;
|
|
639
|
-
const rows = this._selContain.all(base, base + SQliteStore.SEQ_SPAN);
|
|
640
|
-
if (rows.length === 0)
|
|
641
|
-
return [];
|
|
642
|
-
// Dedup across pages (a pair re-added after its page merged into the base
|
|
643
|
-
// may repeat in the tail until the next base merge), preserving order.
|
|
644
|
-
const seen = new Set();
|
|
645
|
-
const out = [];
|
|
646
|
-
for (const r of rows) {
|
|
647
|
-
for (const p of unpackKids(r.parents)) {
|
|
648
|
-
if (!seen.has(p)) {
|
|
649
|
-
seen.add(p);
|
|
650
|
-
out.push(p);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return out;
|
|
655
|
-
}
|
|
656
|
-
_upsContainPage = null;
|
|
657
|
-
_delContainPage = null;
|
|
658
|
-
_dbAppendContain(child, parents) {
|
|
659
|
-
if (parents.length === 0)
|
|
660
|
-
return;
|
|
661
|
-
if (!this._upsContainPage) {
|
|
662
|
-
this._upsContainPage = this.sqlite.prepare("INSERT INTO contain (id, parents) VALUES (?, ?) " +
|
|
663
|
-
"ON CONFLICT(id) DO UPDATE SET parents = excluded.parents");
|
|
664
|
-
this._delContainPage = this.sqlite.prepare("DELETE FROM contain WHERE id = ?");
|
|
665
|
-
}
|
|
666
|
-
// Fold stored tail pages into the new page IN MEMORY, then write ONCE.
|
|
667
|
-
// Merge rule per fold: below a 256-byte floor always merge (most children
|
|
668
|
-
// have a handful of parents — two tiny rows would double their per-row
|
|
669
|
-
// overhead for no amortization benefit); above it, merge only while the
|
|
670
|
-
// stored page is ≤ 2× the accumulated one, which bounds total re-writing
|
|
671
|
-
// to O(fan-in · log fan-in) bytes and keeps the page directory
|
|
672
|
-
// logarithmic. A fold that reaches the seq-0 base page dedups, squeezing
|
|
673
|
-
// out duplicates on the same geometric schedule. Writing the fold's
|
|
674
|
-
// result as one upsert (an in-place row replace in the common
|
|
675
|
-
// small-child case) rather than delete+insert churn keeps B-tree leaf
|
|
676
|
-
// fill at the packed single-row format's level — measured: churn alone
|
|
677
|
-
// cost ~26% extra pages on the same payload.
|
|
678
|
-
const segs = this.containPages(child);
|
|
679
|
-
let cur = packKids(parents);
|
|
680
|
-
let seq = segs.length > 0 ? segs[segs.length - 1].seq + 1 : 0;
|
|
681
|
-
if (seq >= SQliteStore.SEQ_SPAN) {
|
|
682
|
-
throw new Error(`contain page seq overflow for child ${child}`);
|
|
683
|
-
}
|
|
684
|
-
const folded = [];
|
|
685
|
-
while (segs.length > 0) {
|
|
686
|
-
const a = segs[segs.length - 1];
|
|
687
|
-
if (a.len + cur.byteLength > 256 && a.len > 2 * cur.byteLength)
|
|
688
|
-
break;
|
|
689
|
-
const pa = this.containPage(child, a.seq);
|
|
690
|
-
if (a.seq === 0) {
|
|
691
|
-
const seen = new Set();
|
|
692
|
-
const ids = [];
|
|
693
|
-
for (const p of unpackKids(pa)) {
|
|
694
|
-
if (!seen.has(p)) {
|
|
695
|
-
seen.add(p);
|
|
696
|
-
ids.push(p);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
for (const p of unpackKids(cur)) {
|
|
700
|
-
if (!seen.has(p)) {
|
|
701
|
-
seen.add(p);
|
|
702
|
-
ids.push(p);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
cur = packKids(ids);
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
708
|
-
const m = new Uint8Array(pa.byteLength + cur.byteLength);
|
|
709
|
-
m.set(pa, 0);
|
|
710
|
-
m.set(cur, pa.byteLength);
|
|
711
|
-
cur = m;
|
|
712
|
-
}
|
|
713
|
-
folded.push(a.seq);
|
|
714
|
-
seq = a.seq;
|
|
715
|
-
segs.pop();
|
|
716
|
-
}
|
|
717
|
-
const base = child * SQliteStore.SEQ_SPAN;
|
|
718
|
-
for (const s of folded) {
|
|
719
|
-
if (s !== seq)
|
|
720
|
-
this._delContainPage.run(base + s);
|
|
721
|
-
}
|
|
722
|
-
this._upsContainPage.run(base + seq, cur);
|
|
723
|
-
}
|
|
724
|
-
// -- Continuation edges --
|
|
725
|
-
_dbInsertEdge(src, dst) {
|
|
726
|
-
if (!this._insertEdge) {
|
|
727
|
-
this._insertEdge = this.sqlite.prepare("INSERT OR IGNORE INTO edge (src, dst) VALUES (?, ?)");
|
|
728
|
-
}
|
|
729
|
-
this._insertEdge.run(src, dst);
|
|
730
|
-
}
|
|
731
|
-
_dbGetNextEdges(id) {
|
|
732
|
-
if (!this._selNext) {
|
|
733
|
-
this._selNext = this.sqlite.prepare("SELECT dst FROM edge WHERE src = ? ORDER BY seq ASC");
|
|
734
|
-
}
|
|
735
|
-
return this._selNext.all(id).map((r) => r.dst);
|
|
736
|
-
}
|
|
737
|
-
_dbGetPrevEdges(id) {
|
|
738
|
-
if (!this._selPrev) {
|
|
739
|
-
this._selPrev = this.sqlite.prepare("SELECT src FROM edge WHERE dst = ? ORDER BY seq DESC");
|
|
740
|
-
}
|
|
741
|
-
return this._selPrev.all(id).map((r) => r.src);
|
|
742
|
-
}
|
|
743
|
-
_selNextFirst = null;
|
|
744
|
-
_dbGetNextEdgesFirst(id, limit) {
|
|
745
|
-
if (!this._selNextFirst) {
|
|
746
|
-
this._selNextFirst = this.sqlite.prepare("SELECT dst FROM edge WHERE src = ? ORDER BY seq ASC LIMIT ?");
|
|
747
|
-
}
|
|
748
|
-
return this._selNextFirst.all(id, limit)
|
|
749
|
-
.map((r) => r.dst);
|
|
750
|
-
}
|
|
751
|
-
_selPrevFirst = null;
|
|
752
|
-
_dbGetPrevEdgesFirst(id, limit) {
|
|
753
|
-
if (!this._selPrevFirst) {
|
|
754
|
-
this._selPrevFirst = this.sqlite.prepare("SELECT src FROM edge WHERE dst = ? ORDER BY seq DESC LIMIT ?");
|
|
755
|
-
}
|
|
756
|
-
return this._selPrevFirst.all(id, limit)
|
|
757
|
-
.map((r) => r.src);
|
|
758
|
-
}
|
|
759
|
-
_selPrevCount = null;
|
|
760
|
-
/** {@link Store.prevCount} — one indexed COUNT over idx_edge_dst, never a
|
|
761
|
-
* row materialisation (a common continuation's reverse fan-in is
|
|
762
|
-
* corpus-sized). */
|
|
763
|
-
prevCount(id) {
|
|
764
|
-
if (!this._selPrevCount) {
|
|
765
|
-
this._selPrevCount = this.sqlite.prepare("SELECT COUNT(*) AS n FROM edge WHERE dst = ?");
|
|
766
|
-
}
|
|
767
|
-
return this._selPrevCount.get(id).n;
|
|
768
|
-
}
|
|
769
|
-
_selSrcExists = null;
|
|
770
|
-
_dbEdgeSrcExists(src) {
|
|
771
|
-
if (!this._selSrcExists) {
|
|
772
|
-
// A prefix probe of idx_edge(src, dst) — one B-tree descent.
|
|
773
|
-
this._selSrcExists = this.sqlite.prepare("SELECT 1 AS one FROM edge WHERE src = ? LIMIT 1");
|
|
774
|
-
}
|
|
775
|
-
return this._selSrcExists.get(src) !== undefined;
|
|
776
|
-
}
|
|
777
|
-
_dbEdgeDistinctSrcCount() {
|
|
778
|
-
return this.sqlite.prepare("SELECT COUNT(*) AS n FROM (SELECT DISTINCT src FROM edge)").get().n;
|
|
779
|
-
}
|
|
780
|
-
// -- Halos (durable 2-bit quantized rows) --
|
|
781
|
-
_dbGetHalo(id) {
|
|
782
|
-
if (!this._selHalo) {
|
|
783
|
-
this._selHalo = this.sqlite.prepare("SELECT vec, mass FROM halo WHERE id = ?");
|
|
784
|
-
}
|
|
785
|
-
const r = this._selHalo.get(id);
|
|
786
|
-
return r ?? null;
|
|
787
|
-
}
|
|
788
|
-
_dbUpsertHalo(id, encodedVec, mass) {
|
|
789
|
-
if (!this._upHalo) {
|
|
790
|
-
this._upHalo = this.sqlite.prepare("INSERT INTO halo (id, vec, mass) VALUES (?, ?, ?) " +
|
|
791
|
-
"ON CONFLICT(id) DO UPDATE SET vec = excluded.vec, mass = excluded.mass");
|
|
792
|
-
}
|
|
793
|
-
this._upHalo.run(id, encodedVec, mass);
|
|
794
|
-
}
|
|
795
|
-
// -- Meta --
|
|
796
|
-
_dbGetMeta(key) {
|
|
797
|
-
if (!this._getMeta) {
|
|
798
|
-
this._getMeta = this.sqlite.prepare("SELECT val FROM meta WHERE key = ?");
|
|
799
|
-
}
|
|
800
|
-
const row = this._getMeta.get(key);
|
|
801
|
-
return row ? row.val : null;
|
|
802
|
-
}
|
|
803
|
-
_dbSetMeta(key, val) {
|
|
804
|
-
if (!this._setMeta) {
|
|
805
|
-
this._setMeta = this.sqlite.prepare("INSERT INTO meta (key, val) VALUES (?, ?) " +
|
|
806
|
-
"ON CONFLICT(key) DO UPDATE SET val = excluded.val");
|
|
807
|
-
}
|
|
808
|
-
this._setMeta.run(key, val);
|
|
809
|
-
}
|
|
810
|
-
_dbDeleteMeta(key) {
|
|
811
|
-
if (!this._delMeta) {
|
|
812
|
-
this._delMeta = this.sqlite.prepare("DELETE FROM meta WHERE key = ?");
|
|
813
|
-
}
|
|
814
|
-
this._delMeta.run(key);
|
|
815
|
-
}
|
|
816
|
-
// -- Canonical-form index (Store optional capability) --
|
|
817
|
-
canonAdd(h, id) {
|
|
818
|
-
if (!this._insCanon) {
|
|
819
|
-
this._insCanon = this.sqlite.prepare("INSERT OR IGNORE INTO canon (h, id) VALUES (?, ?)");
|
|
820
|
-
}
|
|
821
|
-
// Join the deferred write transaction (committed by flush/commit), so a
|
|
822
|
-
// bulk index build coalesces instead of paying autocommit per row.
|
|
823
|
-
this._dbBeginTx();
|
|
824
|
-
this._insCanon.run(h, id);
|
|
825
|
-
}
|
|
826
|
-
canonFind(h) {
|
|
827
|
-
if (!this._selCanon) {
|
|
828
|
-
this._selCanon = this.sqlite.prepare("SELECT id FROM canon WHERE h = ?");
|
|
829
|
-
}
|
|
830
|
-
return this._selCanon.all(h).map((r) => r.id);
|
|
831
|
-
}
|
|
832
|
-
canonCount() {
|
|
833
|
-
if (!this._cntCanon) {
|
|
834
|
-
this._cntCanon = this.sqlite.prepare("SELECT count(*) AS c FROM canon");
|
|
835
|
-
}
|
|
836
|
-
return this._cntCanon.get().c;
|
|
837
|
-
}
|
|
838
|
-
eachContent(cb, fromId = 0) {
|
|
839
|
-
if (!this._selContentFrom) {
|
|
840
|
-
// Content-bearing nodes are FLAT branches: leaf present, kids an
|
|
841
|
-
// empty (zero-length) blob — the same population PART 2 of the
|
|
842
|
-
// diagnostics calls "distinct content spans".
|
|
843
|
-
this._selContentFrom = this.sqlite.prepare("SELECT id, leaf FROM node " +
|
|
844
|
-
"WHERE id >= ? AND leaf IS NOT NULL " +
|
|
845
|
-
"AND (kids IS NULL OR length(kids) = 0)");
|
|
846
|
-
}
|
|
847
|
-
for (const row of this._selContentFrom.iterate(fromId)) {
|
|
848
|
-
const r = row;
|
|
849
|
-
cb(r.id, new Uint8Array(r.leaf));
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
// -- Snapshot --
|
|
853
|
-
_dbSaveSnapshot(bytes) {
|
|
854
|
-
if (!this._insSnapshot) {
|
|
855
|
-
this._insSnapshot = this.sqlite.prepare("INSERT INTO snapshot (id, data) VALUES (1, ?) " +
|
|
856
|
-
"ON CONFLICT(id) DO UPDATE SET data = excluded.data");
|
|
857
|
-
}
|
|
858
|
-
this._insSnapshot.run(bytes);
|
|
859
|
-
// Checkpoint cadence: persisting the dedup filter here keeps the crash
|
|
860
|
-
// top-up (the id ≥ upto rescan at next open) bounded by one checkpoint
|
|
861
|
-
// interval instead of the whole session.
|
|
862
|
-
this._bloomPersist();
|
|
863
|
-
}
|
|
864
|
-
_dbLoadSnapshot() {
|
|
865
|
-
if (!this._selSnapshot) {
|
|
866
|
-
this._selSnapshot = this.sqlite.prepare("SELECT data FROM snapshot WHERE id = 1");
|
|
867
|
-
}
|
|
868
|
-
const r = this._selSnapshot.get();
|
|
869
|
-
return r ? new Uint8Array(r.data) : null;
|
|
870
|
-
}
|
|
871
|
-
// -- Vector DB: content (gist) index --
|
|
872
|
-
_vecContentUpsert(entries) {
|
|
873
|
-
this.content.upsertMany(entries);
|
|
874
|
-
}
|
|
875
|
-
_vecContentQuery(v, k, ef) {
|
|
876
|
-
return this.content.query(v, k, { ef });
|
|
877
|
-
}
|
|
878
|
-
_vecContentHas(id) {
|
|
879
|
-
return this.content ? this.content.has(id) : false;
|
|
880
|
-
}
|
|
881
|
-
_vecContentSize() {
|
|
882
|
-
return this.content ? this.content.size : 0;
|
|
883
|
-
}
|
|
884
|
-
_vecContentLastReads() {
|
|
885
|
-
return this.content ? this.content.lastQueryStorageReads : 0;
|
|
886
|
-
}
|
|
887
|
-
_vecContentPhysicalSize() {
|
|
888
|
-
return this.content ? this.content.physicalSize : 0;
|
|
889
|
-
}
|
|
890
|
-
_vecContentClusterCount() {
|
|
891
|
-
return this.content ? this.content.clusterCount : 0;
|
|
892
|
-
}
|
|
893
|
-
_vecContentCompact() {
|
|
894
|
-
this.content.compact();
|
|
895
|
-
}
|
|
896
|
-
_vecContentDeleteMany(ids) {
|
|
897
|
-
this.content.deleteMany(ids);
|
|
898
|
-
}
|
|
899
|
-
*_vecContentEntriesSince(after) {
|
|
900
|
-
if (this.content)
|
|
901
|
-
yield* this.content.keysSince(after);
|
|
902
|
-
}
|
|
903
|
-
/** {@link AbstractStore._dbEdgeOrHaloIds} — one C-side scan; UNION dedups
|
|
904
|
-
* and (with the ORDER BY) sorts, so the result is ready for the binary-
|
|
905
|
-
* search membership probes and the ascending-id maintenance walks. */
|
|
906
|
-
_dbEdgeOrHaloIds() {
|
|
907
|
-
const rows = this.sqlite.prepare("SELECT src AS id FROM edge UNION SELECT dst FROM edge " +
|
|
908
|
-
"UNION SELECT id FROM halo ORDER BY 1").all();
|
|
909
|
-
const out = new Array(rows.length);
|
|
910
|
-
for (let i = 0; i < rows.length; i++)
|
|
911
|
-
out[i] = rows[i].id;
|
|
912
|
-
return out;
|
|
913
|
-
}
|
|
914
|
-
// -- Vector DB: halo index --
|
|
915
|
-
_vecHaloUpsert(entries) {
|
|
916
|
-
this.halos.upsertMany(entries);
|
|
917
|
-
}
|
|
918
|
-
_vecHaloQuery(v, k, ef) {
|
|
919
|
-
return this.halos.query(v, k, { ef });
|
|
920
|
-
}
|
|
921
|
-
_vecHaloSize() {
|
|
922
|
-
return this.halos ? this.halos.size : 0;
|
|
923
|
-
}
|
|
924
|
-
_vecHaloPhysicalSize() {
|
|
925
|
-
return this.halos ? this.halos.physicalSize : 0;
|
|
926
|
-
}
|
|
927
|
-
_vecHaloClusterCount() {
|
|
928
|
-
return this.halos ? this.halos.clusterCount : 0;
|
|
929
|
-
}
|
|
930
|
-
_vecHaloCompact() {
|
|
931
|
-
this.halos.compact();
|
|
932
|
-
}
|
|
933
|
-
/** Pre-fill both vector indices' RAM caches with sequential scans (up to
|
|
934
|
-
* their budget caps) — seconds of streaming instead of the minutes of
|
|
935
|
-
* random point reads a cold training/query session otherwise pays while
|
|
936
|
-
* warming. Optional; call once after open before sustained work.
|
|
937
|
-
* Returns rows warmed. */
|
|
938
|
-
async warmVectorCaches() {
|
|
939
|
-
await this._ensureReady();
|
|
940
|
-
return (this.content?.warmCache() ?? 0) + (this.halos?.warmCache() ?? 0);
|
|
941
|
-
}
|
|
942
|
-
}
|