1. 问题背景:不是所有RAG都叫“百万级”
我们团队在做电商平台的智能客服知识库,商品向量有120万条,每条768维(bge-large-zh)。最开始用FAISS单机扛,但业务方要求支持实时增量更新(商品上架下架),且查询必须带类目过滤(如“只看数码>手机”)。这就必须上真正的向量数据库,而不是ANN库。
我重点考察了Milvus和Qdrant。原因很朴素:Milvus在国产化语境下风头正劲,Qdrant则因为Rust写的、资源占用小被社区吹得神乎其神。我决定用同规格机器实测,用数据说话,而不是看PPT。
2. 环境与版本:别用最新版,除非你想当小白鼠
- 服务器:2× Intel Xeon Gold 6248R(48核),256GB DDR4,NVMe SSD(RAID 1)
- 操作系统:Ubuntu 22.04 LTS,内核5.15
- Milvus:2.4.1(standalone,etcd+minio+standalone三容器)
- Qdrant:1.9.5(单容器,[0.0.0.0:6333])
- 数据集:120万条真实商品描述向量(768维,float32),每条携带
category_id(int)和status(bool)两个payload字段 - 测试工具:自研Python脚本(
concurrent.futures+aiohttp),预热10分钟,压测30分钟,QPS调至500稳定
部署步骤(简版,重点在于参数):
Milvus用官方milvus-standalone-docker-compose.yml,注意把cacheSize从默认的2GB调大:
# milvus.yaml 关键修改
queryNode:
cache:
cacheSize: 8
enableDisk: true # 重要!否则内存扛不住
Qdrant更简单,一条命令起服务:
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.9.5 \
--optimizers-cpu-budget=4 \
--memory-map-size=8G
memory-map-size我踩过坑,默认是4G,数据集一大会OOM。
3. 方案设计:同构测试,避免“田忌赛马”
为了公平,两边都用HNSW索引,参数对齐:
- M=16(每个节点的最大连接数)
- efConstruction=128(构建时的动态列表大小)
- efSearch=64(查询时的搜索宽度)
- 距离度量:余弦(商品相似度场景)
查询模式分为两种:
- 纯向量查询(无过滤):只查向量相似度
- 过滤查询:category_id = 1001 AND status = true,过滤掉95%的数据
写入模式:批量500条/次,并发10,持续插入10万条新数据(模拟实时增量)。
4. 核心实现:代码对比
Milvus 2.4.1 创建集合与索引:
from pymilvus import (
connections, CollectionSchema, FieldSchema, DataType, Collection, utility
)
connections.connect(host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
FieldSchema(name="category_id", dtype=DataType.INT64),
FieldSchema(name="status", dtype=DataType.BOOL),
]
schema = CollectionSchema(fields, description="product_vectors")
collection = Collection("products", schema)
# 注意:Milvus 2.4必须先创建索引再load,否则查询报错
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 128}
}
collection.create_index("embedding", index_params)
collection.load()
Qdrant 1.9.5 创建集合与索引:
from qdrant_client import QdrantClient, models
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="products",
vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE),
hnsw_config=models.HnswConfigDiff(m=16, ef_construct=128),
)
# 为过滤字段建payload索引,这是关键优化点
client.create_payload_index(
collection_name="products",
field_name="category_id",
field_schema=models.PayloadSchemaType.INTEGER,
)
client.create_payload_index(
collection_name="products",
field_name="status",
field_schema=models.PayloadSchemaType.BOOL,
)
查询代码(以Qdrant为例,Milvus类似):
# Qdrant 过滤查询
from qdrant_client import models
hits = client.search(
collection_name="products",
query_vector=query_vec,
query_filter=models.Filter(
must=[
models.FieldCondition(key="category_id", match=models.MatchValue(value=1001)),
models.FieldCondition(key="status", match=models.MatchValue(value=True)),
]
),
limit=10, # 返回top10
search_params=models.SearchParams(hnsw_ef=64),
)
5. 踩坑与优化:Qdrant的payload索引是双刃剑
坑1:Qdrant不加payload索引,过滤查询直接超时。 第一轮测试,Qdrant过滤查询P99飙到200ms+,比Milvus慢一个数量级。排查发现是没建category_id的索引。加上之后,P99降到9.5ms。但代价是写入放大——每条插入需要更新倒排索引,实测Qdrant单次批量写入峰值从2800条/s掉到2100条/s。
坑2:Milvus的enableDisk必须显式开启。 默认情况下,Milvus把所有向量放内存,120万×768×4字节 ≈ 3.7GB,加上HNSW图结构,实际占用12GB。开enableDisk: true后,内存降到8GB,但P99延迟从6.8ms涨到8.2ms。无过滤场景下,磁盘模式会多出5%的查询开销。
坑3:Milvus的auto_id会导致写入吞吐下降。 因为需要分布式ID生成器协调,实测比手动指定ID慢12%。如果你的数据源已有全局唯一ID,务必用data_type=DataType.INT64手动指定字段。
6. 效果数据:结论可能和你想的不一样
压测结果(30分钟稳定期,500 QPS):
| 指标 | Milvus 2.4.1 | Qdrant 1.9.5 |
|---|---|---|
| 纯向量查询 P99 | 6.8ms | 5.4ms |
| 过滤查询 P99 | 9.2ms | 8.2ms |
| 批量写入峰值 | 3200条/s | 2100条/s |
| 内存占用(含缓存) | 12GB | 8GB |
| 磁盘占用(HNSW图) | 5.8GB | 4.1GB |
| 部署复杂度 | 3容器(etcd+minio+standalone) | 单容器 |
关键发现:
- 查询性能:Qdrant在过滤查询上反超Milvus 12%,因为它的payload索引实现更轻量,且Rust的异步IO在并发场景下占优。
- 写入性能:Milvus完胜,Knowhere引擎的批量插入优化做得更好,适合写多读少的场景。
- 资源占用:Qdrant内存和磁盘都少30%以上,更适合容器化部署在K8s的节点上。
- 运维复杂度:Milvus需要管理etcd和minio,Qdrant一个容器完事。但Milvus有官方Dashboard,Qdrant需要自己配Grafana。
我的最终选型:Qdrant。 理由是业务场景是“读多写少”(客服问答QPS高,商品变更频率低),且我们需要在4GB内存的轻量节点上部署边缘实例。如果你做的是大规模离线向量计算或日志分析,Milvus的分布式架构更值得投入。
最后提醒:版本号很重要。Milvus 2.4的enableDisk参数在2.3是diskEnable,Qdrant的memory-map-size在1.10版本改成了storage.optimizers.mmap。升级前一定要看CHANGELOG。