为 ChatCrystal 选择向量存储时,首先要解决的不是哪张性能榜更好看,而是用户能否安装、能否备份,以及出问题后能否恢复。项目需要支持 Node.js CLI 和桌面入口,个人用户不一定愿意再维护一个数据库服务。
本文讨论这组约束下的 Vectra、Chroma 和 hnswlib。它是一份架构取舍说明,不是三种实现的性能横评。实现细节按 2026 年 9 月 23 日修订;ChatCrystal 参照提交 7642768,Vectra 的检索实现参照 v0.15.0。
Vectra 的检索方式与计算成本
这里使用的 Vectra LocalIndex 不是 LSH 索引。 在 v0.15.0 的 queryItems() 中,它加载索引、应用元数据过滤,遍历候选项计算余弦相似度,再通过有界最小堆保留 Top K。
这条向量检索路径会对过滤后保留的全部候选评分,没有使用 LSH 或 HNSW 的近似候选搜索。因此,是否命中有用的笔记,仍需从 embedding 模型、分块、查询表达和过滤条件来分析;增加图谱关系可以提供更多探索路径,但不是在修补这里并不存在的 LSH 漏召回。
对于 N 个候选、D 维向量和 K 个结果,评分工作随 N 和 D 增长,Top K 维护另有开销。不要仅引用堆的复杂度就忽略向量计算,也不要给出未经相同硬件和数据验证的“毫秒级保证”。
三种方案解决的层次不同
| 方案 | 接入方式 | 应用仍需承担什么 |
|---|---|---|
Vectra LocalIndex |
Node.js 进程内、以文件持久化的索引 | 数据生命周期、写入协调、恢复和容量验证 |
| Chroma | 数据库及客户端;TypeScript 可连接独立服务 | 服务部署、连接、认证和升级管理 |
| hnswlib | C++ 近似最近邻索引库;Node.js 需选择绑定 | 原生依赖分发,以及文档、元数据和业务接口 |
它们不是三个完全等价的数据库产品。把 hnswlib 的算法速度与一个含网络调用、过滤和文档读取的服务接口直接比较,容易得出没有实际意义的结论。
Vectra:把索引放进现有 Node.js 进程
ChatCrystal 的 getIndex() 在数据目录下创建 vectra-index,实例化 LocalIndex 并缓存对象。应用不用为了这一层再启动独立索引服务。项目实现
这种接入方式便于与现有 TypeScript 服务组合。索引以文件保存,也便于纳入备份流程。不过“索引库不要求额外的数据库服务”不等于整个应用零依赖,也不意味着把文件复制走就一定得到了写入时一致的备份。
Chroma:把检索作为一个独立服务边界
Chroma 的官方文档提供客户端/服务端模式,TypeScript 客户端连接独立的 Chroma 服务。这种边界适合需要共同访问一个索引的多个进程,或已经有服务部署体系的应用。官方说明
对一个简单的桌面安装流程来说,需要额外处理服务启动、端口、健康检查和升级。但这属于产品与部署成本,不能推导成“Chroma 不能本地用”或“Python 一定导致查询更慢”。语言标签无法代替对当前架构和实际访问路径的测量。
hnswlib:优先解决近似最近邻查询
hnswlib 提供 HNSW 索引,查询参数可以调节速度与召回的取舍,并支持保存和加载索引。它本身不是完整的文档管理服务。项目文档
在 Node.js 或 Electron 中使用时,还需要单独评估选定绑定对目标系统、CPU 架构和运行时的支持。是否需要本机编译取决于绑定和预编译产物,不能一概说所有用户都要安装编译器。这里真正的成本是维护这张兼容性矩阵,以及自己补上业务存储层。
为什么 ChatCrystal 采用进程内索引
对这个项目而言,优先级是:沿用 Node.js 技术栈、降低额外进程管理负担、让索引随用户的数据目录保存,并能从笔记重新生成。Vectra 的接入方式与这些需求契合。
但选择并不是“唯一可行”。如果产品逐渐转向共享服务、更多并发访问或更大的向量规模,原来的优先级也可能改变。本地优先也不要求所有入口都在同一台机器上运行:应用服务可以封装进程内索引向客户端提供接口,关键是明确谁拥有写入权。
规模应按向量块计算
笔记数不是索引条目数。假设一条笔记产生多个 chunk,那么要估算的是所有 chunk 的总数、维度和元数据,而不是界面上显示的笔记数量。
以 10,000 个 1,536 维向量为例,仅以每维 4 字节估算,向量有效载荷约为 58.6 MiB。这只是容量计算示例,不是 Vectra 的实测内存。 JavaScript 对象、数组、元数据、索引文件解析以及其他服务状态都会增加占用。
选型实验至少应固定:
- 相同的输入向量、维度、距离定义及元数据过滤条件。
- 数据量、每条笔记的分块数量以及 Top K。
- 冷启动和热查询,查询延迟的中位数与高分位数。
- 峰值内存、索引加载时间、更新和删除成本。
- 对近似检索,使用同一基准答案测召回率。
一个方案可能查询快,但加载慢;也可能单次查询合适,却在导入与查询并行时出现明显内存压力。没有这些条件,不适合给出“万级一定足够”“百万级一定毫秒响应”的分档表。
文件索引仍然需要事务与恢复设计
SQLite 中的笔记和 Vectra 中的向量是两个状态来源。笔记写入成功,并不自动保证所有 chunk 的向量也已提交。
ChatCrystal 的向量删除逻辑使用 beginUpdate()、endUpdate(),出错时尝试 cancelUpdate()。这说明业务需要管理一次索引修改的边界;它不等于 SQLite 与 Vectra 之间存在一个共同事务。
恢复策略应回答几个问题:哪些笔记需要重新生成向量?失败任务如何重试?删除是否清理了全部 chunk?更换 embedding 模型或维度后,能否明确废弃旧索引并重建?备份时怎样避免只拿到一半更新?
笔记内容应作为可恢复的来源,向量索引则应能根据明确的模型和分块配置重新生成。把图谱关系加入检索可以增加探索路径,但不能代替一致性检查,也不能保证找回所有遗漏结果。
什么时候应该重新选型
如果实测表明进程内索引的加载时间、内存或查询延迟超出预算,应重新比较方案;如果需要多进程写入、集中权限控制和共享数据,服务边界可能比嵌入式接入更重要;如果业务已经有成熟的原生模块分发体系,hnswlib 绑定的额外成本也可能很低。
维护风险同样要落到具体版本、发布记录、测试覆盖和升级计划上。代码少不自动意味着风险小,依赖多也不自动意味着不适合。
这次取舍的价值,是让部署方式与项目需求一致,并知道在什么条件下改变决定。索引的下一步是服务真实工作流,例如把知识导出到 Obsidian;上游输入的格式边界可参考五种 AI 对话格式。