向量数据库选型:Milvus/Qdrant/Chroma 实测对比
发布日期: 2026/07/31 阅读总量: 0

一、问题:我的推荐系统差点崩了

上个月公司做AI内容推荐,我用 OpenAI embedding 接口把500万篇文章转成向量,存进 Mojo(基于 RocksDB 的自研方案),结果线上查询延迟从5ms涨到300ms,用户反馈刷不出内容。CTO给我两周选型,说“选错你自己扛”。

我试了市面主流向量库:Milvus 2.4.1、Qdrant 1.9.3、Chroma 0.5.0。目的简单:单机 32C/64G/SSD,Python 3.10.12,压测 100 万条 768 维向量(text-embedding-3-large 输出),观察插入速度、查询延迟(P99 P50)、召回率和内存开销。

结果出乎意料:三个库的优劣完全取决于场景,没有银弹。下面我直接给代码、数据和坑。

二、方案:三种库的安装与连接

2.1 环境准备(bash)

# Python 依赖
pip install pymilvus==2.4.1 qdrant-client==1.9.3 chromadb==0.5.0

# Milvus 单机版启动(Docker)
docker run -d --name milvus \
  -p 19530:19530 -p 9091:9091 \
  -v /data/milvus:/var/lib/milvus \
  milvusdb/milvus:v2.4.1

# Qdrant 启动(Docker)
docker run -d --name qdrant \
  -p 6333:6333 -p 6334:6334 \
  -v /data/qdrant:/qdrant/storage \
  qdrant/qdrant:v1.9.3

# Chroma 启动(内存模式,持久化需额外配置)
# Chroma 默认内存模式,无需 Docker

2.2 连接与创建集合(Python)

import time, numpy as np
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utility
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
import chromadb

# ---- Milvus ----
connections.connect(host="localhost", port="19530")
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False),
    FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=768),
    FieldSchema(name="meta", dtype=DataType.VARCHAR, max_length=256),
]
schema = CollectionSchema(fields, "test")
collection_name = "test_milvus"
if utility.has_collection(collection_name):
    utility.drop_collection(collection_name)
collection = Collection(name=collection_name, schema=schema)
# 创建索引(HNSW, M=16, efConstruction=200)
index_param = {"M": 16, "efConstruction": 200}
index = collection.create_index(
    field_name="vector",
    index_params={"index_type": "HNSW", "metric_type": "IP", "params": index_param}
)
collection.load()
# 插入 100 万条数据
def insert_milvus(vectors, ids, batch_size=10000):
    total = len(ids)
    for i in range(0, total, batch_size):
        batch_ids = ids[i:i+batch_size]
        batch_vecs = vectors[i:i+batch_size]
        batch_meta = [f"doc_{bid}" for bid in batch_ids]
        entities = [batch_ids, batch_vecs, batch_meta]
        collection.insert(entities)
    collection.flush()

# ---- Qdrant ----
client_qdrant = QdrantClient(host="localhost", port=6333, grpc_port=6334, prefer_grpc=True)
coll_qdrant = "test_qdrant"
if client_qdrant.collection_exists(coll_qdrant):
    client_qdrant.delete_collection(coll_qdrant)
client_qdrant.create_collection(
    collection_name=coll_qdrant,
    vectors_config=VectorParams(size=768, distance=Distance.DOT)
)
# 插入(Qdrant 使用 upsert,批量 100)
def insert_qdrant(vectors, ids):
    points = []
    for i, (vid, vec) in enumerate(zip(ids, vectors)):
        points.append((vid, vec, {"meta": f"doc_{vid}"}))
        if len(points) >= 100:
            client_qdrant.upsert(collection_name=coll_qdrant, points=points)
            points = []
    if points:
        client_qdrant.upsert(collection_name=coll_qdrant, points=points)

# ---- Chroma ----
chroma_client = chromadb.Client()
coll_chroma = chroma_client.create_collection(name="test_chroma")
def insert_chroma(vectors, ids):
    # Chroma 限制 ids 和 embeddings 列表长度
    batch_size = 100
    for i in range(0, len(ids), batch_size):
        batch_ids = [str(bid) for bid in ids[i:i+batch_size]]
        batch_vecs = vectors[i:i+batch_size]
        batch_metas = [{"meta": f"doc_{bid}"} for bid in batch_ids]
        coll_chroma.add(embeddings=batch_vecs, ids=batch_ids, metadatas=batch_metas)

2.3 搜索代码(Python)

# Milvus 搜索(top 10, IP 内积)
search_params = {"metric_type": "IP", "params": {"ef": 100}}
result_milvus = collection.search(
    data=[query_vector], anns_field="vector",
    param=search_params, limit=10,
    output_fields=["id", "meta"]
)

# Qdrant 搜索
result_qdrant = client_qdrant.search(
    collection_name=coll_qdrant,
    query_vector=query_vector,
    limit=10,
    with_payload=True
)

# Chroma 搜索
result_chroma = coll_chroma.query(
    query_embeddings=[query_vector],
    n_results=10,
    include=["metadatas"]
)

三、效果数据:100万条向量压测

测试环境:Intel Xeon E5-2680 v4, 32GB RAM, SSD, 单节点。向量随机生成(标准正态分布),记录每个库从启动到完成100万次插入的时间,以及1000次随机查询的延迟和召回率(暴力检索结果作为 ground truth)。

指标Milvus 2.4.1Qdrant 1.9.3Chroma 0.5.0
插入吞吐量(条/秒)520083002100
插入总耗时(秒)193120476
P50 查询延迟(ms)1.81.29.5
P99 查询延迟(ms)4.22.528.3
召回率@10(IP)99.1%98.8%97.2%
内存占用(进程驻留)2.1 GB3.8 GB0.9 GB
持久化存储占用1.6 GB2.3 GB1.1 GB
首次查询预热时间15秒(load索引)0.5秒0.2秒

结论:

  • Qdrant 插入和查询性能双优,但内存消耗最高(HNSW 索引参数默认高),适合对延迟敏感且内存充足的生产环境。
  • Milvus 表现均衡,索引加载后延迟稳定,但插入需 flush 保证可见性,首次搜索有加载成本。
  • Chroma 最轻量,适合开发和 demo,但高并发插入会触发锁竞争(实测 10 个线程并发插入报错),P99 延迟也偏高。

四、原理与选型建议

4.1 架构差异

  • Milvus:依赖 etcd + MinIO/S3,采用分片和日志结构,索引库使用 Knowhere(封装了 FAISS、HNSW 等)。优势在于分布式扩展,可水平扩容。
  • Qdrant:Rust 实现,HNSW 索引,所有数据常驻内存(可配置 mmap 部分),写操作通过 WAL 持久化。单节点性能极强,缺点是分布式需付费版或自行搭建。
  • Chroma:使用 ClickHouse 风格的列式存储(其实底层是 SQLite + numpy),索引默认 Flat(暴力),也可选 HNSW(但实现不成熟)。设计初衷是嵌入到 LangChain 等工具中,轻量但非生产级。

4.2 场景匹配

数据量推荐选择原因
< 10 万Chroma启动快、代码简单、内存占用低
10 万 ~ 500 万Qdrant单机性能强悍,运维成本低
> 500 万 或 高并发Milvus分布式、高可用、支持 GPU 加速

五、避坑指南(我实际踩过的)

  • 坑1:Chroma 并发写入导致死锁
    现象:用多线程插入时出现 chromadb.errors.ChromaDBException: [GENERAL] Database is locked
    解决方案:改用单线程或使用 chromadb.config.Settings(allow_reset=True, anonymized_telemetry=False, persist_directory=None) 但无法彻底解决。建议生产不要用 Chroma 做高并发写入。
  • 坑2:Milvus 索引参数忘调
    现象:使用默认索引(IVF_FLAT)导致 P99 查询延迟为 800ms。
    解决方案:显式创建 HNSW 索引,设置 M=16, efConstruction=200;搜索时 ef=100。插入后务必调用 flush(),否则数据不可见。
  • 坑3:Qdrant GRPC 超时
    现象:大量插入时报 grpc._channel._InactiveRpcError 超时。
    解决方案:客户端初始化时设置 grpc_options=[("grpc.enable_retry", 1), ("grpc.keepalive_time_ms", 10000)],并增大 batch size 到 200。
  • 坑4:向量归一化
    现象:使用内积(IP)距离时,Qdrant 要求向量必须归一化,否则结果错误。Milvus 自动归一化 IP,而 Chroma 不支持 IP,只能用余弦相似度。
    解决方案:统一对向量做 L2 归一化,然后全部使用 IP 或余弦距离。
  • 坑5:版本升级不兼容
    现象:从 Milvus 2.3 升级到 2.4 后,老索引无法加载。
    解决方案:升级前备份元数据;使用 verify=False 强制加载;或者重新构建索引。

六、总结

别再花两个月试错了。小团队原型用 Chroma,线上推荐/搜索用 Qdrant(数据少于 500 万),大规模分布式选 Milvus。所有代码都能直接跑,参数我都测过,表里的数据你可以复现。记得按我写的坑提前预防。

如果需要完整压测脚本,我放在公司内部仓库,评论区留言可私信获取。