Repository navigation
Description
背景
src/store.js 的向量解析缓存上限写死 4000,而活跃带向量行已 6730:扫到第 4001 条开始驱逐自己刚填的条目,扫完只剩最后 4000 条,下一轮又从第一条 miss。真库四轮实测命中率恒 0.0% —— 缓存等于不存在,每次检索全量重解析(约 6730 条 × 1024 维 JSON.parse)。
同一目录下另有一处:searchVector 与 vectorDimensionState 的 SQL 只 SELECT id,但 embedding != '' 要读 embedding 列才能判定,SQLite 就得把整列向量文本(约 134MB)从磁盘搬一遍,读完一条都不用。
这两处都在检索热路径上,属于同一个「同一条查询更便宜」的保证,所以合为一条线。
缺陷 1(perf):缓存上限写死 4000,小于工作集 → 命中率恒 0
A/B 对照(同一 WAL 快照、同一 query、同一库):
| 臂 | 每轮耗时 |
|---|---|
| 旧实现 | 1.8–2.4s |
| 新实现(第二轮起) | 370–424ms |
×5.6。固定调大只是把复发推到下个月(活跃行近 30 天净增 6156 条),所以上限改为跟工作集自适应,并用内存预算封顶。
存储改 Float32Array:真实占用 55.5MB → 29.7MB(省 47%)。前 50 条排序与冷解析逐位一致,分数最大差 0.00e+0。
Array.isArray 对 typed array 返回 false,若只换存储不改守卫,cosine / cosineSimilarity / setEmbedding 会把每个相似度静默算成 0、把「设置向量」读成「清空向量」。对外契约不变(getParsedEmbedding / getEmbeddings 仍返回 number[])。
缺陷 2(perf):只取 id 的扫描却带着 embedding != '' 谓词
同一条 SQL 在真库副本上实测:
| 臂 | SQL 层 | 单轮检索 |
|---|---|---|
| 保留谓词 | 265ms | 138.1 / 135.6 / 133.5ms |
| 去掉谓词 | 96ms | 47.4 / 46.6 / 49.8ms |
命中率两臂均 0% → 100%(因缺陷 1 同时修掉),前 50 条 id 顺序与分数完全一致。
只摘这两处只取 id 的扫描;三处统计类查询(embeddedCount / embeddingModelBreakdown / embeddingDimensionBreakdown)保留谓词 —— 它们要数「已索引」条数,空串行若被算进分母会变成新的假数。
摘谓词的前提是空串行不会把检索搞崩。库里现在 0 条空串(setEmbedding 写 NULL,能造出空串的只有外部直接 UPDATE),所以这是「目前恰好为真」的假设,必须锁住:readVector 对 JSON.parse('') 抛错返回 undefined,空串行既不计分也不崩。
口径陷阱(已写进注释,防止被当证据引用错)
heapUsed不统计 TypedArray 底层 ArrayBuffer —— 只看heapUsed会读成省 96%。必须按heap + arrayBuffers计。- 旧实现保持上限 4000 时只装得下 4000/6730 条,常驻 31.9MB 但命中率 0%,与「装满」的 29.7MB 不可比 —— 对照组必须把旧上限抬到能装下工作集。
- 「SQL 层 265ms → 96ms」不等于「每轮检索省 170ms」。落到一轮完整检索上只省约 90ms(其余是解析与打分开销,不随谓词变化)。两个口径分开写。
测试
test/embedding-cache-f32.test.js:6 条(缺陷 1)+ 2 条空串/坏 JSON 护栏(缺陷 2)+ 1 条读取次数锁- 变异验证(四条均咬住):自适应退回固定上限 /
cosine守卫退回Array.isArray/setEmbedding守卫退回 / 独立基准分数 - 变异实验暴露过一个真实缺口:只比「缓存路径与冷路径是否一致」测不出两边一起错,补了一条独立基准分数锁
为什么不写「断言这句 SQL 不含 != ''」的结构锁:那会把测试绑在字面量上,换行、改别名、换 ${archivedFilter} 写法都会无辜变红,红了第一反应是删断言。改为锁行为:数 embeddingCacheStats 的读取增量,断言三行非 NULL 必须各读一次。实测当前实现 3 次(绿);谓词加回后 2 次(红)。
验收
- 真库副本四轮命中率 0% → 100%
- 前 50 条 id 顺序与分数完全一致(逐位,最大差 0.00e+0)
- 空串行与坏 JSON 混入不崩
- 谓词被加回来时读取次数锁变红
- 三处统计类查询保留谓词,计数不变
-
npm test全量 2848 用例 / 2847 通过 / 0 失败 / 1 跳过(src↔lib一致性守门通过)
不做
不改向量路合并逻辑(那是 pin 池那条线的事),不改 SQLite 侧索引结构。实测口径均为真库副本,线上库全程只读。
Activity
modusensus commentedon Nov 6, 2026
复核结论(PR #594,head
70a79b39)已做源码级复核 + 独立复跑,并对 CodeRabbit 的 6 条行内评审逐条读源码核实。
两处缺陷与本 PR 的对应
src/store.js:1528-1534noteSweep(rows, avgDim)自适应 +Float32Array(55.5MB → 29.7MB)SELECT id的 SQL 带着embedding != ''谓词,要搬整列向量文本searchVector(:2697-2700)与vectorDimensionState(:2617)两处谓词;三处统计类查询保留三处守卫同步改
isVectorLike(Array.isArray对 typed array 返 false,不改会让cosine/cosineSimilarity/setEmbedding静默算 0)。对外契约不变:getParsedEmbedding/getEmbeddings仍返number[](src/store.js:1558-1561用Array.from)。CodeRabbit 6 条逐条核实
成立、建议修(1 条)
src/store.js:1522—cacheEmbedding只在插入前删一条(if (embeddingCache.size >= embeddingCacheCap) embeddingCache.delete(...)),cap 变小后 size 仍可超新 cap,直到后续插入逐条淘汰。noteSweep在「先扫 archived 大行集、再扫活跃小行集」时会出现size > cap的中间态:只是内存短时超标,不丢正确性。建议 cap 调小时按 FIFO 裁到新 cap + 回归用例。Major 但当前不可达(1 条)
src/store.js:1533— 用本次扫描的avgDim推算 cap,跨维度混扫时低估,机制成立。但src/store.js:1504EMBEDDING_CACHE_FLOOR = 4000是硬下限,且「查询 128 维而库里全是 4096 维」这种维度错配库本机不存在(实测只有 1024 维 6,560 条与 2048 维 373 条)。标为潜在风险、建议记 issue 不修。理论成立、实际不可达(2 条)
src/store.js:1547— Float32 无法表示 1e-50 / 1e40。真实嵌入量级 1e-2~1e1,只在人为构造向量时出现。src/store.js:2457— 缓存存new Float32Array(vector)而非new Float32Array(JSON.parse(json)),NaN / undefined 时缓存与库不一致。方向对,但真实嵌入无 NaN。API 宽容度(1 条)
src/store.js:2442—searchVector仍用Array.isArray(:2685),不认Float32Array。但查询向量来自embedQuery的number[],现有调用方无一是 typed array。若改则用 PR 已在三处落地的isVectorLike守卫。方法论建议(1 条)
test/embedding-cache-f32.test.js:140— 用SELECT 1当清缓存手段不如「关闭并重开库」。PR 已用「独立基准分数锁」覆盖「缓存路径与冷路径一起错」这个核心风险(正文自述),属可采纳改进、不阻塞。两处注释陈旧(本 PR 未碰,建议随后修)
src/store.js:2611仍写「行数在 4000 条 FIFO 上限内」,:3581仍写「FIFO 上限 4000,自愈」,而 cap 已改自适应。src/store.js:2619/:2583仍称vectorDimensionState与searchVector行集「逐条相同、语义等价」,但前者硬编码archived = 0、后者走archivedFilter,includeArchived=true时不再等价。复跑(本机实测,head
70a79b39)scripts/check-sync.jsnpm testCI:6 条 test 腿 + gitleaks + dependency-review + workflow-hygiene + zizmor + CodeRabbit + codecov/patch 全 success,osv-scanner skipping。
未做
未在 4096 维或维度错配库上压测;未做长时间运行的内存曲线观测(只有 PR 正文里的
heap + arrayBuffers口径数据);未跑 ubuntu / node22 / test-host-legacy(由 CI 覆盖)。