把Chroma换到Qdrant时踩过的坑
去年做个 RAG 系统时,我以为选个向量数据库是件小事。
项目早期用 Chroma 确实方便,pip install 就能跑起来。
起因:一个 RAG 系统的性能瓶颈
项目需求很简单:把公司的技术文档、会议纪要、邮件记录都导入向量数据库,让用户可以自然语言提问得到相关内容。最开始选 Chroma 是因为文档写得清楚,而且它的 Python API 很直观:
import chromadb
from chromadb.utils import embedding_functions
# 初始化客户端,默认把数据存到本地
client = chromadb.PersistentClient(path="./chroma_data")
# 用 OpenAI 的 embedding 模型
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key="your-api-key",
model_name="text-embedding-3-small"
)
collection = client.get_or_create_collection(
name="documents",
embedding_function=openai_ef
)
# 添加文档
collection.add(
documents=["这是一份技术文档..."],
metadatas=[{"type": "tech", "date": "2026-01-15"}],
ids=["doc_001"]
)
开发阶段一切正常,查询也很快。直到把真实的 12 万条文档导入后,问题才暴露出来:
# 查询延迟飙升
results = collection.query(
query_texts=["如何处理向量数据库的性能问题?"],
n_results=5
) # 这个调用从 50ms 涨到了 800ms
第一反应是 embedding 模型太慢,但加了日志后发现瓶颈在数据库本身的查询。用 cProfile 跑了一遍,确实大部分时间花在向量相似性计算上。
问题分析:Chroma 的限制在哪里
Chroma 有个设计是默认把所有数据加载到内存,方便快速查询。这在数据量不大时很方便,但我们的数据量已经超出了单机内存的舒适区。
另一个问题是 Chroma 的索引方式。它默认用 HNSW(Hierarchical Navigable Small World)索引,但参数调优空间有限,而且它的实现和底层向量计算库的耦合度比较高,不方便做底层优化。
还有一个实际问题是部署。Chroma 提供了服务模式,但它的 HTTP API 相对简单,缺少一些高级功能比如批量操作、权限控制、监控指标。对生产环境来说,这些功能迟早会需要。
迁移方案:为什么选 Qdrant
调研了一圈后选了 Qdrant,主要原因有几个:
- 性能确实好:Qdrant 用 Rust 写的,内存管理比 Chroma 的 Python 实现更可控,而且它的 HNSW 实现经过了优化。
- 功能更完整:有批量导入、索引管理、查询过滤、监控接口,而且支持分布式部署。
- API 设计合理:HTTP API 和 gRPC 都有,而且查询语法更灵活。
另外有个实际原因是 Qdrant 的 Docker 部署很简单,一条命令就能跑起来:
docker run -p 6333:6333 qdrant/qdrant:latest
数据迁移:不是简单的 export/import
迁移过程中踩的第一个坑是数据格式。Chroma 存的数据结构是自己的,Qdrant 虽然也有导入导出功能,但格式不直接兼容。
写了个迁移脚本,先把 Chroma 的数据导出成 JSON,再导入到 Qdrant:
import chromadb
from qdrant_client import QdrantClient, models
import numpy as np
import json
# 从 Chroma 导出
chroma_client = chromadb.PersistentClient(path="./chroma_data")
collection = chroma_client.get_collection(name="documents")
# 获取所有数据
all_data = collection.get(include=["documents", "metadatas", "embeddings"])
# 导入到 Qdrant
qdrant_client = QdrantClient(url="http://localhost:6333")
# 创建 collection,使用 HNSW 索引
qdrant_client.create_collection(
collection_name="documents",
vectors_config=models.VectorParams(
size=1536, # OpenAI text-embedding-3-small 的维度
distance=models.Distance.COSINE,
hnsw_config=models.HnswConfigDiff(
m=16, # 每个节点的连接数
ef_construct=100 # 构建时的搜索范围
)
)
)
# 批量插入
batch_size = 100
for i in range(0, len(all_data['ids']), batch_size):
batch_ids = all_data['ids'][i:i+batch_size]
batch_embeddings = all_data['embeddings'][i:i+batch_size]
batch_documents = all_data['documents'][i:i+batch_size]
batch_metadatas = all_data['metadatas'][i:i+batch_size]
qdrant_client.upsert(
collection_name="documents",
points=[
models.PointStruct(
id=batch_ids[j],
vector=batch_embeddings[j],
payload={
"text": batch_documents[j],
**batch_metadatas[j]
}
)
for j in range(len(batch_ids))
]
)
这个脚本跑了大概 2 小时,中间还踩了个坑:Chroma 的 embedding 是 numpy 数组,Qdrant 需要的是普通 Python list,直接转换会有类型问题。用 tolist() 转一下就行,但一开始没注意到这个细节,报错时花了些时间排查。
索引调优:参数不是随便设的
Qdrant 的 HNSW 索引有几个关键参数需要根据数据量和查询模式调整:
m:每个节点连接的邻居数,值越大查询越准但内存占用越高ef_construct:构建索引时的搜索范围,值越大索引质量越好但构建时间越长ef:查询时的搜索范围,影响查询精度和速度的平衡
我一开始直接用默认参数,结果查询精度不够。调整参数后性能有明显改善:
qdrant_client.create_collection(
collection_name="documents",
vectors_config=models.VectorParams(
size=1536,
distance=models.Distance.COSINE,
hnsw_config=models.HnswConfigDiff(
m=32, # 增加连接数提升查询精度
ef_construct=200, # 提高构建质量
ef=128 # 查询时搜索更多候选
)
)
)
这里有个经验:m 设为 16-32、ef_construct 设为 100-200、ef 设为 64-128 是一个比较常见的区间,但最好针对自己的数据做 A/B 测试。
查询优化:不只是改参数
迁移到 Qdrant 后,查询延迟从 800ms 降到了 150ms,但还是不够理想。从几个方向做了优化:
1. 减少返回字段
最初查询时把所有 payload 都返回了,但实际上只需要文本内容:
# 优化前,返回所有 payload
results = qdrant_client.search(
collection_name="documents",
query_vector=embedding,
limit=5
)
# 优化后,只返回需要的字段
results = qdrant_client.search(
collection_name="documents",
query_vector=embedding,
limit=5,
with_payload=["text", "type"] # 只返回这两个字段
)
2. 使用过滤条件
有些查询只需要特定类型的数据,用过滤能大幅减少搜索空间:
# 只查技术文档
results = qdrant_client.search(
collection_name="documents",
query_vector=embedding,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="type",
match=models.MatchValue(value="tech")
)
]
),
limit=5
)
3. 批量查询
如果有多个相关问题需要查询,用 batch query 比分别查要快:
# 批量查询多个问题
embeddings = [get_embedding(q) for q in questions]
results = qdrant_client.search_batch(
collection_name="documents",
query_vectors=embeddings,
limit=5
)
这些优化加起来,查询延迟最终降到了 60-80ms 左右,基本能满足业务需求。
部署和监控:生产环境需要的不是只有查询
迁移完成后,还需要考虑部署和监控的问题。
Qdrant 提供了多个监控端点,可以通过 Prometheus 抓取:
# 查询指标
curl http://localhost:6333/metrics
常用指标包括:
qdrant_collections_total_vector_count:总向量数量qdrant_collections_total_points_count:总文档数量qdrant_search_requests_total:查询请求总数qdrant_search_latency_seconds:查询延迟
部署方面,用 Docker Compose 可以快速搭建一个包含 Qdrant 和监控的完整环境:
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- ./qdrant_storage:/qdrant/storage
environment:
- QDRANT__SERVICE__GRPC_PORT=6334
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
depends_on:
- qdrant
一些不成熟的判断
这次迁移走完,对向量数据库有了些新的理解:
没有银弹:Chroma 和 Qdrant 各有适用场景。数据量小、快速验证想法时 Chroma 很方便;数据量大、对性能有要求时 Qdrant 更合适。
参数调优很重要:HNSW 索引的参数不是随便设的,需要根据数据特征和查询模式做针对性调整。
embedding 模型的影响被低估了:花了很多时间调数据库,但换个更好的 embedding 模型,效果提升可能比数据库优化还明显。
监控和运维成本不能忽视:向量数据库不只是个存储,在生产环境中需要考虑备份、监控、容错等问题。
收尾
迁移完成后,系统的查询性能确实好了不少,用户体验也改善了。但向量数据库只是 RAG 系统的一环,前面有 embedding 模型,后面有检索重排和答案生成,每个环节都需要仔细优化。
向量搜索的本质是"在海量数据中快速定位相关信息",这个需求在 LLM 时代变得更加重要。但技术选型不是追求最先进的工具,而是找到适合自己场景的那个。
如果你也在做类似的系统,建议先从简单的方案开始,等业务和数据量上来后再考虑更复杂的优化。毕竟工具是为了解决问题,不是为了展示技术。
参考资源
版权声明: 本文首发于 指尖魔法屋-把Chroma换到Qdrant时踩过的坑(https://blog.thinkmoon.cn/post/141-vector-database-chroma-qdrant-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。