@hviana/sema 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/AGENTS.md +6 -5
  2. package/CITATION.cff +49 -0
  3. package/HOW_IT_WORKS.md +11 -12
  4. package/README.md +7 -5
  5. package/dist/example/demo.js +25 -25
  6. package/dist/example/train_base.d.ts +19 -21
  7. package/dist/example/train_base.js +1700 -1952
  8. package/dist/src/alphabet.d.ts +3 -3
  9. package/dist/src/alphabet.js +24 -27
  10. package/dist/src/alu/src/alu.d.ts +169 -194
  11. package/dist/src/alu/src/alu.js +374 -414
  12. package/dist/src/alu/src/expr.d.ts +36 -43
  13. package/dist/src/alu/src/expr.js +252 -278
  14. package/dist/src/alu/src/index.d.ts +9 -98
  15. package/dist/src/alu/src/index.js +10 -64
  16. package/dist/src/alu/src/kernel-arith.d.ts +1 -5
  17. package/dist/src/alu/src/kernel-arith.js +195 -285
  18. package/dist/src/alu/src/kernel-bits.d.ts +1 -5
  19. package/dist/src/alu/src/kernel-bits.js +81 -115
  20. package/dist/src/alu/src/kernel-logic.js +33 -63
  21. package/dist/src/alu/src/kernel-nd.js +155 -195
  22. package/dist/src/alu/src/kernel-numeric.d.ts +16 -80
  23. package/dist/src/alu/src/kernel-numeric.js +259 -331
  24. package/dist/src/alu/src/operation.d.ts +112 -132
  25. package/dist/src/alu/src/operation.js +149 -163
  26. package/dist/src/alu/src/parser.d.ts +168 -175
  27. package/dist/src/alu/src/parser.js +382 -421
  28. package/dist/src/alu/src/resonance.d.ts +16 -25
  29. package/dist/src/alu/src/resonance.js +49 -56
  30. package/dist/src/alu/src/text.d.ts +3 -7
  31. package/dist/src/alu/src/text.js +31 -37
  32. package/dist/src/alu/src/value.d.ts +14 -14
  33. package/dist/src/alu/src/value.js +150 -160
  34. package/dist/src/alu/test/alu.test.js +543 -656
  35. package/dist/src/bytes.d.ts +1 -5
  36. package/dist/src/bytes.js +33 -40
  37. package/dist/src/config.d.ts +99 -102
  38. package/dist/src/config.js +78 -83
  39. package/dist/src/derive/src/deduction.d.ts +60 -62
  40. package/dist/src/derive/src/deduction.js +103 -109
  41. package/dist/src/derive/src/index.d.ts +1 -7
  42. package/dist/src/derive/src/priority-queue.d.ts +8 -8
  43. package/dist/src/derive/src/priority-queue.js +57 -61
  44. package/dist/src/derive/src/rewrite.d.ts +15 -18
  45. package/dist/src/derive/src/rewrite.js +59 -67
  46. package/dist/src/derive/src/trie.d.ts +56 -56
  47. package/dist/src/derive/src/trie.js +175 -179
  48. package/dist/src/derive/test/derive.test.js +96 -100
  49. package/dist/src/extension.d.ts +13 -16
  50. package/dist/src/geometry.d.ts +29 -43
  51. package/dist/src/geometry.js +324 -268
  52. package/dist/src/index.d.ts +2 -17
  53. package/dist/src/index.js +2 -12
  54. package/dist/src/ingest-cache.d.ts +25 -30
  55. package/dist/src/ingest-cache.js +108 -127
  56. package/dist/src/mind/articulation.d.ts +1 -5
  57. package/dist/src/mind/articulation.js +77 -112
  58. package/dist/src/mind/attention.d.ts +40 -108
  59. package/dist/src/mind/attention.js +754 -872
  60. package/dist/src/mind/canonical.d.ts +4 -19
  61. package/dist/src/mind/canonical.js +27 -31
  62. package/dist/src/mind/graph-search.d.ts +201 -225
  63. package/dist/src/mind/graph-search.js +742 -821
  64. package/dist/src/mind/index.d.ts +2 -10
  65. package/dist/src/mind/junction.d.ts +31 -58
  66. package/dist/src/mind/junction.js +172 -237
  67. package/dist/src/mind/learning.d.ts +16 -52
  68. package/dist/src/mind/learning.js +143 -165
  69. package/dist/src/mind/match.d.ts +20 -69
  70. package/dist/src/mind/match.js +259 -318
  71. package/dist/src/mind/mechanisms/alu.js +16 -16
  72. package/dist/src/mind/mechanisms/cast.d.ts +9 -13
  73. package/dist/src/mind/mechanisms/cast.js +363 -456
  74. package/dist/src/mind/mechanisms/confluence.d.ts +8 -12
  75. package/dist/src/mind/mechanisms/confluence.js +152 -183
  76. package/dist/src/mind/mechanisms/cover.d.ts +2 -8
  77. package/dist/src/mind/mechanisms/cover.js +148 -210
  78. package/dist/src/mind/mechanisms/extraction.d.ts +8 -34
  79. package/dist/src/mind/mechanisms/extraction.js +234 -288
  80. package/dist/src/mind/mechanisms/recall.d.ts +6 -10
  81. package/dist/src/mind/mechanisms/recall.js +141 -185
  82. package/dist/src/mind/mind.d.ts +194 -155
  83. package/dist/src/mind/mind.js +392 -289
  84. package/dist/src/mind/pipeline-mechanism.d.ts +112 -124
  85. package/dist/src/mind/pipeline-mechanism.js +161 -172
  86. package/dist/src/mind/pipeline.d.ts +4 -14
  87. package/dist/src/mind/pipeline.js +125 -189
  88. package/dist/src/mind/primitives.d.ts +20 -33
  89. package/dist/src/mind/primitives.js +124 -120
  90. package/dist/src/mind/rationale.d.ts +86 -98
  91. package/dist/src/mind/rationale.js +113 -121
  92. package/dist/src/mind/reasoning.d.ts +2 -13
  93. package/dist/src/mind/reasoning.js +129 -166
  94. package/dist/src/mind/recognition.d.ts +1 -4
  95. package/dist/src/mind/recognition.js +198 -210
  96. package/dist/src/mind/resonance.d.ts +5 -22
  97. package/dist/src/mind/resonance.js +0 -0
  98. package/dist/src/mind/trace.d.ts +6 -25
  99. package/dist/src/mind/trace.js +50 -58
  100. package/dist/src/mind/traverse.d.ts +29 -58
  101. package/dist/src/mind/traverse.js +344 -356
  102. package/dist/src/mind/types.d.ts +122 -127
  103. package/dist/src/mind/types.js +60 -69
  104. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  105. package/dist/src/rabitq-ivf/src/database.js +201 -0
  106. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  107. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
  108. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  109. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  110. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  111. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  112. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  113. package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
  114. package/dist/src/sema.d.ts +9 -13
  115. package/dist/src/sema.js +26 -40
  116. package/dist/src/store-sqlite.d.ts +171 -166
  117. package/dist/src/store-sqlite.js +768 -662
  118. package/dist/src/store.d.ts +597 -630
  119. package/dist/src/store.js +1434 -1581
  120. package/dist/src/vec.d.ts +5 -9
  121. package/dist/src/vec.js +61 -73
  122. package/example/train_base.ts +33 -15
  123. package/index.html +65 -0
  124. package/package.json +1 -1
  125. package/src/alu/README.md +1 -1
  126. package/src/alu/src/index.ts +1 -1
  127. package/src/config.ts +19 -27
  128. package/src/geometry.ts +93 -1
  129. package/src/index.ts +6 -11
  130. package/src/mind/attention.ts +11 -6
  131. package/src/mind/mechanisms/recall.ts +29 -13
  132. package/src/mind/mind.ts +250 -19
  133. package/src/mind/primitives.ts +43 -6
  134. package/src/mind/recognition.ts +26 -8
  135. package/src/mind/traverse.ts +47 -0
  136. package/src/mind/types.ts +20 -21
  137. package/src/rabitq-ivf/README.md +56 -0
  138. package/src/rabitq-ivf/src/database.ts +276 -0
  139. package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
  140. package/src/rabitq-ivf/src/ivf.ts +1330 -0
  141. package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
  142. package/src/store-sqlite.ts +204 -9
  143. package/src/store.ts +27 -34
  144. package/test/08-storage.test.mjs +3 -3
  145. package/test/13-conversation.test.mjs +190 -20
  146. package/test/14-scaling.test.mjs +2 -2
  147. package/test/35-ivf.test.mjs +263 -0
  148. package/test/36-bloom.test.mjs +123 -0
  149. package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
  150. package/dist/src/rabitq-hnsw/src/database.js +0 -405
  151. package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
  152. package/dist/src/rabitq-hnsw/src/heap.js +0 -94
  153. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
  154. package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
  155. package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
  156. package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
  157. package/dist/src/rabitq-hnsw/src/prng.js +0 -38
  158. package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
  159. package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
  160. package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
  161. package/dist/src/rabitq-hnsw/src/store.js +0 -916
  162. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
  163. package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
  164. package/src/rabitq-hnsw/README.md +0 -303
  165. package/src/rabitq-hnsw/src/database.ts +0 -492
  166. package/src/rabitq-hnsw/src/heap.ts +0 -90
  167. package/src/rabitq-hnsw/src/hnsw.ts +0 -514
  168. package/src/rabitq-hnsw/src/store.ts +0 -994
  169. package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
  170. /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
@@ -1,514 +0,0 @@
1
- import { Heap } from "./heap.js";
2
- import { Prng } from "./prng.js";
3
- import { QueryContext, RaBitQuantizer } from "./rabitq.js";
4
- import { NodeRec, Store } from "./store.js";
5
-
6
- export interface HnswParams {
7
- M: number;
8
- efConstruction: number;
9
- efSearch: number;
10
- seed: number;
11
- }
12
-
13
- export interface KnnHit {
14
- /** external (user) id */
15
- id: number;
16
- /** estimated cosine distance (1 - cosine) */
17
- distance: number;
18
- }
19
-
20
- /**
21
- * Hierarchical Navigable Small World graph (Malkov & Yashunin, 2018) backed by
22
- * 1-bit RaBitQ codes that live in SQLite (see Store) rather than in RAM. The
23
- * algorithm is the textbook one; the only difference is that codes and adjacency
24
- * lists are read and written through the store on demand, so the resident set is
25
- * the working set of the current operation -- never the whole graph.
26
- *
27
- * Distances use the codes throughout:
28
- * - searching with a full-precision query uses RaBitQ's accurate estimator;
29
- * - building the graph and searching by code compare two codes by sign-bit
30
- * Hamming distance.
31
- * Building on codes alone is what lets `compact` rebuild an honest index with no
32
- * access to the original vectors.
33
- */
34
- export class HnswIndex {
35
- readonly M: number;
36
- readonly Mmax0: number;
37
- readonly efConstruction: number;
38
-
39
- private readonly mL: number;
40
- private readonly quantizer: RaBitQuantizer;
41
- private readonly store: Store;
42
- private readonly seed: number;
43
- private readonly levelRng: Prng;
44
-
45
- // tiny global scalars, cached from meta and written through on change
46
- private entry: number;
47
- private maxLevel: number;
48
- private live: number;
49
- private total: number;
50
- private _efSearch: number;
51
-
52
- // Per-operation working set: the records the current insert/query is actively
53
- // comparing. Cleared at the start of every op and bounded by efConstruction /
54
- // efSearch (the nodes one operation can touch), never by the collection size.
55
- // This is the algorithm's working memory -- it makes each touched node cost one
56
- // storage read per op -- not a tunable cache. The only cache is SQLite's page
57
- // cache (see Store), sized in MB.
58
- private readonly working = new Map<number, NodeRec>();
59
- private readonly visited = new Set<number>();
60
- // Epoch-tagged visited marks — the Set above without per-candidate hashing:
61
- // visitedTag[id] === visitedEpoch means "seen this operation", and bumping
62
- // the epoch clears the whole set in O(1). Semantically IDENTICAL to the
63
- // Set (results never differ); used only when the store has a cache budget,
64
- // because the array is 4 B per internal id — amortized working memory that
65
- // grows with the collection, which the cacheSizeMb:0 flat-memory mode must
66
- // not pay. The epoch wrap (2³²−1 ops) refills the array once.
67
- private visitedTag = new Uint32Array(1024);
68
- private visitedEpoch = 0;
69
-
70
- private seenTag(id: number, epoch: number): boolean {
71
- return id < this.visitedTag.length && this.visitedTag[id] === epoch;
72
- }
73
-
74
- private markVisited(id: number, epoch: number): void {
75
- if (id >= this.visitedTag.length) {
76
- const grown = new Uint32Array(
77
- Math.max(id + 1, this.visitedTag.length * 2),
78
- );
79
- grown.set(this.visitedTag);
80
- this.visitedTag = grown;
81
- }
82
- this.visitedTag[id] = epoch;
83
- }
84
- private readonly candHeap = new Heap(true); // min-heap: nearest first
85
- private readonly resHeap = new Heap(false); // max-heap: farthest first
86
- private readonly singleEp: number[] = [0];
87
- private readonly idxScratch: number[] = [];
88
- private readonly distScratch: number[] = [];
89
- private readonly freshScratch: number[] = [];
90
-
91
- // distance dispatch: a full-precision query sets qCtx; building or a code
92
- // query sets refBytes (code<->code). `building` only suppresses the counter.
93
- private qCtx: QueryContext | null = null;
94
- private refBytes: Uint8Array | null = null;
95
- private building = false;
96
-
97
- lastQueryDistComps = 0;
98
- /** Storage row reads issued by the most recent query (cache-independent). */
99
- lastQueryStorageReads = 0;
100
-
101
- constructor(quantizer: RaBitQuantizer, store: Store, params: HnswParams) {
102
- this.quantizer = quantizer;
103
- this.store = store;
104
- this.M = params.M;
105
- this.Mmax0 = this.M * 2;
106
- this.efConstruction = params.efConstruction;
107
- this._efSearch = params.efSearch;
108
- this.mL = 1 / Math.log(this.M);
109
- this.seed = params.seed >>> 0;
110
-
111
- const s = store.loadState();
112
- this.entry = s.entryPoint;
113
- this.maxLevel = s.maxLevel;
114
- this.live = s.live;
115
- this.total = s.total;
116
- this.levelRng = new Prng(this.seed);
117
- this.levelRng.restore(s.rng);
118
- }
119
-
120
- get size(): number {
121
- return this.live;
122
- }
123
- get physicalSize(): number {
124
- return this.total;
125
- }
126
- get bytesPerVector(): number {
127
- return this.quantizer.codeWords * 4;
128
- }
129
- get efSearch(): number {
130
- return this._efSearch;
131
- }
132
- set efSearch(v: number) {
133
- this._efSearch = Math.max(1, v | 0);
134
- this.store.setEfSearch(this._efSearch);
135
- }
136
-
137
- private persistState(): void {
138
- this.store.saveState({
139
- entryPoint: this.entry,
140
- maxLevel: this.maxLevel,
141
- live: this.live,
142
- total: this.total,
143
- rng: this.levelRng.snapshot(),
144
- });
145
- }
146
-
147
- private randomLevel(): number {
148
- let u = this.levelRng.next();
149
- if (u < 1e-12) u = 1e-12;
150
- return Math.floor(-Math.log(u) * this.mL);
151
- }
152
-
153
- /**
154
- * Fetch a node record into the operation's working set. The first touch in an
155
- * op is a storage read; later touches in the same op reuse it. The set is
156
- * cleared per op and bounded by efConstruction / efSearch, so storage reads
157
- * per op equal the number of *distinct* nodes the op visits -- minimal, and
158
- * independent of any cache.
159
- */
160
- private node(id: number): NodeRec {
161
- const hit = this.working.get(id);
162
- if (hit !== undefined) return hit;
163
- const rec = this.store.getNode(id);
164
- if (rec === null) throw new Error(`node ${id} not found`);
165
- this.working.set(id, rec);
166
- return rec;
167
- }
168
-
169
- /** Prefetch several nodes into the working set with ONE batched storage
170
- * read for the misses. Point queries per neighbour made statement
171
- * dispatch — not distance arithmetic — the dominant cost of a large
172
- * build; the batch keeps `reads` accounting identical per row. */
173
- private fetchNodes(ids: number[], count = ids.length): void {
174
- // getNodesInto skips ids already present in `working` itself — no
175
- // second pre-filter pass over the same map here.
176
- this.store.getNodesInto(ids, this.working, count);
177
- }
178
-
179
- /** Distance from the current source (query vector or reference code) to a node. */
180
- private distOf(rec: NodeRec): number {
181
- if (this.qCtx !== null) {
182
- this.lastQueryDistComps++;
183
- return this.quantizer.estimate(rec.code, 0, this.qCtx);
184
- }
185
- if (!this.building) this.lastQueryDistComps++;
186
- return this.quantizer.codeDistanceBytes(
187
- this.refBytes as Uint8Array,
188
- rec.code,
189
- );
190
- }
191
-
192
- /** Code-to-code distance between two stored nodes (graph wiring only). */
193
- private codeDist(a: number, b: number): number {
194
- return this.quantizer.codeDistanceBytes(
195
- this.node(a).code,
196
- this.node(b).code,
197
- );
198
- }
199
-
200
- /**
201
- * SEARCH-LAYER (Algorithm 2). Frontier in `candHeap`, bounded result set in
202
- * `resHeap` (non-deleted only). Deleted nodes are traversed for routing but
203
- * never returned. `visited` is a per-call set sized by the nodes seen here.
204
- */
205
- private searchLayer(entryPoints: number[], ef: number, layer: number): void {
206
- const cand = this.candHeap;
207
- const res = this.resHeap;
208
- cand.clear();
209
- res.clear();
210
- // Visited marks: epoch tags when RAM-for-speed is allowed, the plain Set
211
- // in flat-memory mode. Identical semantics either way.
212
- const useTags = this.store.cacheEnabled;
213
- let epoch = 0;
214
- const visited = this.visited;
215
- if (useTags) {
216
- epoch = ++this.visitedEpoch;
217
- if (epoch === 0xffffffff) {
218
- this.visitedTag.fill(0);
219
- epoch = this.visitedEpoch = 1;
220
- }
221
- } else {
222
- visited.clear();
223
- }
224
-
225
- for (let k = 0; k < entryPoints.length; k++) {
226
- const ep = entryPoints[k];
227
- if (useTags) {
228
- if (this.seenTag(ep, epoch)) continue;
229
- this.markVisited(ep, epoch);
230
- } else {
231
- if (visited.has(ep)) continue;
232
- visited.add(ep);
233
- }
234
- const rec = this.node(ep);
235
- const d = this.distOf(rec);
236
- cand.push(d, ep);
237
- if (rec.deleted === 0) {
238
- res.push(d, ep);
239
- if (res.size > ef) res.pop();
240
- }
241
- }
242
-
243
- while (cand.size > 0) {
244
- const cd = cand.topKey();
245
- if (res.size >= ef && cd > res.topKey()) break;
246
- const c = cand.topVal();
247
- cand.pop();
248
- const nbrs = this.store.getNeighbors(c, layer);
249
- if (nbrs === null) continue;
250
- // Batch: mark the unvisited neighbours, fetch their codes in one
251
- // storage read, then score them in the original order.
252
- const fresh = this.freshScratch;
253
- fresh.length = 0;
254
- for (let i = 0; i < nbrs.length; i++) {
255
- const e = nbrs[i];
256
- if (useTags) {
257
- if (this.seenTag(e, epoch)) continue;
258
- this.markVisited(e, epoch);
259
- } else {
260
- if (visited.has(e)) continue;
261
- visited.add(e);
262
- }
263
- fresh.push(e);
264
- }
265
- if (fresh.length > 1) this.fetchNodes(fresh);
266
- for (let i = 0; i < fresh.length; i++) {
267
- const e = fresh[i];
268
- const rec = this.node(e);
269
- const d = this.distOf(rec);
270
- const worst = res.size > 0 ? res.topKey() : Infinity;
271
- if (res.size < ef || d < worst) {
272
- cand.push(d, e);
273
- if (rec.deleted === 0) {
274
- res.push(d, e);
275
- if (res.size > ef) res.pop();
276
- }
277
- }
278
- }
279
- }
280
- }
281
-
282
- /** Greedy single-best descent from `fromLayer` down to (but not into) `toLayer`. */
283
- private greedyDescend(
284
- entry: number,
285
- fromLayer: number,
286
- toLayer: number,
287
- ): number {
288
- let cur = entry;
289
- const ep = this.singleEp;
290
- for (let layer = fromLayer; layer > toLayer; layer--) {
291
- ep[0] = cur;
292
- this.searchLayer(ep, 1, layer);
293
- if (this.resHeap.size > 0) cur = this.resHeap.vals[0];
294
- }
295
- return cur;
296
- }
297
-
298
- /**
299
- * SELECT-NEIGHBORS-HEURISTIC (Algorithm 4) on codes. Picks up to `M` diverse
300
- * neighbours from `candIds`, preferring those closer to `base` than to any
301
- * already-chosen neighbour, then fills remaining slots with the closest
302
- * leftovers (keep-pruned connections). Appends to `out`.
303
- */
304
- private selectNeighbors(
305
- base: number,
306
- candIds: number[],
307
- count: number,
308
- M: number,
309
- out: number[],
310
- ): void {
311
- const idx = this.idxScratch;
312
- const ds = this.distScratch;
313
- this.fetchNodes(candIds, count); // one batched read for the misses
314
- idx.length = count;
315
- ds.length = count;
316
- for (let i = 0; i < count; i++) {
317
- idx[i] = i;
318
- ds[i] = this.codeDist(base, candIds[i]);
319
- }
320
- idx.sort((a, b) => ds[a] - ds[b]);
321
- for (let s = 0; s < count; s++) {
322
- if (out.length >= M) break;
323
- const i = idx[s];
324
- const cid = candIds[i];
325
- if (cid === base) continue;
326
- const cd = ds[i];
327
- let keep = true;
328
- for (let j = 0; j < out.length; j++) {
329
- if (this.codeDist(cid, out[j]) < cd) {
330
- keep = false;
331
- break;
332
- }
333
- }
334
- if (keep) out.push(cid);
335
- }
336
- if (out.length < M) {
337
- for (let s = 0; s < count && out.length < M; s++) {
338
- const cid = candIds[idx[s]];
339
- if (cid === base) continue;
340
- let dup = false;
341
- for (let j = 0; j < out.length; j++) {
342
- if (out[j] === cid) {
343
- dup = true;
344
- break;
345
- }
346
- }
347
- if (!dup) out.push(cid);
348
- }
349
- }
350
- }
351
-
352
- /**
353
- * Insert a vector's code under external id `ext`; returns the internal node id.
354
- * All graph reads/writes go through the store; the caller owns the transaction.
355
- */
356
- insert(ext: number, code: Uint8Array, efC?: number): number {
357
- const ef = efC !== undefined && efC >= 1
358
- ? Math.min(efC, this.efConstruction)
359
- : this.efConstruction;
360
- this.working.clear();
361
- const level = this.randomLevel();
362
- const id = this.store.addNode(ext, level, code);
363
- this.total++;
364
- this.working.set(id, { code, deleted: 0, ext });
365
-
366
- if (this.entry === -1) {
367
- this.entry = id;
368
- this.maxLevel = level;
369
- this.live++;
370
- this.persistState();
371
- return id;
372
- }
373
-
374
- this.qCtx = null;
375
- this.refBytes = code;
376
- this.building = true;
377
-
378
- let entry = this.entry;
379
- const topLayer = this.maxLevel;
380
- if (topLayer > level) entry = this.greedyDescend(entry, topLayer, level);
381
-
382
- let entryPoints = [entry];
383
- const startLayer = Math.min(topLayer, level);
384
- for (let layer = startLayer; layer >= 0; layer--) {
385
- this.searchLayer(entryPoints, ef, layer);
386
- const res = this.resHeap;
387
- const wCount = res.size;
388
-
389
- const selected: number[] = [];
390
- this.selectNeighbors(id, res.vals, wCount, this.M, selected);
391
- if (selected.length > 0) this.store.setNeighbors(id, layer, selected);
392
-
393
- const cap = layer === 0 ? this.Mmax0 : this.M;
394
- for (let s = 0; s < selected.length; s++) {
395
- const nb = selected[s];
396
- const cur = this.store.getNeighbors(nb, layer);
397
- const list: number[] = cur ? Array.from(cur) : [];
398
- list.push(id);
399
- if (list.length > cap) {
400
- const pruned: number[] = [];
401
- this.selectNeighbors(nb, list, list.length, cap, pruned);
402
- this.store.setNeighbors(nb, layer, pruned);
403
- } else {
404
- this.store.setNeighbors(nb, layer, list);
405
- }
406
- }
407
-
408
- if (wCount > 0) {
409
- const next = new Array<number>(wCount);
410
- for (let i = 0; i < wCount; i++) next[i] = res.vals[i];
411
- entryPoints = next;
412
- } else {
413
- entryPoints = [entry];
414
- }
415
- }
416
-
417
- this.building = false;
418
- this.refBytes = null;
419
- this.live++;
420
- if (level > this.maxLevel) {
421
- this.maxLevel = level;
422
- this.entry = id;
423
- }
424
- this.persistState();
425
- return id;
426
- }
427
-
428
- /** k-NN with a full-precision query vector (accurate estimator). */
429
- searchKnn(vec: ArrayLike<number>, k: number, ef?: number): KnnHit[] {
430
- this.lastQueryDistComps = 0;
431
- this.store.resetReads();
432
- this.working.clear();
433
- this.building = false;
434
- this.refBytes = null;
435
- if (this.entry === -1 || k <= 0) {
436
- this.lastQueryStorageReads = 0;
437
- return [];
438
- }
439
- this.qCtx = this.quantizer.prepareQuery(vec);
440
- const hits = this.collectKnn(k, Math.max(ef ?? this._efSearch, k));
441
- this.qCtx = null;
442
- this.lastQueryStorageReads = this.store.reads;
443
- return hits;
444
- }
445
-
446
- /** k-NN with an already-quantized code (sign-bit Hamming / angular distance). */
447
- searchKnnByCode(codeBytes: Uint8Array, k: number, ef?: number): KnnHit[] {
448
- this.lastQueryDistComps = 0;
449
- this.store.resetReads();
450
- this.working.clear();
451
- this.building = false;
452
- this.qCtx = null;
453
- if (this.entry === -1 || k <= 0) {
454
- this.lastQueryStorageReads = 0;
455
- return [];
456
- }
457
- this.refBytes = codeBytes;
458
- const hits = this.collectKnn(k, Math.max(ef ?? this._efSearch, k));
459
- this.refBytes = null;
460
- this.lastQueryStorageReads = this.store.reads;
461
- return hits;
462
- }
463
-
464
- private collectKnn(k: number, efs: number): KnnHit[] {
465
- let entry = this.entry;
466
- if (this.maxLevel > 0) entry = this.greedyDescend(entry, this.maxLevel, 0);
467
- this.singleEp[0] = entry;
468
- this.searchLayer(this.singleEp, efs, 0);
469
-
470
- const res = this.resHeap;
471
- const n = res.size;
472
- const hits: KnnHit[] = new Array(n);
473
- for (let i = 0; i < n; i++) {
474
- let d = res.keys[i];
475
- if (d < 0) d = 0;
476
- hits[i] = { id: this.node(res.vals[i]).ext as number, distance: d };
477
- }
478
- hits.sort((a, b) => a.distance - b.distance);
479
- if (hits.length > k) hits.length = k;
480
- return hits;
481
- }
482
-
483
- /** Tombstone a live node by internal id. Caller owns the transaction. */
484
- remove(id: number): boolean {
485
- const rec = this.store.getNode(id);
486
- if (rec === null || rec.deleted === 1) return false;
487
- this.store.tombstone(id);
488
- this.live--;
489
- this.persistState();
490
- return true;
491
- }
492
-
493
- /**
494
- * Reclaim tombstones with a graph-preserving splice (see
495
- * {@link Store.spliceCompact}): live nodes keep their internal ids and their
496
- * wiring; each dead neighbour is replaced by its own live neighbours. Codes
497
- * are untouched, so the index loses nothing beyond what deletion itself
498
- * removes. The previous implementation replayed every live code through a
499
- * full HNSW insert — O(live · ef · log N) storage reads, HOURS on a trained
500
- * multi-million-node store, inside one WAL-bloating transaction. The splice
501
- * is a streaming pass with batched commits, then a VACUUM to return the
502
- * freed pages.
503
- */
504
- compact(): void {
505
- const r = this.store.spliceCompact(this.M, this.Mmax0, this.entry);
506
- this.entry = r.entry;
507
- this.maxLevel = r.maxLevel;
508
- this.live = r.live;
509
- this.total = r.live;
510
- this.persistState();
511
- this.working.clear();
512
- this.store.vacuum();
513
- }
514
- }