本文面向ChatCrystal 语义搜索返回空结果或报错怀疑向量索引损坏的开发者。预计阅读时间7 分钟症状语义搜索出了问题具体表现为以下几种搜索页面输入关键词后返回空结果但笔记明明存在搜索时报错500 Internal Server Error服务端日志出现 vectra 相关异常crystal search xxx命令无结果但crystal notes list能看到笔记新笔记的embedding_status一直停在pending或syncing不会变成done这些问题很可能指向同一个原因vectra 向量索引文件损坏了。vectra 是什么vectra 是一个纯 JavaScript 实现的本地向量搜索引擎。ChatCrystal 用它存储笔记的 embedding 向量支撑语义搜索功能。它不需要外部服务数据全部存在本地磁盘的文件里。关键特性纯 JS无原生依赖跨平台基于文件系统的本地索引非 SQLite支持元数据过滤ChatCrystal 用noteId标记每个向量属于哪条笔记提供beginUpdate()/endUpdate()事务式写入正因为索引是独立于 SQLite 的文件系统结构它有自己的损坏风险。索引文件在哪vectra 索引存储在数据目录下的vectra-index/文件夹~/.chatcrystal/data/vectra-index/ # 默认位置 %APPDATA%/ChatCrystal/data/vectra-index/ # Electron 打包版这个目录由 vectra 的LocalIndex类管理内部包含向量数据和索引元文件。ChatCrystal 在内存中维护一个单例_index首次访问时创建LocalIndex实例并调用createIndex()初始化。对应的源码路径是server/src/services/vector-index.tsconstINDEX_PATHresolve(appConfig.dataDir,vectra-index);let_index:LocalIndex|nullnull;exportasyncfunctiongetIndex():PromiseLocalIndex{if(_index)return_index;_indexnewLocalIndex(INDEX_PATH);if(!(await_index.isIndexCreated())){await_index.createIndex();}return_index;}常见损坏场景1. 进程中断embedding 生成是一个多步操作先调用 Embedding 模型拿到向量然后通过beginUpdate()写入索引再更新 SQLite最后endUpdate()提交。如果在这中间被 kill 掉CtrlC、系统崩溃、Electron 窗口强关索引可能处于半写入状态。// embedding.ts 中的写入流程awaitindex.beginUpdate();// ... 插入新向量 ...// ... 更新 SQLite ...// ... 删除旧向量 ...awaitindex.endUpdate();// 如果没走到这里索引不完整2. 磁盘空间不足vectra 写入索引文件时如果磁盘满了写入会失败。部分写入的文件会导致索引结构不一致。3. 并发写入冲突ChatCrystal 的任务队列p-queue已经把并发限制为 1正常情况下不会出现并发写入。但如果你同时运行了多个 Crystal 实例比如 Electron 版和 CLI 版共用同一个数据目录两个进程可能同时写索引导致文件冲突。4. 手动删除了部分索引文件有些人清理磁盘时会误删vectra-index/目录下的部分文件。vectra 的索引是一个整体结构删掉任何一个文件都可能导致整个索引不可用。embedding_status 状态机理解索引问题的关键是理解embedding_status的状态流转pending → syncing → done → failed状态含义pending笔记已生成但还没有 embeddingsyncing向量已写入 vectra等待确认提交doneembedding 完全就绪可用于搜索failed生成过程中出错需要重试如果一条笔记卡在syncing状态说明向量可能已经写入了 vectra 但endUpdate()没有成功执行。下次搜索时 ChatCrystal 会尝试自动恢复见下文但不一定能成功。自动恢复机制ChatCrystal 内置了两层自动恢复机制大多数情况下你不需要手动干预。搜索前自动清理每次执行语义搜索时semanticSearch()会先调用preflightSemanticSearchVectorCleanup()处理最多 25 条待清理任务exportasyncfunctionpreflightSemanticSearchVectorCleanup():Promisevoid{try{awaitprocessPending({limit:25});}catch{// 搜索不应因为清理失败而中断}}这个清理任务的来源是vector_cleanup_tasks表。当笔记被删除或需要重新生成 embedding 时系统会往这个表里插入一条待清理记录由下次搜索触发执行。syncing 状态自动修复当generateEmbeddings()发现笔记处于syncing状态时会先检查 vectra 中的向量是否完整。如果完整直接将状态标记为done跳过重新生成if(awaitmaybeFinalizeCommittedSyncingNote(db,index,noteId,noteStatus,currentDbVectraIds)){returnchunks.length;// 已经修好了不用重做}清理失败不阻塞搜索两个关键设计preflightSemanticSearchVectorCleanup的异常被 catch 吞掉搜索照常进行vector_cleanup_tasks记录失败后保留pending状态下次搜索会重试这意味着即使索引有部分损坏搜索功能不会完全瘫痪。手动修复方案如果自动恢复不起作用或者索引损坏严重手动修复是最可靠的方式。方案一删除索引重建推荐最简单粗暴但最有效的方法。删除整个索引目录然后重新生成所有 embedding# 1. 停止服务crystal serve stop# 2. 删除索引目录rm-rf~/.chatcrystal/data/vectra-index/# 3. 重启服务crystal serve# 4. 重新生成所有 embeddingcurl-XPOST http://localhost:3721/api/embeddings/batch这个接口会找出所有embedding_status不是done的笔记重新加入队列生成 embedding。注意crystal summarize --all只会处理状态为imported、error或summarizing的对话不会为已有的笔记重新生成 embedding。如果你的笔记已经存在只想重建索引必须使用上面的 batch API。方案二重建单条笔记的 embedding如果只有个别笔记的 embedding 有问题可以单独重建# 通过 API 重建指定笔记的 embeddingcurl-XPOST http://localhost:3721/api/notes/123/embed方案三代码级清除如果你在开发或调试可以直接调用clearEmbeddingIndex()import{clearEmbeddingIndex}from./services/vector-index.js;clearEmbeddingIndex();// 删除整个索引目录 清空内存缓存这个函数会将内存中的_index单例设为null用rmSync递归删除vectra-index/目录下次getIndex()调用时自动重建空索引beginUpdate / endUpdate 事务模式vectra 的写入不是原子的。beginUpdate()开启一个写入批次所有insertItem()/deleteItem()操作在endUpdate()之前都不会持久化到磁盘。ChatCrystal 在所有使用这个模式的地方都有错误处理letupdateOpenfalse;try{awaitindex.beginUpdate();updateOpentrue;// ... 写入操作 ...awaitindex.endUpdate();updateOpenfalse;}catch(error){if(updateOpen){try{index.cancelUpdate();// 回滚未提交的变更}catch{// 忽略取消失败优先抛出原始错误}}throwerror;}cancelUpdate()会丢弃未提交的变更。但如果你的进程在beginUpdate()之后、cancelUpdate()之前被强杀vectra 内部可能残留中间状态。这就是索引损坏的主要来源。预防措施1. 不要共用数据目录Electron 版和 CLI 版如果同时运行确保它们使用不同的数据目录或者同一时间只有一个进程在运行。2. 保持磁盘空间充足embedding 向量会占用一定磁盘空间每个向量约 6KB取决于模型维度。定期检查数据目录所在磁盘的剩余空间。3. 正常关闭服务用crystal serve stop或 Electron 的退出菜单关闭不要直接 kill 进程。这给正在执行的 embedding 任务一个完成的机会。4. 定期检查 embedding 状态# 查看数据库统计对话数、笔记数、标签数crystal status如果发现大量笔记处于failed或syncing状态及时用POST /api/embeddings/batch重新队列。排查流程总结搜索无结果 ↓ crystal status → 确认笔记存在 ↓ curl GET /api/notes?embedding_statuspending → 检查是否有待处理的笔记 ↓ 有 pending/failed/syncing ├── 是 → curl -X POST /api/embeddings/batch → 等待完成 → 重试搜索 │ ↓ │ 仍然失败 │ ├── 是 → rm -rf vectra-index/ → 重启 → /api/embeddings/batch │ └── 否 → 搞定 └── 全是 done 但仍无结果 ↓ Embedding 模型配置是否正确→ 见「LLM 和 Embedding 不能混用」下一步LLM 和 Embedding 不能混用 — 确保 Embedding 模型配置正确Embedding 模型选型与配置 — 不同模型的维度和性能对比Ollama 本地部署零成本跑通全流程 — 用本地模型跑 embedding项目地址github.com/ZengLiangYi/ChatCrystal
vectra 向量索引文件损坏怎么办
本文面向ChatCrystal 语义搜索返回空结果或报错怀疑向量索引损坏的开发者。预计阅读时间7 分钟症状语义搜索出了问题具体表现为以下几种搜索页面输入关键词后返回空结果但笔记明明存在搜索时报错500 Internal Server Error服务端日志出现 vectra 相关异常crystal search xxx命令无结果但crystal notes list能看到笔记新笔记的embedding_status一直停在pending或syncing不会变成done这些问题很可能指向同一个原因vectra 向量索引文件损坏了。vectra 是什么vectra 是一个纯 JavaScript 实现的本地向量搜索引擎。ChatCrystal 用它存储笔记的 embedding 向量支撑语义搜索功能。它不需要外部服务数据全部存在本地磁盘的文件里。关键特性纯 JS无原生依赖跨平台基于文件系统的本地索引非 SQLite支持元数据过滤ChatCrystal 用noteId标记每个向量属于哪条笔记提供beginUpdate()/endUpdate()事务式写入正因为索引是独立于 SQLite 的文件系统结构它有自己的损坏风险。索引文件在哪vectra 索引存储在数据目录下的vectra-index/文件夹~/.chatcrystal/data/vectra-index/ # 默认位置 %APPDATA%/ChatCrystal/data/vectra-index/ # Electron 打包版这个目录由 vectra 的LocalIndex类管理内部包含向量数据和索引元文件。ChatCrystal 在内存中维护一个单例_index首次访问时创建LocalIndex实例并调用createIndex()初始化。对应的源码路径是server/src/services/vector-index.tsconstINDEX_PATHresolve(appConfig.dataDir,vectra-index);let_index:LocalIndex|nullnull;exportasyncfunctiongetIndex():PromiseLocalIndex{if(_index)return_index;_indexnewLocalIndex(INDEX_PATH);if(!(await_index.isIndexCreated())){await_index.createIndex();}return_index;}常见损坏场景1. 进程中断embedding 生成是一个多步操作先调用 Embedding 模型拿到向量然后通过beginUpdate()写入索引再更新 SQLite最后endUpdate()提交。如果在这中间被 kill 掉CtrlC、系统崩溃、Electron 窗口强关索引可能处于半写入状态。// embedding.ts 中的写入流程awaitindex.beginUpdate();// ... 插入新向量 ...// ... 更新 SQLite ...// ... 删除旧向量 ...awaitindex.endUpdate();// 如果没走到这里索引不完整2. 磁盘空间不足vectra 写入索引文件时如果磁盘满了写入会失败。部分写入的文件会导致索引结构不一致。3. 并发写入冲突ChatCrystal 的任务队列p-queue已经把并发限制为 1正常情况下不会出现并发写入。但如果你同时运行了多个 Crystal 实例比如 Electron 版和 CLI 版共用同一个数据目录两个进程可能同时写索引导致文件冲突。4. 手动删除了部分索引文件有些人清理磁盘时会误删vectra-index/目录下的部分文件。vectra 的索引是一个整体结构删掉任何一个文件都可能导致整个索引不可用。embedding_status 状态机理解索引问题的关键是理解embedding_status的状态流转pending → syncing → done → failed状态含义pending笔记已生成但还没有 embeddingsyncing向量已写入 vectra等待确认提交doneembedding 完全就绪可用于搜索failed生成过程中出错需要重试如果一条笔记卡在syncing状态说明向量可能已经写入了 vectra 但endUpdate()没有成功执行。下次搜索时 ChatCrystal 会尝试自动恢复见下文但不一定能成功。自动恢复机制ChatCrystal 内置了两层自动恢复机制大多数情况下你不需要手动干预。搜索前自动清理每次执行语义搜索时semanticSearch()会先调用preflightSemanticSearchVectorCleanup()处理最多 25 条待清理任务exportasyncfunctionpreflightSemanticSearchVectorCleanup():Promisevoid{try{awaitprocessPending({limit:25});}catch{// 搜索不应因为清理失败而中断}}这个清理任务的来源是vector_cleanup_tasks表。当笔记被删除或需要重新生成 embedding 时系统会往这个表里插入一条待清理记录由下次搜索触发执行。syncing 状态自动修复当generateEmbeddings()发现笔记处于syncing状态时会先检查 vectra 中的向量是否完整。如果完整直接将状态标记为done跳过重新生成if(awaitmaybeFinalizeCommittedSyncingNote(db,index,noteId,noteStatus,currentDbVectraIds)){returnchunks.length;// 已经修好了不用重做}清理失败不阻塞搜索两个关键设计preflightSemanticSearchVectorCleanup的异常被 catch 吞掉搜索照常进行vector_cleanup_tasks记录失败后保留pending状态下次搜索会重试这意味着即使索引有部分损坏搜索功能不会完全瘫痪。手动修复方案如果自动恢复不起作用或者索引损坏严重手动修复是最可靠的方式。方案一删除索引重建推荐最简单粗暴但最有效的方法。删除整个索引目录然后重新生成所有 embedding# 1. 停止服务crystal serve stop# 2. 删除索引目录rm-rf~/.chatcrystal/data/vectra-index/# 3. 重启服务crystal serve# 4. 重新生成所有 embeddingcurl-XPOST http://localhost:3721/api/embeddings/batch这个接口会找出所有embedding_status不是done的笔记重新加入队列生成 embedding。注意crystal summarize --all只会处理状态为imported、error或summarizing的对话不会为已有的笔记重新生成 embedding。如果你的笔记已经存在只想重建索引必须使用上面的 batch API。方案二重建单条笔记的 embedding如果只有个别笔记的 embedding 有问题可以单独重建# 通过 API 重建指定笔记的 embeddingcurl-XPOST http://localhost:3721/api/notes/123/embed方案三代码级清除如果你在开发或调试可以直接调用clearEmbeddingIndex()import{clearEmbeddingIndex}from./services/vector-index.js;clearEmbeddingIndex();// 删除整个索引目录 清空内存缓存这个函数会将内存中的_index单例设为null用rmSync递归删除vectra-index/目录下次getIndex()调用时自动重建空索引beginUpdate / endUpdate 事务模式vectra 的写入不是原子的。beginUpdate()开启一个写入批次所有insertItem()/deleteItem()操作在endUpdate()之前都不会持久化到磁盘。ChatCrystal 在所有使用这个模式的地方都有错误处理letupdateOpenfalse;try{awaitindex.beginUpdate();updateOpentrue;// ... 写入操作 ...awaitindex.endUpdate();updateOpenfalse;}catch(error){if(updateOpen){try{index.cancelUpdate();// 回滚未提交的变更}catch{// 忽略取消失败优先抛出原始错误}}throwerror;}cancelUpdate()会丢弃未提交的变更。但如果你的进程在beginUpdate()之后、cancelUpdate()之前被强杀vectra 内部可能残留中间状态。这就是索引损坏的主要来源。预防措施1. 不要共用数据目录Electron 版和 CLI 版如果同时运行确保它们使用不同的数据目录或者同一时间只有一个进程在运行。2. 保持磁盘空间充足embedding 向量会占用一定磁盘空间每个向量约 6KB取决于模型维度。定期检查数据目录所在磁盘的剩余空间。3. 正常关闭服务用crystal serve stop或 Electron 的退出菜单关闭不要直接 kill 进程。这给正在执行的 embedding 任务一个完成的机会。4. 定期检查 embedding 状态# 查看数据库统计对话数、笔记数、标签数crystal status如果发现大量笔记处于failed或syncing状态及时用POST /api/embeddings/batch重新队列。排查流程总结搜索无结果 ↓ crystal status → 确认笔记存在 ↓ curl GET /api/notes?embedding_statuspending → 检查是否有待处理的笔记 ↓ 有 pending/failed/syncing ├── 是 → curl -X POST /api/embeddings/batch → 等待完成 → 重试搜索 │ ↓ │ 仍然失败 │ ├── 是 → rm -rf vectra-index/ → 重启 → /api/embeddings/batch │ └── 否 → 搞定 └── 全是 done 但仍无结果 ↓ Embedding 模型配置是否正确→ 见「LLM 和 Embedding 不能混用」下一步LLM 和 Embedding 不能混用 — 确保 Embedding 模型配置正确Embedding 模型选型与配置 — 不同模型的维度和性能对比Ollama 本地部署零成本跑通全流程 — 用本地模型跑 embedding项目地址github.com/ZengLiangYi/ChatCrystal