1. 问题背景:不是所有向量库都适合你的业务
我们团队在做一个电商平台的“以图搜图”功能升级,商品库从原来的10万暴涨到100万,且每天增量约5万。原来的方案是Elasticsearch + dense_vector插件,但查询延迟在数据量上去后从80ms飙升到400ms,召回率也掉得厉害。于是我们开始评估专用向量数据库。
选型时我们锁定了两个候选:Milvus和Qdrant。为什么是这两个?因为社区活跃度高、文档相对完善、且都支持Python原生接口。但网络上的对比文章大多停留在“Milvus功能全、Qdrant轻量”这种层面,缺乏同环境下的量化对比。本文就用100万条真实商品标题的768维向量(来自Sentence-BERT)做一次彻底的实测。
2. 环境与版本:统一硬件,避免“田忌赛马”
测试机器是公司的一台闲置物理机:Intel Xeon Gold 6248R(16核分配)、64GB内存、NVMe SSD 1TB。操作系统为Ubuntu 22.04 LTS,Docker版本24.0.5,Python 3.10.12。
- Milvus:v2.4.1(standalone模式,用docker-compose启动)
- Qdrant:v1.9.2(单节点模式,同样用Docker)
关键配置参数:
- Milvus:config.yaml中设置cache.cacheSize: 4096(MB),索引类型为HNSW(M=16, efConstruction=200)
- Qdrant:config.yaml设置quantization: scalar(量化加速),hnsw_config: m: 16, ef_construct: 200
注意:两者都关闭了日志持久化,避免IO干扰测试结果。
3. 方案设计:从部署到压测的完整链路
我们的测试分三部分:
1. 部署对比:记录从拉取镜像到服务可用的总耗时、磁盘占用、初始内存占用。
2. 写入性能:用Python脚本分批插入100万条向量,batch_size=256,记录总耗时和QPS。
3. 查询性能:模拟真实业务——按“品牌+价格区间”过滤(过滤后剩余30%数据),再执行TopK=10的向量检索。压测工具使用locust(开100个并发用户,持续10分钟)。
为什么强调过滤查询?因为电商场景几乎不可能做纯向量检索,必须配合标量过滤。而Qdrant官方宣称其“Payload索引”比Milvus的“标量过滤”更高效,这需要实测验证。
4. 核心实现:部署命令与压测代码
4.1 Milvus部署(Docker Compose)
# docker-compose-milvus.yml
version: '3.5'
services:
etcd:
image: quay.io/coreos/etcd:v3.5.5
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
command: minio server /minio_data
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
standalone:
image: milvusdb/milvus:v2.4.1
command: ["milvus", "run", "standalone"]
ports:
- "19530:19530"
- "9091:9091"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
depends_on:
- etcd
- minio
4.2 Qdrant部署(单行命令)
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.9.2
4.3 Python压测脚本核心片段
# 写入性能测试
from pymilvus import Collection, connections
from qdrant_client import QdrantClient
import numpy as np
import time
def write_test_milvus(vectors, labels):
connections.connect(host='localhost', port='19530')
collection = Collection('product_search')
start = time.time()
for i in range(0, len(vectors), 256):
batch = vectors[i:i+256]
batch_labels = labels[i:i+256]
collection.insert([batch, batch_labels])
collection.flush()
elapsed = time.time() - start
qps = len(vectors) / elapsed
print(f"Milvus write QPS: {qps:.1f}, total: {elapsed:.2f}s")
def write_test_qdrant(vectors, labels):
client = QdrantClient(host='localhost', port=6333)
start = time.time()
for i in range(0, len(vectors), 256):
batch = vectors[i:i+256]
ids = list(range(i, i+len(batch)))
payloads = [{"label": label} for label in labels[i:i+256]]
client.upsert(
collection_name='product_search',
points=zip(range(i, i+len(batch)), batch, payloads)
)
elapsed = time.time() - start
qps = len(vectors) / elapsed
print(f"Qdrant write QPS: {qps:.1f}, total: {elapsed:.2f}s")
5. 踩坑与优化:那些文档没告诉你的
Milvus的索引构建是个坑。我们用默认参数插入100万条后,直接建HNSW索引耗时11分23秒,而且期间查询直接被阻塞(因为默认create_index是同步的)。后来改用create_index(..., index_name="hnsw", params={"M": 16, "efConstruction": 200, "sync": False})异步构建,但注意此时查询会走暴力搜索(flat),延迟飙到150ms。优化方案:预先创建集合时指定索引,然后分批插入,最后再异步构建索引。
Qdrant的Scalar量化真香。默认开启后,向量内存占用从768*4字节降到768字节(1字节/维),磁盘占用也降了75%。但代价是召回率从0.98降到0.94(用recall@10评估)。在电商搜索场景,这个召回损失可以接受,但如果做人脸识别建议关掉。
内存对比悬殊。用docker stats监控,Milvus稳定在14.2GB,其中9GB是页缓存(被cache.cacheSize控制),Qdrant只有6.8GB。但Milvus多出来的内存换来的是更高的写入吞吐。
6. 效果数据:实测结果一览
| 指标 | Milvus 2.4.1 | Qdrant 1.9.2 |
|---|---|---|
| 部署耗时 | 6分30秒(需拉etcd、minio) | 1分12秒(单镜像) |
| 磁盘占用 | 3.2GB(含etcd+minio数据) | 2.1GB |
| 写入QPS(batch=256) | 8,240 | 3,570 |
| 纯向量查询P99(无过滤) | 21ms | 18ms |
| 过滤查询P99(30%过滤率) | 19.4ms | 12.1ms |
| 峰值内存 | 14.2GB | 6.8GB |
| TopK=10召回率(默认参数) | 0.98 | 0.97(有量化时0.94) |
结论:如果你的业务是高频写入+低并发复杂查询(如数据管道),选Milvus;如果查询占比极高且对延迟敏感(如在线搜索),Qdrant更合适。我们最终选择了Qdrant,因为电商搜索是读多写少,且Qdrant的标量过滤机制(基于HNSW的payload索引)在带过滤的查询上优势明显。
7. 总结与建议
向量数据库没有银弹,选型一定要基于自己的真实数据跑benchmark。我踩过的坑包括:Milvus的同步索引构建导致查询假死、Qdrant量化参数对召回率的影响。最后给两点建议:
1. 压测时至少用100万条以上数据,10万条级别的测试结果没有参考价值。
2. 关注过滤查询性能,纯向量检索场景两个库差异不大。
如果你们也在做选型,建议直接复制我的测试脚本,换自己的数据跑一遍。有不同结论的欢迎评论区交流。