Files
NotesAgentic/docs/development/模型隔离向量索引与增量登记.md
T

4.1 KiB
Raw Permalink Blame History

模型隔离向量索引与增量登记

目标与实现

向量仍保存在本地 app.db,重启复用。routed_block_vectors 主键升级为 (space_id, dimensions, block_id),同维度不同模型、同模型不同维度均独立存储。每个空间建立以模型标识和维度的 SHA-256 命名的 vec0 虚拟表,动态表名不包含厂商输入。

首次访问旧空间时,在事务中将已有 JSON 向量转换为归一化 float32 索引,不重新调用 Embedding 模型。随后检索由 sqlite-vec 执行精确 KNN,避免每次在 Python 中解码所有向量、计算点积。该实现是精确搜索,不是 ANN;依据归一化向量的欧氏距离换算余弦分数。参见 sqlite-vec KNN 文档

覆盖检查和 KNN 使用同一事务。向量更新、删除及笔记级联删除通过触发器清理派生索引,新向量与原始向量在同一保存点写入。索引缺失或不完整仍明确失败/按已有策略回退,不混入其他模型结果。本地专用笔记按 local_only 元数据过滤,不同空间的结果继续使用 RRF 融合。

数据库搜索及旧空间转换在线程中运行,不阻塞异步服务事件循环。首次转换仍会占用 SQLite 写事务;超大库应进一步评估迁移耗时。新增空间保留原空间索引,暂不自动清理历史空间。

首次转换使用进程内迁移锁串行执行,在打开检索读快照之前以 BEGIN IMMEDIATE 获取写事务;等待迁移时不持有读事务,避免多个搜索从读锁升级写锁发生冲突。迁移完成后二次检查即可复用。普通检索和隐私分区检索共用该流程,已有索引的检索仅执行读取。

首次转换同时通过 Vault 的异步写入锁与笔记保存、后台索引协调,避免保存事务读取旧 Block 后与迁移争抢写锁。等待是异步的;取消检索时,仍等待迁移线程结束后才释放锁。已就绪的空间直接走只读检查,不进入写入队列。写入锁按事件循环生命周期创建,避免重启后复用已关闭循环的锁。

外部新增文件

文件树检测到新增 Markdown 后先登记元数据与全文索引,然后为每条新笔记持久化 note_vectors_pending:<note_id>,逐笔记在后台计算。不再因新增文件设置全库重建标记;已存在的全库待处理标记仍保留,以免跳过之前未完成的任务。

新增文件在推理期间再次变化时校验快照并重试,只同步该笔记。写入失败保留待处理标记。手动“重建全部”仍执行全库重建,正常重新打开已完成的工作区不再入队。

验证

  • 隔离数据库运行后端全套测试:637 项通过;最终覆盖检查优化另运行检索及后台工作区回归,96 项通过。
  • 新增用例验证同模型不同维度并存、重新连接复用且查询不解码向量 JSON、旧表迁移只计算查询向量,以及外部新增文件不触发全量重建。
  • 并发迁移回归用同步事件暂停第一个请求的转换,同时发起第二个请求;验证普通/隐私分区路径均只迁移一次、两个请求结果一致,并开启 SQLite query_only 验证后续检索不会写入。相关检索与后台工作区测试 91 项通过。
  • 并发保存回归暂停迁移后发起实际 update_note(..., defer_vectors=True),验证保存等待、事件循环仍可运行、放行迁移后正文成功落盘并保留新内容的向量待处理标记;另覆盖搜索被取消时不得提前放行保存。
  • backend/scripts/vector-index-benchmark.py 使用临时数据库、固定随机种子,比较 4000 条 384 维向量的 top-20,旧新路径结果顺序一致。5 次采样中位数:Python 扫描约 1289.64 mssqlite-vec 约 19.67 ms;首次转换约 1329.51 ms。该结果只测向量检索,不包含查询 Embedding、重排与 HTTP 耗时,不代表端到端加速比例。
  • 原始结果:2026-09-06-sqlite-vec-search.json。基准可用后端虚拟环境 Python 直接执行上述脚本,不读写用户 Vault。