---
title: 本地向量数据库选型：Vectra、Chroma 与 hnswlib
description: 从 ChatCrystal 的安装、运行和维护约束出发，对比 Vectra、Chroma 与 hnswlib 的接入方式、检索成本、备份和恢复边界。
image: "https://zengliangyi.com/og/blog/local-vector-database-selection.png"
---

为 [ChatCrystal](/projects/chatcrystal) 选择向量存储时，首先要解决的不是哪张性能榜更好看，而是用户能否安装、能否备份，以及出问题后能否恢复。项目需要支持 Node.js CLI 和桌面入口，个人用户不一定愿意再维护一个数据库服务。

本文讨论这组约束下的 Vectra、Chroma 和 hnswlib。它是一份架构取舍说明，不是三种实现的性能横评。实现细节按 2026 年 9 月 23 日修订；ChatCrystal 参照提交 [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8)，Vectra 的检索实现参照 [v0.15.0](https://github.com/Stevenic/vectra/blob/v0.15.0/src/LocalIndex.ts)。

## 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` 并缓存对象。应用不用为了这一层再启动独立索引服务。[项目实现](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/vector-index.ts)

这种接入方式便于与现有 TypeScript 服务组合。索引以文件保存，也便于纳入备份流程。不过“索引库不要求额外的数据库服务”不等于整个应用零依赖，也不意味着把文件复制走就一定得到了写入时一致的备份。

### Chroma：把检索作为一个独立服务边界

Chroma 的官方文档提供客户端／服务端模式，TypeScript 客户端连接独立的 Chroma 服务。这种边界适合需要共同访问一个索引的多个进程，或已经有服务部署体系的应用。[官方说明](https://docs.trychroma.com/docs/run-chroma/client-server)

对一个简单的桌面安装流程来说，需要额外处理服务启动、端口、健康检查和升级。但这属于产品与部署成本，不能推导成“Chroma 不能本地用”或“Python 一定导致查询更慢”。语言标签无法代替对当前架构和实际访问路径的测量。

### hnswlib：优先解决近似最近邻查询

hnswlib 提供 HNSW 索引，查询参数可以调节速度与召回的取舍，并支持保存和加载索引。它本身不是完整的文档管理服务。[项目文档](https://github.com/nmslib/hnswlib)

在 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](/blog/chatcrystal-to-obsidian)；上游输入的格式边界可参考[五种 AI 对话格式](/blog/ai-conversation-data-formats)。

分享：

##  相关文章

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

从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵，检查工具标记、思考文本、顺序与快照覆盖范围。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/ai-conversation-parser-boundaries)    [##  AI SDK Provider 抽象：能力表与协议边界

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出，用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#TypeScript](/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter 内部链路：身份、内容哈希与重新导入

对照 ChatCrystal 的注册表、导入器和远端载荷，验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/source-adapter-internals)

##  从文章到代码

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

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "本地向量数据库选型：Vectra、Chroma 与 hnswlib",
  "description": "从 ChatCrystal 的安装、运行和维护约束出发，对比 Vectra、Chroma 与 hnswlib 的接入方式、检索成本、备份和恢复边界。",
  "url": "https://zengliangyi.com/blog/local-vector-database-selection/",
  "image": "https://zengliangyi.com/images/blog/local-vector-database-selection.svg",
  "datePublished": "2026-06-09T01:00:00.000Z",
  "dateModified": "2026-09-24T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/local-vector-database-selection/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "本地向量数据库选型：Vectra、Chroma 与 hnswlib",
      "item": "https://zengliangyi.com/blog/local-vector-database-selection/"
    }
  ]
}
```
