从 Chroma 换成 Qdrant,我踩了 100 万向量的坑

Qdrant Chroma 数据量 Python 100
发布于 2026-06-12
1

我们非常重视原创文章,为尊重知识产权并避免潜在的版权问题,我们在此提供文章的摘要供您初步了解。如果您想要查阅更为详尽的内容,访问作者的公众号页面获取完整文章。

扫码阅读
手机扫码阅读

TL;DR

  • 选向量库不是在比功能,是把你的场景参数(数据量、查询复杂度、运维条件)套进决策框架
  • Chroma 是嵌入式帮手(零运维、<100 万向量最佳),Qdrant 是独立引擎(生产级、过滤不伤召回率)
  • 两段可运行 Python 代码已给出——今天就能在自己的数据上跑起来

你打开三篇向量数据库对比文章,关掉三篇。

不是写得太差——它们列的功能都对。但看完你依然不知道自己的项目该用 Chroma 还是 Qdrant。

问题出在问法上。"Chroma 和 Qdrant 哪个更好"——这是错的。

选型不是在比产品,是在比场景。

你的数据量多大?查询条件是简单标签还是复杂组合?有人帮你管服务器吗?

换个说法你就懂了:你不会问"轿车和货车哪个更好"——你问"我每天送两个孩子上学顺路买菜,选哪个"。

01选型决策矩阵

你的情况
选谁
原因
个人项目 / 快速原型 / pip install 就完事
Chroma
零运维,和 Python 脚本共享进程
数据量 < 100 万向量
两个都行
这个量级你感知不到差异
数据量 > 100 万向量
Qdrant
Chroma 在这个规模开始吃力
查询经常带复杂过滤(时间 + 标签 + 状态...)
Qdrant
过滤不伤召回率
预算紧 + 数据量大(TB 级)
Chroma
S3 比纯内存便宜 250 倍
生产环境 / 需要高可用
Qdrant
Raft 共识 + 水平分片 + 自动修复

我自己最早做 RAG 原型的时候,无脑选了 Chroma——就图个 pip install 省事。后来一个项目数据量涨到 200 万条,检索从 50ms 漂到 800ms,排查了半天才发现不是 embedding 模型的问题,是 Chroma 在数据量过阈值之后合并层扛不住了。换成 Qdrant,延迟回到 40ms 以内。所以上面那个 100 万的阈值不是我从哪抄的——是真的交过学费。

还是不确定?往下看。

02同一个操作,两种活法

装包:

pip install chromadb qdrant-client 

Chroma 版:跟你的 Python 脚本住在一起

import chromadb  # 不需要启动任何独立服务——Chroma 活在你的 Python 进程里 client = chromadb.Client()  # 建集合,384 维向量 collection = client.create_collection(  name="my_docs",  metadata={"hnsw:space": "cosine"} # 余弦相似度 )  # 塞数据——文本和向量一起存 collection.add(  documents=[  "Python 的 asyncio 在 3.11 后性能提升明显",  "Qdrant 用 Rust 写的,GIL 管不着它",  "Chroma 默认用 HNSW 做索引",  ],  metadatas=[  {"topic": "python", "year": 2024},  {"topic": "vector-db", "year": 2025},  {"topic": "chroma", "year": 2024},  ],  ids=["doc_1", "doc_2", "doc_3"], )  # 语义搜索 results = collection.query(  query_texts=["向量检索怎么加速?"],  n_results=2, )  for i, doc_id in enumerate(results["ids"][0]):  doc = results["documents"][0][i]  distance = results["distances"][0][i]  print(f"  [{doc_id}] {doc} (距离: {distance:.3f})") 

没启动任何服务,没配任何网络,没写任何 docker-compose。这就是嵌入式数据库——它就是你程序里 import 的一个库,像 SQLite 一样活在你的进程里。

Qdrant 版:独立生活,专人专事

from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct import numpy as np  # 先启动 Qdrant 服务(docker 或 cloud) # docker run -p 6333:6333 qdrant/qdrant client = QdrantClient(host="localhost", port=6333)  # 建集合时就要声明向量维度和距离算法 client.create_collection(  collection_name="my_docs",  vectors_config=VectorParams(size=384, distance=Distance.COSINE), )  # 插入数据——每条是一个 Point client.upsert(  collection_name="my_docs",  points=[  PointStruct(  id=1,  vector=np.random.rand(384).tolist(), # 真实项目换成 embedding 输出  payload={"topic": "python", "year": 2024,  "text": "Python 的 asyncio 在 3.11 后性能提升明显"},  ),  PointStruct(  id=2,  vector=np.random.rand(384).tolist(),  payload={"topic": "vector-db", "year": 2025,  "text": "Qdrant 用 Rust 写的,GIL 管不着它"},  ),  PointStruct(  id=3,  vector=np.random.rand(384).tolist(),  payload={"topic": "chroma", "year": 2024,  "text": "Chroma 默认用 HNSW 做索引"},  ),  ], )  # 语义搜索——写法一样简单,但背后是独立查询引擎 results = client.search(  collection_name="my_docs",  query_vector=np.random.rand(384).tolist(),  limit=2, )  for hit in results:  print(f"  [{hit.id}] {hit.payload['text']} (score: {hit.score:.3f})") 

表面看,两段代码做的事差不多。背后是完全不同的故事。

Chroma 在你的 Python 进程里开了一个 Rust 运行时(Tokio),把计算密集的向量距离扔给 Rust Worker 线程,绕过 Python GIL。数据在进程内零拷贝传递。

Qdrant 是一个独立服务进程。你的 QdrantClient 通过 HTTP/gRPC 跟它说话。它自己管内存、管磁盘、管索引——你的 Python 程序挂了,Qdrant 还在跑,数据不丢。

Chroma 是住在你家客房的帮手。Qdrant 是隔壁开了间办公室的团队。

这个区别不神秘——你用过 SQLite 和 PostgreSQL 就秒懂。

03同样跑查询,里面在做什么?

当你执行 .query().search() 时,两个库的内部动作完全不同。不一定每天用得到,但它决定了你的查询在什么条件下会变慢。

Chroma:推模式

Chroma 把查询计划拆成微小数据切片(Morsels),给多个 Worker 并行算。同时做两件事:

  1. 已建索引的数据 → HNSW 图遍历
  2. 还没建索引的新数据 → 暴力扫描

两边结果在位图合并层汇合后返回。

好处:刚插入的数据立刻能被搜到——不用等索引构建。做 Agent 记忆、频繁增删数据的场景下,体验很好。

代价:合并层的开销依赖 Python HTTP 客户端的封包效率。数据量超过 100 万向量 时,这个层的压力开始压不住。

Qdrant:分段隔离

Qdrant 把数据拆成多个独立"段"(Segment)。每个段有自己的向量存储、元数据索引和 HNSW 图。写入先记 WAL(预写日志),再应用到对应段——断电了 WAL 帮你还原。

段分两种:可追加的(新数据往里写)和不可追加的(后台默默合并、重建索引)。前台查询和后台合段是隔离的——你不会因为索引重建而感觉查询变卡。

用一个类比来记:

Chroma 像开放式厨房——厨师同时切菜、炒菜、装盘,顾客看到全过程。菜出了直接端走。效率高,但客人一多就乱。

Qdrant 像分区仓库——进货区、存储区、出货区物理隔离。入库有登记簿(WAL),出库不影响理货。吞吐量大,但前期基建投入多。

我踩过一个经典的坑:Flask 服务里跑了 Chroma 做知识库检索,读写都在同一条线上。高峰期用户一多,GIL 把向量计算和 HTTP 响应绑在一起,整条链路都在等。后来拆成"Qdrant 独立服务 + Python 只做业务逻辑",才消停了。根因不是 Chroma 不行,是我没做好读写隔离——但 Chroma 的嵌入式设计确实让这种错误更容易犯。

04什么时候会翻车

两个库都有明确的退化边界。知道这些数字,比背一百行功能对比有用。

Chroma 的软肋:100 万向量

基准测试显示,Chroma 在这个量级附近开始出现明显性能退化。不是突然挂——延迟开始不稳定,有时候 20ms 返回,有时候 200ms

原因在架构层:Python 和 Rust 之间的跨语言通信在大数据量时成瓶颈,元数据连接的内存膨胀也会触发 GC 抖动。

100 万向量 是什么概念?按每条文本 500 token、embedding 维度 768 来算,大约是 10 万篇中等长度文章。大多数个人项目和中小团队,离这个天花板还远。

Qdrant 的"大材小用"

Qdrant 在 5000 万向量 规模下依然稳定在 ~41 QPS(99% 召回率),靠三件套:Rust SIMD 指令集 + 分段隔离 + TurboQuant 量化压缩。

但如果你只有 5 万个向量,Qdrant 的优势你完全感受不到。反倒多了一个要维护的 Docker 容器。

菜市场买菜开了辆重卡——不是车不好,是场景不对。

查询过滤:最容易被忽略的杀手

假设你存了 50 万条文档,每条 5 个标签。你做语义搜索时还要过滤掉 2023 年之前的、只保留某些话题。

Chroma 的做法是先搜再过滤,或者先过滤再搜。前者精度可能不够(搜出来的被过滤掉后不够数),后者在条件复杂时性能衰减。

Qdrant 的 Filterable HNSW 直接在 HNSW 图遍历时应用位图掩码——语义搜索和条件过滤同时完成,互不干扰。

你的 WHERE 子句越复杂,越该考虑 Qdrant。

05所以你该怎么选

回到场景-需求框架。四个问题:

  • 数据量:一年内向量存量会超过 100 万吗?
  • 查询复杂度:过滤条件是简单标签还是多字段组合?
  • 运维条件:你愿意维护一个独立服务吗?
  • 预算:大规模场景下,RAM 成本敏感吗?

把你的答案套进去:

数据量 < 100 万 且 过滤简单 → Chroma
数据量 < 100 万 且 过滤复杂 → Qdrant
数据量 > 100 万               → Qdrant(几乎必选)
预算极紧 + 数据量超大           → Chroma(S3,250 倍成本差)
做 Agent / 上下文管理          → Chroma(原生支持更好)

我现在自己的做法:新项目一律 Chroma 起步,快速验证想法。等到数据量和查询复杂度真的摸到阈值了,再迁 Qdrant——到那时候,迁移的理由是数据告诉我的,不是别人推荐的。这个思路也送给你:别提前优化,但也别不知道边界在哪。

代码都给你了,今天就能跑。两个库都装上,拿你自己的数据试一圈,比看完十篇对比文章更清楚自己该选谁。

数据STUDIO

点击领取《Python学习手册》,后台回复「福利」获取。『数据STUDIO』专注于数据科学原创文章分享,内容以 Python 为核心语言,涵盖机器学习、数据分析、可视化、MySQL等领域干货知识总结及实战项目。

158 篇文章
浏览 204K

还在用多套工具管项目?

一个平台搞定产品、项目、质量与效能,告别整合之苦,实现全流程闭环。

加入社区微信群
与行业大咖零距离交流学习
PMO实践白皮书
白皮书上线