把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,主要原因有几个:

  1. 性能确实好:Qdrant 用 Rust 写的,内存管理比 Chroma 的 Python 实现更可控,而且它的 HNSW 实现经过了优化。
  2. 功能更完整:有批量导入、索引管理、查询过滤、监控接口,而且支持分布式部署。
  3. 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

一些不成熟的判断

这次迁移走完,对向量数据库有了些新的理解:

  1. 没有银弹:Chroma 和 Qdrant 各有适用场景。数据量小、快速验证想法时 Chroma 很方便;数据量大、对性能有要求时 Qdrant 更合适。

  2. 参数调优很重要:HNSW 索引的参数不是随便设的,需要根据数据特征和查询模式做针对性调整。

  3. embedding 模型的影响被低估了:花了很多时间调数据库,但换个更好的 embedding 模型,效果提升可能比数据库优化还明显。

  4. 监控和运维成本不能忽视:向量数据库不只是个存储,在生产环境中需要考虑备份、监控、容错等问题。


收尾

迁移完成后,系统的查询性能确实好了不少,用户体验也改善了。但向量数据库只是 RAG 系统的一环,前面有 embedding 模型,后面有检索重排和答案生成,每个环节都需要仔细优化。

向量搜索的本质是"在海量数据中快速定位相关信息",这个需求在 LLM 时代变得更加重要。但技术选型不是追求最先进的工具,而是找到适合自己场景的那个。

如果你也在做类似的系统,建议先从简单的方案开始,等业务和数据量上来后再考虑更复杂的优化。毕竟工具是为了解决问题,不是为了展示技术。


参考资源

版权声明: 本文首发于 指尖魔法屋-把Chroma换到Qdrant时踩过的坑https://blog.thinkmoon.cn/post/141-vector-database-chroma-qdrant-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!