akm-cli 0.9.15 → 0.9.16-alpha.1
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/CHANGELOG.md +144 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/cli/retired-commands.js +2 -0
- package/dist/cli/unknown-flags.js +36 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +6 -4
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/proposal/repository.js +12 -3
- package/dist/commands/read/curate.js +34 -44
- package/dist/commands/read/search.js +50 -2
- package/dist/commands/sources/index-status.js +99 -0
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +33 -12
- package/dist/commands/sources/source-add.js +21 -6
- package/dist/commands/sources/stash-cli.js +119 -111
- package/dist/core/adapter/adapters/akm-adapter.js +35 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -1
- package/dist/core/asset/asset-placement.js +35 -0
- package/dist/core/config/schema/embedding.js +7 -30
- package/dist/core/config/schema/search.js +11 -9
- package/dist/core/errors.js +5 -2
- package/dist/core/hash.js +18 -0
- package/dist/core/maintenance-barrier.js +8 -6
- package/dist/core/paths.js +0 -11
- package/dist/core/run-lock.js +5 -2
- package/dist/core/state/migrations.js +26 -1
- package/dist/core/state-db.js +63 -27
- package/dist/indexer/drain.js +306 -0
- package/dist/indexer/embedding-identity.js +20 -0
- package/dist/indexer/enrich.js +260 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/index-written-assets.js +133 -171
- package/dist/indexer/indexer.js +458 -1621
- package/dist/indexer/lookup/adapter-concept-owner.js +19 -5
- package/dist/indexer/passes/metadata.js +18 -1
- package/dist/indexer/reconcile.js +890 -0
- package/dist/indexer/scan/drain-dir.js +27 -70
- package/dist/indexer/scan/parse-file.js +66 -0
- package/dist/indexer/search/db-search.js +373 -89
- package/dist/indexer/search/ranking-contributors.js +21 -16
- package/dist/indexer/search/ranking.js +135 -57
- package/dist/indexer/units/unit.js +159 -0
- package/dist/llm/client.js +10 -1
- package/dist/llm/embedder.js +10 -3
- package/dist/llm/embedders/provider-limits.js +288 -0
- package/dist/llm/embedders/remote.js +133 -104
- package/dist/llm/feature-gate.js +4 -2
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +19 -13
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +5 -2
- package/dist/scripts/akm-migrate-node.js +1141 -1237
- package/dist/scripts/akm-migrate.js +1141 -1237
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/steps/connection.js +3 -2
- package/dist/storage/repositories/files-repository.js +181 -0
- package/dist/storage/repositories/index-connection.js +1 -3
- package/dist/storage/repositories/index-entries-repository.js +77 -68
- package/dist/storage/repositories/index-entry-schema.js +16 -25
- package/dist/storage/repositories/index-fts-repository.js +29 -263
- package/dist/storage/repositories/index-meta-repository.js +0 -29
- package/dist/storage/repositories/index-schema.js +115 -122
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +21 -334
- package/dist/storage/repositories/units-repository.js +510 -0
- package/docs/migration/release-notes/0.9.15.md +34 -36
- package/docs/migration/release-notes/0.9.16.md +110 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +93 -87
- package/docs/reference/configuration.md +128 -89
- package/docs/reference/data-and-telemetry.md +2 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +2 -58
- package/dist/indexer/index-db-contention.js +0 -56
- package/dist/indexer/index-rebuild-lock.js +0 -73
- package/dist/indexer/materialize-embeddings.js +0 -771
- package/dist/indexer/passes/dir-staleness.js +0 -161
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
|
@@ -2,24 +2,37 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* `index.db`
|
|
5
|
+
* `index.db` sqlite-vec extension load/availability probe.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* table,
|
|
7
|
+
* This module used to also own the legacy per-entry vector store (a BLOB
|
|
8
|
+
* `embeddings` table, an `entries_vec` vec0 mirror, and a JS-cosine fallback
|
|
9
|
+
* over the BLOB rows) — all of it dead code once the write-time indexing
|
|
10
|
+
* pipeline moved to the content-addressed `units`/`units_vec` store
|
|
11
|
+
* (`units-repository.ts`, docs/plans/index-fragment-vectors.md) and nothing
|
|
12
|
+
* wrote to `embeddings`/`entries_vec` any more. The tables themselves, and
|
|
13
|
+
* every function whose only purpose was reading or writing them, were
|
|
14
|
+
* deleted in the index-redesign's final cleanup (B5h) — see
|
|
15
|
+
* `docs/architecture/internals/storage-locations.md` for what replaced them.
|
|
16
|
+
* What remains here — loading the extension and reporting whether it loaded
|
|
17
|
+
* — is still shared by both the units store and every other vec0 consumer.
|
|
9
18
|
*/
|
|
10
19
|
import { createRequire } from "node:module";
|
|
11
|
-
import { bestEffort } from "../../core/best-effort.js";
|
|
12
|
-
import { warn } from "../../core/warn.js";
|
|
13
|
-
import { cosineSimilarity } from "../../llm/embedders/types.js";
|
|
14
|
-
import { getMeta, setMeta } from "./index-meta-repository.js";
|
|
15
|
-
import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
|
|
16
20
|
// ── sqlite-vec extension ────────────────────────────────────────────────────
|
|
17
21
|
const vecStatus = new WeakMap();
|
|
22
|
+
let forceVecUnavailableForTests = false;
|
|
23
|
+
/** TEST-ONLY. Force `loadVecExtension` to record unavailable without touching the package; pass false to restore. */
|
|
24
|
+
export function _setVecUnavailableForTests(unavailable) {
|
|
25
|
+
forceVecUnavailableForTests = unavailable;
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Attempt to load the sqlite-vec extension into `db`, recording availability.
|
|
20
29
|
* Exported so the connection lifecycle can arm it at open time.
|
|
21
30
|
*/
|
|
22
31
|
export function loadVecExtension(db) {
|
|
32
|
+
if (forceVecUnavailableForTests) {
|
|
33
|
+
vecStatus.set(db, false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
23
36
|
try {
|
|
24
37
|
const esmRequire = createRequire(import.meta.url);
|
|
25
38
|
const sqliteVec = esmRequire("sqlite-vec");
|
|
@@ -37,329 +50,3 @@ export function loadVecExtension(db) {
|
|
|
37
50
|
export function isVecAvailable(db) {
|
|
38
51
|
return vecStatus.get(db) ?? false;
|
|
39
52
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Meta key persisting whether the sqlite-vec fast-path table (`entries_vec`) is
|
|
42
|
-
* fully populated and trustworthy for this index. Set to "0" by the embedding
|
|
43
|
-
* phase when one or more vec inserts FAILED (e.g. a vec0 dimension mismatch)
|
|
44
|
-
* while their BLOB rows still wrote — so semantic search reads the complete
|
|
45
|
-
* BLOB table via the JS-cosine fallback instead of a partial/mismatched vec
|
|
46
|
-
* table. Absent (legacy indexes) and "1" both mean the fast path is trusted.
|
|
47
|
-
*/
|
|
48
|
-
const VEC_FAST_PATH_READY_META = "vecFastPathReady";
|
|
49
|
-
/** Persist whether the sqlite-vec fast path is trustworthy (see the meta doc). */
|
|
50
|
-
export function setVecFastPathReady(db, ready) {
|
|
51
|
-
setMeta(db, VEC_FAST_PATH_READY_META, ready ? "1" : "0");
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* True unless the embedding phase recorded a vec insert failure. Reflects the
|
|
55
|
-
* ACTUAL insert outcomes recorded at index time — not an inference from how many
|
|
56
|
-
* BLOB rows exist — so a degraded vec table routes search to the JS fallback
|
|
57
|
-
* rather than silently returning partial fast-path results.
|
|
58
|
-
*/
|
|
59
|
-
export function isVecFastPathReady(db) {
|
|
60
|
-
if (getMeta(db, VEC_FAST_PATH_READY_META) === "0")
|
|
61
|
-
return false;
|
|
62
|
-
// The meta flag alone is not sufficient. An index built while sqlite-vec was
|
|
63
|
-
// unavailable wrote only BLOB rows, and because "unavailable" outcomes were
|
|
64
|
-
// not counted as failures the flag was still set to "1" against a table that
|
|
65
|
-
// is empty or absent. If sqlite-vec later becomes loadable — the user installs
|
|
66
|
-
// it, or the same index is opened under the other runtime — the fast path
|
|
67
|
-
// would then be trusted and return zero neighbours while the BLOB table holds
|
|
68
|
-
// every embedding. Indexes written by earlier versions still carry that stale
|
|
69
|
-
// flag, so the read path has to verify the table really exists.
|
|
70
|
-
return hasVecTable(db);
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Verify that the vec fast-path table mirrors the complete durable BLOB set.
|
|
74
|
-
*
|
|
75
|
-
* A targeted embedding write preserves the prior readiness decision because
|
|
76
|
-
* its subset cannot prove an older degraded generation is healed. Global
|
|
77
|
-
* materialization uses this aggregate check before promoting the persisted
|
|
78
|
-
* flag; search itself still reads the cheap flag and does not repeat the check
|
|
79
|
-
* per query.
|
|
80
|
-
*/
|
|
81
|
-
export function isVecFastPathComplete(db) {
|
|
82
|
-
if (!isVecAvailable(db) || !hasVecTable(db))
|
|
83
|
-
return false;
|
|
84
|
-
try {
|
|
85
|
-
const missingVecRows = db
|
|
86
|
-
.prepare(`
|
|
87
|
-
SELECT id FROM embeddings
|
|
88
|
-
EXCEPT
|
|
89
|
-
SELECT id FROM entries_vec
|
|
90
|
-
LIMIT 1
|
|
91
|
-
`)
|
|
92
|
-
.all();
|
|
93
|
-
if (missingVecRows.length > 0)
|
|
94
|
-
return false;
|
|
95
|
-
const orphanVecRows = db
|
|
96
|
-
.prepare(`
|
|
97
|
-
SELECT id FROM entries_vec
|
|
98
|
-
EXCEPT
|
|
99
|
-
SELECT id FROM embeddings
|
|
100
|
-
LIMIT 1
|
|
101
|
-
`)
|
|
102
|
-
.all();
|
|
103
|
-
return orphanVecRows.length === 0;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const vecTablePresent = new WeakMap();
|
|
110
|
-
/**
|
|
111
|
-
* Whether `entries_vec` exists on this connection, memoized per handle.
|
|
112
|
-
*
|
|
113
|
-
* openExistingDatabase loads the vec extension but deliberately does not run
|
|
114
|
-
* ensureSchema, so the table is not created on read paths — its absence is a
|
|
115
|
-
* normal state, not an error.
|
|
116
|
-
*/
|
|
117
|
-
function hasVecTable(db) {
|
|
118
|
-
// Only a POSITIVE result is memoized. The table cannot vanish from a live
|
|
119
|
-
// connection, but it CAN appear — ensureSchema creates it partway through an
|
|
120
|
-
// index run — so caching "absent" would pin a stale answer for the rest of
|
|
121
|
-
// the handle's life.
|
|
122
|
-
if (vecTablePresent.get(db) === true)
|
|
123
|
-
return true;
|
|
124
|
-
let present = false;
|
|
125
|
-
try {
|
|
126
|
-
present =
|
|
127
|
-
db.prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = 'entries_vec'").get() !==
|
|
128
|
-
undefined;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
present = false;
|
|
132
|
-
}
|
|
133
|
-
if (present)
|
|
134
|
-
vecTablePresent.set(db, true);
|
|
135
|
-
return present;
|
|
136
|
-
}
|
|
137
|
-
/** Remove both vector representations for an entry whose embedding input changed. */
|
|
138
|
-
export function deleteEntryVectors(db, id) {
|
|
139
|
-
db.prepare("DELETE FROM embeddings WHERE id = ?").run(id);
|
|
140
|
-
if (isVecAvailable(db))
|
|
141
|
-
db.prepare("DELETE FROM entries_vec WHERE id = ?").run(id);
|
|
142
|
-
}
|
|
143
|
-
const VEC_DOCS_URL = "https://github.com/itlackey/akm/blob/main/docs/reference/configuration.md#sqlite-vec-extension";
|
|
144
|
-
const VEC_FALLBACK_THRESHOLD = 10_000;
|
|
145
|
-
// Per-database warning state: tracks which databases have already emitted the
|
|
146
|
-
// vec-missing warning so we don't spam on every openDatabase() call.
|
|
147
|
-
const vecInitWarnedDbs = new WeakSet();
|
|
148
|
-
/**
|
|
149
|
-
* Warn if sqlite-vec is unavailable and embedding count exceeds threshold.
|
|
150
|
-
* Called from openDatabase (once at init) and from indexer (each run).
|
|
151
|
-
*/
|
|
152
|
-
export function warnIfVecMissing(db, { once } = { once: false }) {
|
|
153
|
-
if (isVecAvailable(db))
|
|
154
|
-
return;
|
|
155
|
-
if (once && vecInitWarnedDbs.has(db))
|
|
156
|
-
return;
|
|
157
|
-
bestEffort(() => {
|
|
158
|
-
const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
|
|
159
|
-
const count = row?.cnt ?? 0;
|
|
160
|
-
if (count >= VEC_FALLBACK_THRESHOLD) {
|
|
161
|
-
warn("Semantic search is using JS fallback for %d entries. Install sqlite-vec for faster performance.\n See: %s", count, VEC_DOCS_URL);
|
|
162
|
-
if (once)
|
|
163
|
-
vecInitWarnedDbs.add(db);
|
|
164
|
-
}
|
|
165
|
-
}, "embeddings table may not exist yet during init");
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Purge stored embeddings (BLOB rows in `embeddings`, plus the `entries_vec`
|
|
169
|
-
* virtual table) and mark the index as embedding-free. The single place that
|
|
170
|
-
* invalidates embeddings — used on a dimension change, a model/provider change,
|
|
171
|
-
* and a full rebuild.
|
|
172
|
-
*
|
|
173
|
-
* No backup: embeddings are a derived cache, fully regenerable from the markdown
|
|
174
|
-
* by the next `akm index`. (Recovery model decided 2026-06-25.)
|
|
175
|
-
*
|
|
176
|
-
* `dropVecTable: true` DROPs `entries_vec` — used on a DIMENSION change, where
|
|
177
|
-
* the vec0 table must be recreated at the new width by the caller. The default
|
|
178
|
-
* clears its rows in place (same dimension, stale vectors).
|
|
179
|
-
*/
|
|
180
|
-
export function purgeEmbeddings(db, opts) {
|
|
181
|
-
bestEffort(() => db.exec("DELETE FROM embeddings"), "purge embeddings");
|
|
182
|
-
if (isVecAvailable(db)) {
|
|
183
|
-
bestEffort(() => db.exec(opts?.dropVecTable ? "DROP TABLE IF EXISTS entries_vec" : "DELETE FROM entries_vec"), "purge entries_vec");
|
|
184
|
-
}
|
|
185
|
-
setMeta(db, "hasEmbeddings", "0");
|
|
186
|
-
}
|
|
187
|
-
export function upsertEmbedding(db, entryId, embedding) {
|
|
188
|
-
// Pre-flight FK guard: when an entry is deleted between when its id is queued
|
|
189
|
-
// for embedding and when this INSERT runs (e.g. consolidation deletes during
|
|
190
|
-
// a concurrent improve cycle), the INSERT throws "FOREIGN KEY constraint failed"
|
|
191
|
-
// and rolls back the entire batch transaction in the caller, losing every
|
|
192
|
-
// embedding for that run. A cheap SELECT here turns the race into a clean skip.
|
|
193
|
-
const exists = db.prepare("SELECT 1 FROM entries WHERE id = ?").get(entryId);
|
|
194
|
-
if (!exists)
|
|
195
|
-
return { stored: false, vec: "unavailable" };
|
|
196
|
-
const buf = float32Buffer(embedding);
|
|
197
|
-
// Always write to BLOB table (works without sqlite-vec; the JS-cosine fallback
|
|
198
|
-
// reads it, so semantic search survives a vec fast-path failure).
|
|
199
|
-
db.prepare("INSERT OR REPLACE INTO embeddings (id, embedding) VALUES (?, ?)").run(entryId, buf);
|
|
200
|
-
if (!isVecAvailable(db))
|
|
201
|
-
return { stored: true, vec: "unavailable" };
|
|
202
|
-
// Fast path: mirror into the sqlite-vec table. Wrapped in a transaction so a
|
|
203
|
-
// crash between DELETE and INSERT does not leave the entry missing. A THROW
|
|
204
|
-
// here — previously swallowed silently by bestEffort — is now surfaced to the
|
|
205
|
-
// caller so the embedding phase can count it, warn, and route search to the
|
|
206
|
-
// (complete) BLOB table rather than a partial/mismatched vec table.
|
|
207
|
-
try {
|
|
208
|
-
db.transaction(() => {
|
|
209
|
-
db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
|
|
210
|
-
db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
|
|
211
|
-
})();
|
|
212
|
-
return { stored: true, vec: "ok" };
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
return { stored: true, vec: "failed" };
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
export function searchVec(db, queryEmbedding, k) {
|
|
219
|
-
// Fast path: sqlite-vec, but ONLY when the extension is loaded AND the
|
|
220
|
-
// embedding phase did not record a vec insert failure. A degraded fast-path
|
|
221
|
-
// table (partial or dimension-mismatched) would return wrong or missing
|
|
222
|
-
// neighbours, so we honestly fall back to the JS-cosine scan over the
|
|
223
|
-
// complete BLOB table instead.
|
|
224
|
-
if (isVecAvailable(db) && isVecFastPathReady(db)) {
|
|
225
|
-
const buf = float32Buffer(queryEmbedding);
|
|
226
|
-
try {
|
|
227
|
-
return db
|
|
228
|
-
.prepare("SELECT id, distance FROM entries_vec WHERE embedding MATCH ? AND k = ?")
|
|
229
|
-
.all(buf, k);
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
// A dimension mismatch (e.g. the embedding provider/model changed since
|
|
233
|
-
// the fast-path table was built) is a real, expected reason this query
|
|
234
|
-
// specifically cannot use the vec table — the complete BLOB table below
|
|
235
|
-
// is unaffected, so fall back to it rather than either silently
|
|
236
|
-
// returning [] (masking a genuinely corrupt index) or failing the whole
|
|
237
|
-
// search over one degraded index.
|
|
238
|
-
warn("[db] searchVec (sqlite-vec path) failed, falling back to JS-cosine scan:", err instanceof Error ? err.message : String(err));
|
|
239
|
-
return searchBlobVec(db, queryEmbedding, k);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// Fallback: JS-based cosine similarity over BLOB table
|
|
243
|
-
return searchBlobVec(db, queryEmbedding, k);
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Return the k nearest neighbours of an already-indexed entry using its
|
|
247
|
-
* persisted embedding — no re-embedding, no network. Decodes the stored BLOB by
|
|
248
|
-
* byte length (dim = bytes / 4) and reuses searchVec (sqlite-vec fast path or
|
|
249
|
-
* JS-cosine fallback). Returns [] when the entry has no stored embedding or the
|
|
250
|
-
* BLOB is corrupt. The query entry itself is typically returned with distance
|
|
251
|
-
* ~0 — callers should filter it out by id.
|
|
252
|
-
*/
|
|
253
|
-
export function getNeighborsByEntryId(db, id, k) {
|
|
254
|
-
const row = db.prepare("SELECT embedding FROM embeddings WHERE id = ?").get(id);
|
|
255
|
-
if (!row)
|
|
256
|
-
return [];
|
|
257
|
-
const queryEmbedding = bufferToFloat32(row.embedding, Math.floor(row.embedding.byteLength / 4));
|
|
258
|
-
if (!queryEmbedding)
|
|
259
|
-
return [];
|
|
260
|
-
return searchVec(db, queryEmbedding, k);
|
|
261
|
-
}
|
|
262
|
-
function float32Buffer(vec) {
|
|
263
|
-
const f32 = new Float32Array(vec);
|
|
264
|
-
return Buffer.from(f32.buffer);
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Decode a stored embedding BLOB into a Float32 array of `expectedDim`
|
|
268
|
-
* dimensions. Returns `null` (and emits a warning) when the byte length does
|
|
269
|
-
* not exactly match `expectedDim * 4`, including the legacy partial-trailing
|
|
270
|
-
* float case the previous truncating-divide silently swallowed.
|
|
271
|
-
*
|
|
272
|
-
* BUG-M2: the previous `buf.byteLength / 4` divide would truncate any
|
|
273
|
-
* trailing partial float and a misaligned `byteOffset` would throw — both
|
|
274
|
-
* surfaced as opaque generic errors caught upstream.
|
|
275
|
-
*/
|
|
276
|
-
function bufferToFloat32(buf, expectedDim) {
|
|
277
|
-
if (buf.byteLength !== expectedDim * 4) {
|
|
278
|
-
warn("[db] bufferToFloat32: skipping embedding row — expected %d bytes (%d dim x 4), got %d", expectedDim * 4, expectedDim, buf.byteLength);
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
// Copy into a fresh ArrayBuffer to sidestep any byteOffset alignment
|
|
282
|
-
// requirements imposed by Float32Array's typed-array view contract.
|
|
283
|
-
const aligned = new ArrayBuffer(buf.byteLength);
|
|
284
|
-
new Uint8Array(aligned).set(buf);
|
|
285
|
-
const f32 = new Float32Array(aligned);
|
|
286
|
-
return Array.from(f32);
|
|
287
|
-
}
|
|
288
|
-
function searchBlobVec(db, queryEmbedding, k) {
|
|
289
|
-
const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
|
|
290
|
-
if (rows.length === 0)
|
|
291
|
-
return [];
|
|
292
|
-
const expectedDim = queryEmbedding.length;
|
|
293
|
-
const scored = [];
|
|
294
|
-
for (const row of rows) {
|
|
295
|
-
const embedding = bufferToFloat32(row.embedding, expectedDim);
|
|
296
|
-
if (embedding === null)
|
|
297
|
-
continue;
|
|
298
|
-
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
299
|
-
scored.push({ id: row.id, similarity });
|
|
300
|
-
}
|
|
301
|
-
scored.sort((a, b) => b.similarity - a.similarity);
|
|
302
|
-
// Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
|
|
303
|
-
// For normalized vectors: L2² = 2(1 - cos_sim)
|
|
304
|
-
return scored.slice(0, k).map(({ id, similarity }) => ({
|
|
305
|
-
id,
|
|
306
|
-
distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
|
|
307
|
-
}));
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Return all entries that do not yet have an embedding row.
|
|
311
|
-
* Used by the embedding phase to determine which entries need vectors generated.
|
|
312
|
-
*/
|
|
313
|
-
export function getAllEntriesForEmbedding(db, entryIds) {
|
|
314
|
-
const select = `
|
|
315
|
-
SELECT e.id, e.search_text AS searchText, e.item_ref AS itemRef, e.file_path AS filePath FROM entries e
|
|
316
|
-
`;
|
|
317
|
-
const missing = "NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)";
|
|
318
|
-
if (entryIds === undefined) {
|
|
319
|
-
return db.prepare(`${select} WHERE ${missing} ORDER BY e.id`).all();
|
|
320
|
-
}
|
|
321
|
-
const targets = [...new Set(entryIds)].sort((left, right) => left - right);
|
|
322
|
-
const rows = [];
|
|
323
|
-
for (let offset = 0; offset < targets.length; offset += SQLITE_CHUNK_SIZE) {
|
|
324
|
-
const chunk = targets.slice(offset, offset + SQLITE_CHUNK_SIZE);
|
|
325
|
-
if (chunk.length === 0)
|
|
326
|
-
continue;
|
|
327
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
328
|
-
rows.push(...db.prepare(`${select} WHERE e.id IN (${placeholders}) AND ${missing} ORDER BY e.id`).all(...chunk));
|
|
329
|
-
}
|
|
330
|
-
return rows;
|
|
331
|
-
}
|
|
332
|
-
export function getEmbeddingCount(db) {
|
|
333
|
-
const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
|
|
334
|
-
return row.cnt;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Sample up to `limit` already-embedded entries (id, search text, and the
|
|
338
|
-
* stored vector) for the embedding-fingerprint canary check: re-embedding
|
|
339
|
-
* these texts with the CURRENT config and comparing against `vector` is how
|
|
340
|
-
* a model-string rename is told apart from a genuine model/dimension change
|
|
341
|
-
* (#955), without trusting the config string alone.
|
|
342
|
-
*
|
|
343
|
-
* Ordered by `id` for a deterministic, cheap sample (no `ORDER BY RANDOM()`)
|
|
344
|
-
* — the canary only needs "some" already-verified vectors, not a
|
|
345
|
-
* statistically representative one. A corrupt stored BLOB (see
|
|
346
|
-
* `bufferToFloat32`) is skipped rather than failing the whole sample.
|
|
347
|
-
*/
|
|
348
|
-
export function sampleEmbeddedEntriesForCanary(db, limit) {
|
|
349
|
-
const rows = db
|
|
350
|
-
.prepare(`
|
|
351
|
-
SELECT e.id, e.search_text AS searchText, em.embedding AS embedding
|
|
352
|
-
FROM entries e
|
|
353
|
-
JOIN embeddings em ON em.id = e.id
|
|
354
|
-
ORDER BY e.id
|
|
355
|
-
LIMIT ?
|
|
356
|
-
`)
|
|
357
|
-
.all(limit);
|
|
358
|
-
const samples = [];
|
|
359
|
-
for (const row of rows) {
|
|
360
|
-
const vector = bufferToFloat32(row.embedding, Math.floor(row.embedding.byteLength / 4));
|
|
361
|
-
if (vector)
|
|
362
|
-
samples.push({ id: row.id, searchText: row.searchText, vector });
|
|
363
|
-
}
|
|
364
|
-
return samples;
|
|
365
|
-
}
|