跳到正文
曾亮懿 ZengLiangYi Blog

本地向量数据库选型:Vectra、Chroma 与 hnswlib

从 ChatCrystal 的安装、运行和维护约束出发,对比 Vectra、Chroma 与 hnswlib 的接入方式、检索成本、备份和恢复边界。

曾亮懿

曾亮懿

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

为 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 对话格式。

分享:

相关文章

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

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

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

AI SDK Provider 抽象:能力表与协议边界

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出,用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。

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

SourceAdapter 内部链路:身份、内容哈希与重新导入

对照 ChatCrystal 的注册表、导入器和远端载荷,验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。

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

从文章到代码

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