@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,202 +0,0 @@
1
- /** External ids are integers only. */
2
- export type ExternalId = number;
3
- export interface DatabaseOptions {
4
- /** Path to the SQLite database file (use ":memory:" for a transient store). */
5
- dbPath: string;
6
- /** Vector dimensionality. Required for a new database; ignored when reopening. */
7
- dim?: number;
8
- /** Max neighbours per node on layers >= 1 (layer 0 uses 2*M). Default 16. */
9
- M?: number;
10
- /** Candidate list size during construction. Default 200. */
11
- efConstruction?: number;
12
- /** Candidate list size during search. Default 100. Tunable at runtime. */
13
- efSearch?: number;
14
- /** Bits used to scalar-quantise the query for the RaBitQ estimator.
15
- * Query-side only (stored codes and the graph are independent of it), so it
16
- * is tunable at reopen, like efSearch. Default 8: 4 bits cannot resolve a
17
- * tight cluster's residual and costs ~18% self-recall there (test 2a); 8
18
- * bits restores it for the price of a Uint16 per-query LUT. */
19
- queryBits?: number;
20
- /** Number of sign-flip + Hadamard rounds in the random rotation. Default 3. */
21
- rotationRounds?: number;
22
- /** Seed for the rotation and graph levels. Default fixed. */
23
- seed?: number;
24
- /** Optional centroid the vectors are centered by before quantisation. */
25
- centroid?: ArrayLike<number>;
26
- /**
27
- * RAM cache size in MiB -- the single memory knob. It sizes SQLite's page
28
- * cache AND the immutable-code LRU (whose entry-capacity is derived from this
29
- * budget and the code size — there is no second knob). Both are purely speed
30
- * enhancements: correctness and the per-operation storage-read count are
31
- * identical with it off. Pass 0 to run with essentially no cache. Default 64.
32
- * Not persisted; set per open.
33
- */
34
- cacheSizeMb?: number;
35
- }
36
- export interface QueryResult {
37
- id: ExternalId;
38
- /** Estimated cosine distance (1 - cosine). */
39
- distance: number;
40
- }
41
- export interface StorageStats {
42
- /** Bytes a single Float32 copy of the vector would take. */
43
- float32BytesPerVector: number;
44
- /** Bytes of 1-bit code kept per vector (on disk). */
45
- codeBytesPerVector: number;
46
- /** Bytes kept per vector (just the code; excludes the graph adjacency). */
47
- bytesPerVector: number;
48
- /** float32BytesPerVector / bytesPerVector. */
49
- compressionRatio: number;
50
- }
51
- /**
52
- * A persistent vector database: an HNSW graph over 1-bit RaBitQ codes (cosine),
53
- * stored entirely in SQLite at `dbPath`. The graph IS the database -- there is no
54
- * load/save step and no in-RAM copy of the data, so resident memory stays flat as
55
- * the collection grows and the store survives process restarts. Reopening the
56
- * same path restores the exact configuration (the rotation is regenerated from
57
- * the persisted seed), so the codes already on disk remain valid.
58
- *
59
- * External ids are integers. The original float vectors are never retained --
60
- * only the sign codes -- so a 256-d vector costs 32 bytes instead of 1024. `get`
61
- * returns the stored code, which can be fed straight back into
62
- * `insert`/`update`/`query`.
63
- */
64
- export declare class VectorDatabase {
65
- readonly dim: number;
66
- private readonly store;
67
- private readonly quantizer;
68
- private readonly index;
69
- private readonly codeWords;
70
- constructor(options: DatabaseOptions);
71
- /** Number of live (non-deleted) vectors. */
72
- get size(): number;
73
- /**
74
- * Physical node count including tombstones from deletes/updates. When it grows
75
- * well beyond `size`, call `compact()` to reclaim the space on disk.
76
- */
77
- get physicalSize(): number;
78
- get efSearch(): number;
79
- set efSearch(value: number);
80
- /** Distance computations performed during the most recent query. */
81
- get lastQueryDistanceComputations(): number;
82
- /**
83
- * Storage row reads issued by the most recent query. This is the honest,
84
- * cache-independent scalability metric: it counts every node/neighbour fetch
85
- * that hit the database, so disabling the cache cannot hide a bad access pattern.
86
- */
87
- get lastQueryStorageReads(): number;
88
- /** Per-vector storage cost of the index versus a Float32 baseline. */
89
- get storage(): StorageStats;
90
- has(id: ExternalId): boolean;
91
- /** Stream every live external id (bounded memory). */
92
- keys(): IterableIterator<ExternalId>;
93
- /**
94
- * Stream live entries whose INTERNAL id is > `after`, as
95
- * {ext, internal} pairs in internal-id order. Internal ids are assigned
96
- * monotonically at insert and preserved by {@link compact}, so the largest
97
- * internal id a caller has seen is a durable incremental watermark: a later
98
- * call with it yields exactly the entries added since.
99
- */
100
- keysSince(after: number): IterableIterator<{
101
- ext: ExternalId;
102
- internal: number;
103
- }>;
104
- private checkId;
105
- /**
106
- * Convert a value to code bytes, selecting by length:
107
- * - `codeWords` elements -> an existing 1-bit code (e.g. from `get()`)
108
- * - otherwise -> a raw `dim`-vector, encoded first
109
- * `codeWords < dim` for any dim >= 2, so the two never collide.
110
- */
111
- private toCodeBytes;
112
- /**
113
- * Create. Accepts a raw `dim`-vector or a 1-bit code (`codeWords` words),
114
- * detected by length. Throws if the id already exists (use `update`/`upsert`).
115
- */
116
- insert(id: ExternalId, value: ArrayLike<number>): void;
117
- /** Insert with the caller owning the transaction (used by {@link upsertMany}).
118
- * `ef` optionally narrows the construction beam for this one vector (a
119
- * caller-declared cheap entry, e.g. a reach-only interior); omitted means
120
- * the index's configured efConstruction. */
121
- private insertCore;
122
- upsert(id: ExternalId, value: ArrayLike<number>): void;
123
- /**
124
- * Upsert many vectors under ONE transaction. The HNSW build touches the store
125
- * on every wired edge, so a transaction per vector is one WAL commit per vector
126
- * — which dominates a bulk load on disk. Wrapping the whole batch in a single
127
- * transaction coalesces those commits into one while leaving the graph and its
128
- * result identical (reads on the connection still see the uncommitted rows).
129
- *
130
- * This is purely about commit batching; it changes nothing about the storage
131
- * model. Codes still live only in SQLite and are read on demand — the
132
- * cache-independent per-operation read count is unchanged — so a batched load
133
- * scales exactly as the per-item path does, just with far fewer fsyncs. A throw
134
- * rolls the whole batch back, so the caller treats it as the per-item path on
135
- * failure.
136
- */
137
- upsertMany(
138
- entries: Array<{
139
- id: ExternalId;
140
- vector: ArrayLike<number>;
141
- ef?: number;
142
- }>,
143
- ): void;
144
- /**
145
- * Read the stored 1-bit code for an id (a copy as a Uint32Array), or null. The
146
- * original vector is not retained; the code round-trips into insert/update/query.
147
- */
148
- get(id: ExternalId): Uint32Array | null;
149
- /**
150
- * Update the vector bound to an id (raw vector or code, detected by length).
151
- * The previous node is tombstoned and a fresh one inserted. Throws if absent.
152
- */
153
- update(id: ExternalId, value: ArrayLike<number>): void;
154
- /** Update with the caller owning the transaction (used by {@link upsertMany}). */
155
- private updateCore;
156
- /** Update when the live internal node id is already known.
157
- *
158
- * Skips the write entirely when the new code equals the stored one — the
159
- * update would tombstone the node and replay a full graph insert for a
160
- * byte-identical result. This is not a corner case: content-addressed
161
- * callers re-upsert unchanged vectors wholesale after a restart (the same
162
- * content always encodes to the same code), and each such no-op otherwise
163
- * costs a tombstone (permanent routing/disk overhead until compaction)
164
- * plus an O(ef·log N) reinsert. */
165
- private updateAt;
166
- /**
167
- * Delete many ids under ONE transaction — same commit-coalescing rationale
168
- * as {@link upsertMany}: a tombstone per implicit transaction is one WAL
169
- * commit per id, which dominates a bulk prune. Absent ids are skipped.
170
- * Returns the number of vectors actually removed.
171
- */
172
- deleteMany(ids: ExternalId[]): number;
173
- /** Delete the vector bound to an id. Returns false if absent. */
174
- delete(id: ExternalId): boolean;
175
- /**
176
- * Pre-fill the RAM caches (codes + neighbour lists) with sequential table
177
- * scans, up to their budget-derived caps. Optional and purely a latency
178
- * optimisation: a cold session otherwise pays the same warming through
179
- * random point reads over its first minutes. Call once after open on a
180
- * session that will do sustained inserts/queries. Returns rows warmed;
181
- * a 0-budget database returns 0 immediately.
182
- */
183
- warmCache(): number;
184
- /**
185
- * Rebuild the graph from the live codes only, dropping tombstones and returning
186
- * the freed pages to the filesystem. Lossless -- equivalent to the original
187
- * build -- and needs no original vectors.
188
- */
189
- compact(): void;
190
- /**
191
- * k-NN search. The argument's length selects the mode:
192
- * - `dim` elements -> a raw vector (accurate 4-bit-query estimator)
193
- * - `codeWords` elements -> a 1-bit code (e.g. `get(id)`), by sign-bit Hamming
194
- * Detection is by length, so a code is recognised whether it is a Uint32Array
195
- * or a plain number[]. `codeWords < dim` for any dim >= 2, so they never collide.
196
- */
197
- query(query: ArrayLike<number>, k?: number, opts?: {
198
- ef?: number;
199
- }): QueryResult[];
200
- /** Close the underlying database. The instance must not be used afterwards. */
201
- close(): void;
202
- }
@@ -1,405 +0,0 @@
1
- import { HnswIndex } from "./hnsw.js";
2
- import { RaBitQuantizer } from "./rabitq.js";
3
- import { Store } from "./store.js";
4
- const DEFAULTS = {
5
- M: 16,
6
- efConstruction: 200,
7
- efSearch: 100,
8
- queryBits: 8,
9
- rotationRounds: 3,
10
- seed: 0x1234abcd,
11
- };
12
- /**
13
- * A persistent vector database: an HNSW graph over 1-bit RaBitQ codes (cosine),
14
- * stored entirely in SQLite at `dbPath`. The graph IS the database -- there is no
15
- * load/save step and no in-RAM copy of the data, so resident memory stays flat as
16
- * the collection grows and the store survives process restarts. Reopening the
17
- * same path restores the exact configuration (the rotation is regenerated from
18
- * the persisted seed), so the codes already on disk remain valid.
19
- *
20
- * External ids are integers. The original float vectors are never retained --
21
- * only the sign codes -- so a 256-d vector costs 32 bytes instead of 1024. `get`
22
- * returns the stored code, which can be fed straight back into
23
- * `insert`/`update`/`query`.
24
- */
25
- export class VectorDatabase {
26
- dim;
27
- store;
28
- quantizer;
29
- index;
30
- codeWords;
31
- constructor(options) {
32
- if (
33
- !options || typeof options.dbPath !== "string" ||
34
- options.dbPath.length === 0
35
- ) {
36
- throw new Error("DatabaseOptions.dbPath (string) is required");
37
- }
38
- this.store = new Store(options.dbPath, options.cacheSizeMb ?? 64);
39
- let cfg = this.store.loadConfig();
40
- if (cfg === null) {
41
- // Fresh database: derive the configuration from the options and persist it.
42
- if (!Number.isInteger(options.dim) || options.dim <= 0) {
43
- throw new Error(
44
- "DatabaseOptions.dim must be a positive integer for a new database",
45
- );
46
- }
47
- const dim = options.dim;
48
- const centroid = options.centroid
49
- ? Float64Array.from(options.centroid)
50
- : null;
51
- const probe = new RaBitQuantizer(dim, { rounds: 1, seed: 0 });
52
- cfg = {
53
- dim,
54
- m: options.M ?? DEFAULTS.M,
55
- efConstruction: options.efConstruction ?? DEFAULTS.efConstruction,
56
- efSearch: options.efSearch ?? DEFAULTS.efSearch,
57
- queryBits: options.queryBits ?? DEFAULTS.queryBits,
58
- rotationRounds: options.rotationRounds ?? DEFAULTS.rotationRounds,
59
- seed: (options.seed ?? DEFAULTS.seed) >>> 0,
60
- centroid,
61
- codeWords: probe.codeWords,
62
- paddedDim: probe.paddedDim,
63
- };
64
- this.store.initConfig(cfg);
65
- } else {
66
- // Reopened: keep the stored STRUCTURAL config, but allow tuning the two
67
- // query-side knobs — efSearch and queryBits shape only how a query is
68
- // executed, never what is stored, so honouring an explicit option here
69
- // is safe and lets an existing database adopt better query settings.
70
- if (options.efSearch !== undefined) {
71
- cfg.efSearch = options.efSearch;
72
- this.store.setEfSearch(options.efSearch);
73
- }
74
- if (
75
- options.queryBits !== undefined && options.queryBits !== cfg.queryBits
76
- ) {
77
- cfg.queryBits = options.queryBits;
78
- this.store.setQueryBits(options.queryBits);
79
- }
80
- }
81
- this.dim = cfg.dim;
82
- this.quantizer = new RaBitQuantizer(cfg.dim, {
83
- queryBits: cfg.queryBits,
84
- rounds: cfg.rotationRounds,
85
- seed: cfg.seed,
86
- centroid: cfg.centroid ?? undefined,
87
- });
88
- if (this.quantizer.codeWords !== cfg.codeWords) {
89
- throw new Error(
90
- "Stored code geometry does not match the quantizer (corrupt database?)",
91
- );
92
- }
93
- this.codeWords = this.quantizer.codeWords;
94
- this.index = new HnswIndex(this.quantizer, this.store, {
95
- M: cfg.m,
96
- efConstruction: cfg.efConstruction,
97
- efSearch: cfg.efSearch,
98
- seed: cfg.seed,
99
- });
100
- }
101
- /** Number of live (non-deleted) vectors. */
102
- get size() {
103
- return this.index.size;
104
- }
105
- /**
106
- * Physical node count including tombstones from deletes/updates. When it grows
107
- * well beyond `size`, call `compact()` to reclaim the space on disk.
108
- */
109
- get physicalSize() {
110
- return this.index.physicalSize;
111
- }
112
- get efSearch() {
113
- return this.index.efSearch;
114
- }
115
- set efSearch(value) {
116
- this.index.efSearch = value;
117
- }
118
- /** Distance computations performed during the most recent query. */
119
- get lastQueryDistanceComputations() {
120
- return this.index.lastQueryDistComps;
121
- }
122
- /**
123
- * Storage row reads issued by the most recent query. This is the honest,
124
- * cache-independent scalability metric: it counts every node/neighbour fetch
125
- * that hit the database, so disabling the cache cannot hide a bad access pattern.
126
- */
127
- get lastQueryStorageReads() {
128
- return this.index.lastQueryStorageReads;
129
- }
130
- /** Per-vector storage cost of the index versus a Float32 baseline. */
131
- get storage() {
132
- const f32 = this.dim * 4;
133
- const bpv = this.index.bytesPerVector;
134
- return {
135
- float32BytesPerVector: f32,
136
- codeBytesPerVector: bpv,
137
- bytesPerVector: bpv,
138
- compressionRatio: f32 / bpv,
139
- };
140
- }
141
- has(id) {
142
- return this.store.idByExt(this.checkId(id)) !== null;
143
- }
144
- /** Stream every live external id (bounded memory). */
145
- *keys() {
146
- yield* this.store.liveExts();
147
- }
148
- /**
149
- * Stream live entries whose INTERNAL id is > `after`, as
150
- * {ext, internal} pairs in internal-id order. Internal ids are assigned
151
- * monotonically at insert and preserved by {@link compact}, so the largest
152
- * internal id a caller has seen is a durable incremental watermark: a later
153
- * call with it yields exactly the entries added since.
154
- */
155
- *keysSince(after) {
156
- yield* this.store.liveExtsSince(after);
157
- }
158
- checkId(id) {
159
- if (!Number.isInteger(id)) {
160
- throw new Error(`External id must be an integer, got ${id}`);
161
- }
162
- return id;
163
- }
164
- /**
165
- * Convert a value to code bytes, selecting by length:
166
- * - `codeWords` elements -> an existing 1-bit code (e.g. from `get()`)
167
- * - otherwise -> a raw `dim`-vector, encoded first
168
- * `codeWords < dim` for any dim >= 2, so the two never collide.
169
- */
170
- toCodeBytes(value) {
171
- if (value.length === this.codeWords) {
172
- return this.quantizer.codeToBytes(value);
173
- }
174
- if (value.length !== this.dim) {
175
- throw new Error(
176
- `Vector dimension mismatch: expected ${this.dim}, got ${value.length}`,
177
- );
178
- }
179
- return this.quantizer.codeToBytes(this.quantizer.encode(value));
180
- }
181
- // ------------------------------- CRUD ------------------------------------
182
- /**
183
- * Create. Accepts a raw `dim`-vector or a 1-bit code (`codeWords` words),
184
- * detected by length. Throws if the id already exists (use `update`/`upsert`).
185
- */
186
- insert(id, value) {
187
- this.store.begin();
188
- try {
189
- this.insertCore(id, value);
190
- this.store.commit();
191
- } catch (e) {
192
- this.store.rollback();
193
- throw e;
194
- }
195
- }
196
- /** Insert with the caller owning the transaction (used by {@link upsertMany}).
197
- * `ef` optionally narrows the construction beam for this one vector (a
198
- * caller-declared cheap entry, e.g. a reach-only interior); omitted means
199
- * the index's configured efConstruction. */
200
- insertCore(id, value, ef) {
201
- this.checkId(id);
202
- if (this.store.idByExt(id) !== null) {
203
- throw new Error(`External id already exists: ${id} (use update())`);
204
- }
205
- this.index.insert(id, this.toCodeBytes(value), ef);
206
- }
207
- upsert(id, value) {
208
- const nodeId = this.store.idByExt(this.checkId(id));
209
- if (nodeId === null) {
210
- this.insert(id, value);
211
- return;
212
- }
213
- this.store.begin();
214
- try {
215
- this.updateAt(nodeId, id, value);
216
- this.store.commit();
217
- } catch (e) {
218
- this.store.rollback();
219
- throw e;
220
- }
221
- }
222
- /**
223
- * Upsert many vectors under ONE transaction. The HNSW build touches the store
224
- * on every wired edge, so a transaction per vector is one WAL commit per vector
225
- * — which dominates a bulk load on disk. Wrapping the whole batch in a single
226
- * transaction coalesces those commits into one while leaving the graph and its
227
- * result identical (reads on the connection still see the uncommitted rows).
228
- *
229
- * This is purely about commit batching; it changes nothing about the storage
230
- * model. Codes still live only in SQLite and are read on demand — the
231
- * cache-independent per-operation read count is unchanged — so a batched load
232
- * scales exactly as the per-item path does, just with far fewer fsyncs. A throw
233
- * rolls the whole batch back, so the caller treats it as the per-item path on
234
- * failure.
235
- */
236
- upsertMany(entries) {
237
- if (entries.length === 0) {
238
- return;
239
- }
240
- this.store.begin();
241
- try {
242
- for (const e of entries) {
243
- const nodeId = this.store.idByExt(this.checkId(e.id));
244
- if (nodeId !== null) {
245
- this.updateAt(nodeId, e.id, e.vector, e.ef);
246
- } else {
247
- this.insertCore(e.id, e.vector, e.ef);
248
- }
249
- }
250
- this.store.commit();
251
- } catch (e) {
252
- this.store.rollback();
253
- throw e;
254
- }
255
- }
256
- /**
257
- * Read the stored 1-bit code for an id (a copy as a Uint32Array), or null. The
258
- * original vector is not retained; the code round-trips into insert/update/query.
259
- */
260
- get(id) {
261
- const nodeId = this.store.idByExt(this.checkId(id));
262
- if (nodeId === null) {
263
- return null;
264
- }
265
- const rec = this.store.getNode(nodeId);
266
- return rec ? this.quantizer.bytesToCode(rec.code) : null;
267
- }
268
- /**
269
- * Update the vector bound to an id (raw vector or code, detected by length).
270
- * The previous node is tombstoned and a fresh one inserted. Throws if absent.
271
- */
272
- update(id, value) {
273
- this.store.begin();
274
- try {
275
- this.updateCore(id, value);
276
- this.store.commit();
277
- } catch (e) {
278
- this.store.rollback();
279
- throw e;
280
- }
281
- }
282
- /** Update with the caller owning the transaction (used by {@link upsertMany}). */
283
- updateCore(id, value) {
284
- const oldId = this.store.idByExt(this.checkId(id));
285
- if (oldId === null) {
286
- throw new Error(`Unknown external id: ${id}`);
287
- }
288
- this.updateAt(oldId, id, value);
289
- }
290
- /** Update when the live internal node id is already known.
291
- *
292
- * Skips the write entirely when the new code equals the stored one — the
293
- * update would tombstone the node and replay a full graph insert for a
294
- * byte-identical result. This is not a corner case: content-addressed
295
- * callers re-upsert unchanged vectors wholesale after a restart (the same
296
- * content always encodes to the same code), and each such no-op otherwise
297
- * costs a tombstone (permanent routing/disk overhead until compaction)
298
- * plus an O(ef·log N) reinsert. */
299
- updateAt(nodeId, ext, value, ef) {
300
- const bytes = this.toCodeBytes(value);
301
- const rec = this.store.getNode(nodeId);
302
- if (rec !== null && rec.code.length === bytes.length) {
303
- let same = true;
304
- for (let i = 0; i < bytes.length; i++) {
305
- if (rec.code[i] !== bytes[i]) {
306
- same = false;
307
- break;
308
- }
309
- }
310
- if (same) {
311
- return; // identical code — the update is a no-op
312
- }
313
- }
314
- this.index.remove(nodeId); // frees the ext id, then re-insert under it
315
- this.index.insert(ext, bytes, ef);
316
- }
317
- /**
318
- * Delete many ids under ONE transaction — same commit-coalescing rationale
319
- * as {@link upsertMany}: a tombstone per implicit transaction is one WAL
320
- * commit per id, which dominates a bulk prune. Absent ids are skipped.
321
- * Returns the number of vectors actually removed.
322
- */
323
- deleteMany(ids) {
324
- if (ids.length === 0) {
325
- return 0;
326
- }
327
- let removed = 0;
328
- this.store.begin();
329
- try {
330
- for (const id of ids) {
331
- const nodeId = this.store.idByExt(this.checkId(id));
332
- if (nodeId !== null && this.index.remove(nodeId)) {
333
- removed++;
334
- }
335
- }
336
- this.store.commit();
337
- } catch (e) {
338
- this.store.rollback();
339
- throw e;
340
- }
341
- return removed;
342
- }
343
- /** Delete the vector bound to an id. Returns false if absent. */
344
- delete(id) {
345
- const nodeId = this.store.idByExt(this.checkId(id));
346
- if (nodeId === null) {
347
- return false;
348
- }
349
- this.store.begin();
350
- try {
351
- this.index.remove(nodeId);
352
- this.store.commit();
353
- } catch (e) {
354
- this.store.rollback();
355
- throw e;
356
- }
357
- return true;
358
- }
359
- /**
360
- * Pre-fill the RAM caches (codes + neighbour lists) with sequential table
361
- * scans, up to their budget-derived caps. Optional and purely a latency
362
- * optimisation: a cold session otherwise pays the same warming through
363
- * random point reads over its first minutes. Call once after open on a
364
- * session that will do sustained inserts/queries. Returns rows warmed;
365
- * a 0-budget database returns 0 immediately.
366
- */
367
- warmCache() {
368
- return this.store.warmCache();
369
- }
370
- /**
371
- * Rebuild the graph from the live codes only, dropping tombstones and returning
372
- * the freed pages to the filesystem. Lossless -- equivalent to the original
373
- * build -- and needs no original vectors.
374
- */
375
- compact() {
376
- this.index.compact();
377
- }
378
- // ------------------------------ search -----------------------------------
379
- /**
380
- * k-NN search. The argument's length selects the mode:
381
- * - `dim` elements -> a raw vector (accurate 4-bit-query estimator)
382
- * - `codeWords` elements -> a 1-bit code (e.g. `get(id)`), by sign-bit Hamming
383
- * Detection is by length, so a code is recognised whether it is a Uint32Array
384
- * or a plain number[]. `codeWords < dim` for any dim >= 2, so they never collide.
385
- */
386
- query(query, k = 10, opts) {
387
- if (query.length === this.codeWords) {
388
- return this.index.searchKnnByCode(
389
- this.quantizer.codeToBytes(query),
390
- k,
391
- opts?.ef,
392
- );
393
- }
394
- if (query.length !== this.dim) {
395
- throw new Error(
396
- `Vector dimension mismatch: expected ${this.dim}, got ${query.length}`,
397
- );
398
- }
399
- return this.index.searchKnn(query, k, opts?.ef);
400
- }
401
- /** Close the underlying database. The instance must not be used afterwards. */
402
- close() {
403
- this.store.close();
404
- }
405
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * Binary heap over two parallel numeric arrays storing (key, value) pairs.
3
- *
4
- * When `minHeap` is true the smallest key sits at the root, otherwise the
5
- * largest. Keys are distances, values are integer node ids. Parallel plain
6
- * arrays are used (rather than an array of objects) to avoid per-element
7
- * allocation in the search hot path.
8
- */
9
- export declare class Heap {
10
- readonly keys: number[];
11
- readonly vals: number[];
12
- private readonly minHeap;
13
- constructor(minHeap: boolean);
14
- get size(): number;
15
- clear(): void;
16
- topKey(): number;
17
- topVal(): number;
18
- /** true if `a` belongs closer to the root than `b`. */
19
- private higher;
20
- push(key: number, val: number): void;
21
- pop(): void;
22
- }