跳到正文
曾亮懿 ZengLiangYi Blog

Vectra 索引异常排查:区分状态、清理与重建

从数据库状态、Vectra 文件和待清理任务三个层面排查语义搜索异常,按风险选择单篇重建或完整索引恢复。

曾亮懿

曾亮懿

阅读约 4 分钟
目录 展开 收起

搜索为空或报错时,不要先把所有问题归为“索引损坏”。ChatCrystal 把笔记状态、SQLite 中的 embedding 记录、Vectra 文件和删除清理任务分开维护;先确定是哪一层不一致,才能选择修复范围。

本文依据固定提交 7642768 的 向量索引、embedding 流程 和 数据库初始化。完整恢复示例会移走索引目录;执行前停止所有写入,包括共享目录的桌面应用,并备份整个数据目录。命令中的 <note-id> 必须替换为实际 ID。

先记录症状与状态

空结果可能来自没有笔记、笔记仍在 pending、向量生成失败、索引目录不存在,或查询与当前模型不匹配。crystal status 只能帮助确认总体数据量;要查看笔记的 embedding_status,用 API 返回的笔记详情:

curl 'http://localhost:3721/api/notes?limit=100'
curl 'http://localhost:3721/api/notes/<note-id>'

列表默认按时间排序,limit 最大为 100,响应里的笔记对象包含状态字段。done 是数据库状态,不足以证明文件索引完整:启动时,只要笔记仍有 embedding 行,初始化代码就会将它标为 done,不会检查 Vectra 中是否存在对应项。pending、failed 和 syncing 则需要进一步检查。不要把 GET /api/notes?embedding_status=pending 当成过滤器:这个路由没有这个查询参数。

了解索引和数据库的边界

getIndex() 首次访问时创建 vectra-index 下的 LocalIndex;它不会扫描 SQLite 的 embedding 表并自动补建所有向量。数据库记录和文件索引因此可能暂时不一致。

一次笔记重建大致按这个顺序执行:先向模型请求向量,在 Vectra 更新批次中插入新项,把 SQLite 的 embedding 行换成新 ID 并标为 syncing,删除旧项,提交更新,最后标为 done。异常时会尝试 cancelUpdate();如果数据库已经换过行,状态会保存为 failed,供后续重试。强制终止进程仍可能留下需要人工判断的中间状态。

删除笔记产生的 Vectra 清理记录写在 vector_cleanup_tasks 表。每次语义搜索会尝试处理最多 25 条待处理清理任务;清理失败会保留任务并等待下次重试,搜索本身不会因这一步失败而直接停止。这是删除任务的重试机制,不是对任意损坏文件的通用修复。

先尝试低风险的单篇重建

如果只有一条笔记状态异常,保留索引目录并调用同步入口,观察响应和状态:

curl --request POST \
  'http://localhost:3721/api/notes/<note-id>/embed'
curl 'http://localhost:3721/api/notes/<note-id>'

这个接口通常会生成向量,但有一个快速确认分支:若笔记为 syncing 且数据库中的向量 ID 全部已在 Vectra 提交,就直接标为 done 返回,不再请求模型。因此它不是无条件强制重建接口,也不会重写笔记摘要。实际生成时,模型、地址和向量维度必须仍然可用;若切换了 provider 或 embedding model,先按 Embedding 配置指南确认清空与重建范围。

再决定是否清空整个索引

只有在确认文件索引不可读、而数据库笔记仍是可用来源时,才考虑完整重建。下面是默认 CLI 目录的示例;备份位置用 mktemp 创建,避免覆盖或嵌套进已有备份:

crystal serve stop &&
  recovery_backup=$(mktemp -d "$HOME/.chatcrystal/vectra-recovery.XXXXXX") &&
  mv "$HOME/.chatcrystal/data/vectra-index" "$recovery_backup/vectra-index" &&
  crystal serve --daemon &&
  curl --request POST 'http://localhost:3721/api/notes/<note-id>/embed'

打包应用的数据目录由运行时路径决定,不能无条件套用上面的默认路径;先从应用配置和日志确认实际目录。通过 GET /api/notes?limit=100&offset=0 分页列出受影响笔记,为每个 ID 重复单篇重建请求并检查响应,直到覆盖全部目标。

clearEmbeddingIndex() 只会删除索引并清空进程内缓存,不会自动把已有 done 笔记改为待处理。POST /api/embeddings/batch 只选择 pending、failed、syncing,而重启还可能把有 embedding 行的笔记改为 done;仅移走索引再调用批量接口,可能得到 queued: 0。逐篇显式重建才能避免漏掉这些笔记。

用可重复的结果验收

恢复后记录三类证据:

  1. 目标笔记的 embedding_status 变为 done,并且批量队列中的失败数不再增加。
  2. 用一个已知答案、一个同义问题和一个无答案问题搜索;检查笔记是否出现、顺序是否变化、是否有误匹配。
  3. 重启服务后再次查询,确认新索引可以被重新打开。

索引可读只证明文件结构能访问,不证明检索质量。若重建后仍为空,转而检查模型配置、实际输入字段和查询条件;参见 语义搜索流程。

分享:

相关文章

AI 对话格式差异:事件、快照与跨库消息定位

对照五个固定适配器,区分 JSONL 容器、事件语义与 SQLite 键布局,用两个内存数据库观察消息索引、孤立数据和未知版本。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

AI 对话管理方案:四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入,区分原始副本、可读导出与知识库,并用合成清单验证身份和信息保留。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

五种 AI 对话解析器:字段保留与损失边界

从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵,检查工具标记、思考文本、顺序与快照覆盖范围。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #TypeScript #Node.js

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。