news 2026/9/8 17:01:30

Chroma 向量数据库表结构详解:从 SQLite 到 HNSW 索引的底层逻辑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Chroma 向量数据库表结构详解:从 SQLite 到 HNSW 索引的底层逻辑

上周末我在给一个内部文档问答项目做向量化检索,几十份 Markdown 切片后全部丢进 Chroma。数据量上来后,我习惯性打开持久化目录里的 SQLite 文件看了一眼,结果被里面密密麻麻的表结构吓了一跳:我只是 add 了一个 collection,为什么会有这么多张表?它们每个都是干嘛的?相互之间怎么关联?如果直接改某张表会不会把整个索引弄坏?

这篇文章就是冲着解决这些问题来的。我会结合一次完整的 Chroma 向量数据库实战,从项目场景、代码落库,到表结构逐张拆解、关联关系串讲,最后把我在实操里踩过的坑一并交底。不管你是第一次接触向量数据库,还是已经用 Chroma / Milvus 做过 RAG 项目,都应该能在这里找到点有用的东西。

1. 向量数据库实战前,先对齐几个核心概念

1.1 从“关键词匹配”到“语义相似”的转变

传统关系型数据库和 Elasticsearch 这类全文检索引擎,擅长处理的是精确匹配和词法匹配。比如你搜“怎么申请年假”,它会把文档里包含“申请”“年假”这些词的内容找出来。但实际业务问题往往不按关键词出牌,用户会说“我想休息两天”“公司规定里有没有休假政策”,这时候关键词匹配就很难召回同一份文档。

向量数据库解决的是“语义相似”的问题。它先把文本、图片、音视频这类非结构化数据交给 Embedding 模型转换成向量,再通过向量距离(余弦相似度、欧氏距离等)判断两个内容在语义上是否接近。这里的核心不是“有没有出现同样的词”,而是“两个向量在空间里的方向是否一致”。

1.2 Embedding 模型、向量索引和向量数据库的关系

很多人第一次上手 Chroma 时会混三个东西:Embedding 模型负责“把内容变成向量”,向量索引负责“让高维向量能被高速近似查找”,向量数据库负责“把向量、元数据、集合管理、持久化、API 整合到一起”。

你可以简单类比成:Embedding 是厨师切好的菜,向量索引是排好队的冷柜,向量数据库则是整个餐厅的后厨管理系统。日常开发里我们不需要亲手写 HNSW 算法,也不需要自己维护索引文件,这些脏活都由 Chroma、Milvus 这类产品包掉了。但如果你完全不懂底层表结构,一旦遇到“数据加了为什么查不到”“为什么文件占空间异常大”这类问题,排查起来就会很痛苦。

1.3 Chroma 在向量数据库生态里的位置

当前社区里热门向量数据库不少,常见的有 Chroma、Milvus、Qdrant、Weaviate、Pinecone 等。Chroma 最大的优势是轻量和本地优先,pip 安装后直接可以跑,非常适合个人知识库、AI 产品原型、本地小规模 RAG 场景。Milvus 则更偏生产级,支持分布式、海量数据,有独立的协调组件和存储层,部署运维复杂度也更高。

我做这个项目的时候选择 Chroma,原因主要有三条:

  • 数据量控制在几十万条之内,单机内存和磁盘足够覆盖;
  • 操作简单,集合、添加、查询的 API 对新手非常友好;
  • 持久化只依赖一个 SQLite 文件和索引目录,便于审计和搬移。

当然,轻量也意味着底层“黑盒”成分更多。所以如果你也想把它用到真实项目里,理解它落盘后的表结构,其实是绕不开的一道功课。

2. 真实项目里,Chroma 落库后到底变成什么样

2.1 最小可运行的添加集合示例

先说一个最小示例。我的持久化路径是./kb_data,所有数据都会落在那个目录里。

pip install chromadb
import chromadb client = chromadb.PersistentClient(path="./kb_data") collection = client.get_or_create_collection( name="employee_policy", metadata={"hnsw:space": "cosine"}, ) collection.add( ids=["doc-001", "doc-002"], documents=[ "员工每年享有15天带薪年假,需提前一周在系统提交申请。", "离职交接流程包括归还设备、转移权限、完成知识库文档更新。", ], metadatas=[ {"category": "leave", "owner": "hr"}, {"category": "offboarding", "owner": "it"}, ], ) print(collection.count())

运行后你会看到集合数量是 2。这个案例很常规,但打开持久化目录,能看到一大堆平时不会注意的产物。

2.2 持久化目录里有哪些“居民”

tree kb_data或者直接打开目录,正常情况下会看到类似下面的结构:

kb_data/ ├── chroma.sqlite3 └── index/ └── xxx-xxxx-xxx/ ├── data.level0 ├── header.bin └── ...

这里的chroma.sqlite3是 Chroma 的元数据和业务数据主库,各种“表”都住在里面。index/目录里则是真正干活的 HNSW 向量索引文件,目录名一般跟某个内部segment的 ID 对应。

很多人会有一个误解,以为添加集合后“多出的好多表”都是集合产生的。其实准确来说,Chroma 初始化持久化目录时就会把基础表建好,集合、索引这些是往这些表里插入记录。

2.3 用 sqlite3 直接查看表清单

在项目目录里执行:

sqlite3 kb_data/chroma.sqlite3 ".tables"

不同版本看到的表名会稍有差异,但主干通常包括下面的一组:

tenants databases collections collection_metadata segments segment_metadata embeddings embedding_metadata max_seq_id migrations

看到这一串后,建议先把敬畏心收起来。它不是让你写 SQL 去人工读写业务数据的,而是一套内部状态编排。Chroma 查询、删除、更新时靠这些表协同完成事务和索引同步。

3. 逐张拆解:Chroma 的每张表到底在干嘛

3.1 租户与库:tenants / databases

table 名称:tenantsdatabases

Chroma 参考了多租户数据库模型。tenants表保存租户信息,默认会有一条类似default_tenant的记录。databases表保存库信息,一条记录会挂在某个tenant_id下。初次使用时通常会生成default_database

表名核心字段(常见)含义
tenantsid, name租户
databasesid, name, tenant_id某个租户下的库

关联关系是:一个租户下可以有多个数据库,一个数据库可以被很多集合使用。

普通开发者平时根本不需要碰这两张表。但要注意:如果你在同一个PersistentClient路径里用tenantdatabase参数创建多租户环境,那么集合的归属关系就会拉长成“租户-库-集合”,查询时如果不指定正确上下文,可能找不到原来的集合。

3.2 集合相关:collections / collection_metadata

table 名称:collectionscollection_metadata

collections表是面向用户的核心入口。我们调用get_or_create_collection(name="employee_policy"),最终就是在collections表里插入一行,记录集合 ID、名称、所属数据库 ID 等信息。

collection_metadata表存储的是这个集合级别的元数据键值对。例如我在创建集合时传入的{"hnsw:space": "cosine"},就会以 key-value 的形式写入这里。Chroma 的元数据是类型化存储的,通常会有bool_valueint_valuefloat_valuestring_value这样的列,读取时再按类型还原。

表名常见字段说明
collectionsid, name, database_id, dimension, configuration_json集合主表
collection_metadataid, collection_id, key, string_value, int_value, float_value, bool_value集合级元数据 KV

这里有个很多新手踩过的细节:Chroma 里collection.metadata不是你传什么就原封不动存成 JSON。它会被拆成一行一行的键值记录,放进collection_metadata表。所以如果你用 SQL 直接改collections表里的字段,往往不会生效,因为真正读到的是元数据表里的值。

3.3 索引段相关:segments / segment_metadata

table 名称:segmentssegment_metadata

Chroma 内部对每个集合会创建“段”(segment),段是用来组织索引和存储的最小逻辑单元。你可以理解成:集合是业务概念,段是存储和索引概念。

创建集合后,segments表里通常至少会出现两类记录:

  • 向量段(VECTOR):负责管理高维向量的索引构建和查询;
  • 元数据段(METADATA):负责管理 embedding 对应的 metadata 过滤。

在 Chroma 的代码里,向量段的类型名一般会包含hnsw,因为默认的近似最近邻索引是 HNSW。

segment_metadata表和collection_metadata类似,存储的是段级元数据。例如 HNSW 的索引参数、算法配置,都有可能出现在这里。

表名常见字段说明
segmentsid, collection_id, type, scope, configuration_json段主表
segment_metadataid, segment_id, key, value段级元数据

需要特别强调的是,你在持久化目录index/[segment_id]下看到的二进制文件,和segments表里的记录是对应的。vector 数据写入后,Chroma 把这个向量段相关的索引写进了文件。所以如果你要手动备份或迁移,只复制chroma.sqlite3是不够的,必须把整个index目录一起带走。

3.4 向量与元数据落库:embeddings / embedding_metadata

table 名称:embeddingsembedding_metadata

这是业务数据真正“安身”的地方。embeddings表每一行对应一个向量记录,往往包含 ID、集合 ID、段 ID、序号、向量数据等字段。如果你之前添加了 100 条文档切片,这张表里就至少会有 100 条向量记录。

embedding_metadata表则保存每个向量对应的业务元数据。例如我上面示例里{"category": "leave", "owner": "hr"}这些内容,会被拆进这张表。还有一个细节:如果你用documents参数而不是metadatas参数传文本,Chromadb 内部其实会用chroma:document这个特殊的元数据 key 把原始文档内容存下来。这也就是为什么你在某些版本的 metadata 表里能看到一个 key 叫chroma:document的原因。

表名常见字段说明
embeddingsid, segment_id, collection_id, embedding, seq_id向量主体
embedding_metadataid, embedding_id, key, string_value/int_value...每个向量的元数据 KV

如果你直接在 SQLite 里查看embeddings表,会看到embedding这一列是一大段难以阅读的二进制或序列化内容。不要尝试手动改它,Chroma 和 HNSW 索引文件的同步关系非常微妙,一行改动很容易造成查询结果和索引不一致。

3.5 状态与版本辅助表:max_seq_id / migrations

table 名称:max_seq_idmigrations

Chroma 使用 WAL 或类似思想保证写入顺序和增量同步。max_seq_id表保存了每个段当前已经写入到哪条序号,下次写入时从这里继续累计。这保证了即使程序重启,也不会出现用旧序号覆盖新数据的问题。

migrations表则是数据库结构版本管理表。Chroma 升级时会执行一系列迁移脚本,更新表结构或必要数据,迁移记录就存在这张表里。如果某次升级了一半、或者你从旧版本直接拷贝数据文件到新版本,很可能因为迁移状态不一致导致启动失败。

这两张表看起来“无足轻重”,但千万不要为了清空数据而顺手删掉。清空max_seq_id可能导致 Chroma 认为后续写入还是旧事件,进而出现无法解释的数据丢失或重复问题。

4. 表之间的关联关系是怎么串起来的

4.1 从“租户-库-集合”到“集合-段-向量”的导航链

如果把主要表的主外键关系用文字拉出来,关系链是这样的:

tenants (1) -> databases (N) databases (1) -> collections (N) collections (1) -> segments (N) collections (1) -> embeddings (N) segments (1) -> embeddings (N) embeddings (1) -> embedding_metadata (N)

这个关系链看着复杂,但逻辑上是逐层下钻的:

  • 先确定你用的是哪个租户和库;
  • 库下面有哪些集合;
  • 集合下面有哪些索引段;
  • 向量数据挂在段下,同时也能通过集合直接筛选;
  • 每个向量又有自己的元数据表。

你可以把集合想象成“文件夹”,把段想象成“分卷压缩包”,把向量记录想象成压缩包里的文件。日常操作面向文件夹,但真正影响检索的是压缩包内部的一致性。

4.2 一次 add 操作,到底写了哪些表

拿最开始那段代码来拆解一次collection.add()。我用的是默认 embedding 函数,所以 Chroma 内部会先做文本向量化,然后再走写入链路。简化后的流程大致是:

  1. 根据name="employee_policy"collections表中找到集合记录;
  2. segments表里找到这个集合对应的向量段;
  3. 对每条文本生成向量;
  4. embeddings表写入一条向量记录,字段包括集合 ID、段 ID、向量数据等;
  5. documents里的文本和metadatas里的业务属性写进embedding_metadata
  6. 更新max_seq_id中对应段的序号;
  7. 将向量同步到index/目录下的 HNSW 索引文件。

这个过程并不是“只往一张大表里塞 JSON”,而是把集合信息、向量信息、元数据、索引状态分散到不同的表和文件中。这样设计的好处是查询时可以分头优化:先用 HNSW 索引快速召回候选向量,再回到 SQLite 里做元数据过滤,不需要全量扫描。

4.3 查询时它反过来怎么用表

执行collection.query(query_text="年假")时,链路大致是反向的:

  1. collections定位集合;
  2. segments找到向量段;
  3. 读入或复用index/下的 HNSW 索引文件;
  4. 将查询文本转成向量后,在索引文件里做 ANN 搜索;
  5. 得到一批候选 embedding ID;
  6. 使用 embeddings / embedding_metadata 表里的信息,对候选做元数据过滤、距离计算和排序;
  7. 返回最终结果。

理解这条链路后,你就明白为什么“改 SQLite 里的表”是危险操作了:查询结果并不只依赖embeddings表,还依赖index/目录下的 HNSW 索引。如果你绕过 Chroma 直接往 SQLite 插入一条向量,而索引文件没有同步,这条向量在搜索时大概率不会被召回。

5. 表相关实操排查与备份技巧

5.1 怎么安全地查看自己库里有哪些集合和数据量

很多“专家”会告诉你直接写 SQL:

SELECT id, name FROM collections; SELECT count(*) FROM embeddings; SELECT key, string_value FROM embedding_metadata LIMIT 20;

这些查询用来审计完全没问题。我在排查自己数据时也经常这样看。需要提醒的是:查询只读数据是安全的,但修改表内容或者直接 DELETE 记录必须避免。数据不一致造成的 bug,往往比业务代码 bug 更难查。

如果你只是想看“集合里有多少条数据”,不要依赖这个:

collection.count()

这个 API 读的是 Chroma 内部的状态,比你自己数embeddings表记录数可靠得多。实际排查时如果二者对不上,优先怀疑自己手动改过库,或者数据库版本迁移出了问题。

5.2 备份 Chroma 数据时最容易犯的错

我项目最早做备份时,只复制了chroma.sqlite3,结果恢复后一个集合都查不出来,后来才发现索引文件丢了。

正确的备份姿势是:

  1. 先正常关闭所有写入和查询的客户端;
  2. 复制整个持久化目录,包括chroma.sqlite3index/
  3. 如果怀疑文件被占用,可以先对目录做一次快速 rsync,等待无写入窗口后再复制一次,保证一致性。

临时导出少量数据可以使用collection.get(include=["documents","metadatas","embeddings"]),但全量迁移建议直接冷备目录。

5.3 表多了、目录大了怎么清理

如果你删除了测试用的集合,磁盘空间却依然很大,不要慌。Chroma 删除集合时会同步删除 SQLite 里的记录,并清理对应 segment 的索引文件。但 SQLite 文件本身不会马上把物理空间还给操作系统,这属于数据库正常表现。

可以定期做 VACUUM,但不是普通操作,更推荐直接重建持久化目录再导入数据。对生产数据来说,最稳妥的清理方案是:用代码导出需要保留的数据,删除旧目录,重建新目录,再重新写入。不要尝试手动清理segments表或collections表里的残留记录,很容易牵连出索引文件和元数据不一致。

5.4 升级后不可打开数据库怎么办

出现类似“数据库 migration 失败”的报错时,通常是migrations表记录的版本,比代码期望的版本旧或新。我的建议是:

  • 先看看migrations表里记录的版本号;
  • 确认备份是否存在;
  • 不要直接用旧版本强行打开新版本创建的数据目录;
  • 如果生产环境,先搭一套同版本环境做验证再升级。

6. 向前一步:Chroma 与 Milvus 的集合表设计差异

6.1 Chroma 简单但不意味着可以乱来

Chroma 的优点是开箱即用,把文档、集合、向量、元数据封装得比较简单。但这种简单只体现在 API 层,底层该有的一致性、缓存、索引同步一样不少。理解它的表结构,不是为了绕开 API,而是为了在遇到玄学问题时知道从哪里下手。

如果你准备用它承载生产流量,至少要做到:明确集合名与 embedding 模型版本、设置合适的 distance 类型、定期冷备整个持久化目录、所有写操作都走官方 API,绝不直接改库。

6.2 Milvus 里的 collection、partition、segment 和 Chroma 有哪些对应点

再往上看 Milvus,会发现概念有不少相似之处,但又更复杂:

  • Chroma 的 collection 对应 Milvus 的 collection,但 Milvus 的 collection 需要定义字段 schema;
  • Chroma 的 segment 是一个内部逻辑索引单元;Milvus 里的 segment 是数据实际落盘的最小单位,随时间增长会自动合并或分裂;
  • Milvus 还有 partition 概念,可以在一个 collection 下按业务维度做物理分区,让查询只扫部分分区,类似在 Chroma 里用 metadata 过滤,但 Milvus 把它放到存储层;
  • Milvus 的元数据存储通常依赖 etcd 和对象存储,不只是一两个 SQLite 文件。

如果你的数据量上涨到需要多节点、需要动态扩缩容、需要熔断和运维可观测性,那时候再迁 Milvus 比较合适。如果数据量在百万条以内、又希望本地跑轻量服务,Chroma 其实已经很能打了。

6.3 从 Chroma 迁移到 Milvus 时,表结构思维要怎么换

我自己做过一次把 Chroma 数据迁移到 Milvus 的验证,最直接的体感是:

  • 原来在 Chroma 里不用显式定义字段类型,到 Milvus 里要先把 collection schema 写清楚;
  • Chroma 的metadata是一个宽松 KV,迁到 Milvus 后最好映射成具体字段,否则过滤查询性能会不太好;
  • Chroma 的持久化是单机文件,Milvus 至少要考虑对象存储、消息队列、索引节点等多个组件;
  • 从 RAG 业务层看,接口差异不大,都是 add 和 query,但底层表结构已经完全不是一个物种。

如果你目前只是做学习项目,不建议一上来就上 Milvus。先把 Chroma 的集合和表结构弄明白,知道向量数据从写入到检索经过了哪些环节,会让你在后续选型和迁移时更有底气。

最后说一个我自己的体会:向量数据库的表结构再复杂,真正要守住的核心还是“数据与索引一致”。Chroma 之所以在 SQLite 之外还生成那么多辅助表,是为了让查询效率和灵活性达到平衡。你要做的,不是研究怎么绕过它,而是学会在它正常工作时不添乱、出问题时能准确判断是集合配置、元数据过滤、索引损坏还是版本迁移的问题。把这个能力练出来,后面的向量数据库之路会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 17:01:13

温控板定制开发全流程解析:从需求到交付的工程实践

1. 需求先行:先搞清楚温控板到底给谁用、控什么、怎么用很多人一上来就问“能不能帮我做一块温控板”,这话听着简单,实际上背后信息量少得可怜。做了十多年温控相关的定制开发,我个人的习惯是:接到需求的第一周不碰原理…

作者头像 李华
网站建设 2026/9/8 17:00:35

拆解RS 2kW电视广播放大器:400-800MHz功放内部结构与再利用

一台服役多年的R&S(罗德与施瓦茨)2千瓦电视广播放大器,频段覆盖400-800MHz,如今因为设备换代被整体报废拆下。这东西在广播发射机房里躺了十几年,外表已经锈迹斑斑,风扇口堆满灰尘,但打开机…

作者头像 李华
网站建设 2026/9/8 16:59:36

本地部署AI模型全攻略:硬件选型、量化与调优实战

1. 先别急着装:你的需求真的适合本地部署吗1.1 本地部署到底解决了什么问题过去两年我一直在反复折腾本地部署AI模型这件事。它不是一句"把模型下到电脑里跑"那么简单,背后是一整套取舍逻辑。我最初动手的原因很实际:公司项目里有大…

作者头像 李华
网站建设 2026/9/8 16:59:02

SRCNN超分辨率复现指南:基于TensorFlow的完整实现与PSNR调优

简介:基于Python与TensorFlow实现的SRCNN超分辨率重构代码包,定位为论文级复现工程,适合图像超分方向的研究者、学生以及需要直接训练或测试SRCNN模型的开发者。与网上多数实现相比,代码已避开数据预处理、训练细节中的常见坑点&a…

作者头像 李华