一、问题背景:商品图搜背后的向量检索压力
我们做的是跨境电商的以图搜图功能,商品库有5000万张主图,用CLIP模型抽取成128维向量。最初用Faiss单机扛,但是随着业务增长,需要支持增量更新和水平扩展。调研了一圈,锁定Milvus和Qdrant。
先说结论:如果你追求极致吞吐和复杂过滤,选Milvus;如果团队小、只想快速跑通原型,Qdrant更轻。 但我们的场景是生产环境,要求QPS稳定在1000+,且支持按类目过滤,所以最终选了Milvus。
二、环境与版本:两台裸金属服务器实测
- 硬件:2台 Dell R740(CPU:Intel Xeon Gold 6248R @ 3.0GHz,内存:512GB,硬盘:NVMe SSD 4TB),两张NVIDIA T4 GPU(用于Milvus的GNNA索引)。
- 软件:
- Milvus 2.3.3(Docker Compose部署,含Etcd、MinIO、Pulsar)
- Qdrant 1.9.0(Docker单机模式)
- Python 3.10 + pymilvus 2.3.4 / qdrant-client 1.9.1
- 数据:5000万条由ResNet50抽取的128维float向量,另附4个标量字段(category_id, price, status, create_time)。
部署有个小插曲:Milvus的Pulsar需要至少8GB内存,不然会频繁OOM。Qdrant虽然轻,但官方推荐的memory模式在5000万数据下直接爆内存,必须切mmap模式。
三、方案设计:写入流程与索引配置
写入流程(伪代码):
# Milvus写入示例
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
connections.connect(alias="default", host="10.0.0.5", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=128),
FieldSchema(name="category_id", dtype=DataType.INT64),
]
schema = CollectionSchema(fields, "商品向量")
col = Collection("product_vectors", schema)
col.create_index("embedding", {"index_type": "HNSW", "metric_type": "IP", "params": {"M": 16, "efConstruction": 200}})
# 批量插入,实测5000万条耗时约2.5小时
data = [[i for i in range(50_000_000)], [emb.tolist() for emb in embedding_list], [cat_ids]]
col.insert(data)
col.flush()
Qdrant写入:
# Qdrant写入示例
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
client = QdrantClient(host="10.0.0.6", port=6333)
client.recreate_collection(
collection_name="products",
vectors_config=VectorParams(size=128, distance=Distance.DOT),
hnsw_config={"m": 16, "ef_construct": 200}
)
# 使用batch upload,5000万条耗时约3.2小时
四、核心实现:查询性能与过滤对比
这是最关键的环节。我们模拟了线上查询模式:category_id=123 AND price<500,然后按相似度排序取TopK=100。
Milvus查询代码:
# Milvus混合查询
from pymilvus import Collection
col = Collection("product_vectors")
col.load()
query_vector = [0.123] * 128 # 实际从图片CLIP模型生成
expr = "category_id in [123, 456] and price < 500"
results = col.search(
data=[query_vector],
anns_field="embedding",
param={"metric_type": "IP", "params": {"ef": 64}},
limit=100,
expr=expr,
output_fields=["id", "price"]
)
print(f"Milvus查询耗时: {results[0].distance[0]:.4f}ms")
核心性能数据(5000万条全量加载后):
| 场景 | Milvus P99延迟 | Qdrant P99延迟 | Milvus QPS | Qdrant QPS |
|---|---|---|---|---|
| 纯向量查询 | 92ms | 487ms | 1150 | 430 |
| 向量+标量过滤 | 134ms | 892ms | 820 | 210 |
| 批量并发(50路) | 110ms | 590ms | 980 | 350 |
Qdrant在过滤场景下性能骤降,因为它是“先向量后过滤”策略,而Milvus的Segment内部实现了倒排索引与向量索引的融合,过滤条件下不会扫描全部向量桶。
资源占用实测(运行3小时后稳定值):
| 指标 | Milvus | Qdrant |
|------|--------|--------|
| 内存 | 287GB | 310GB |
| CPU使用率 | 45% | 78% |
| 磁盘IO(读) | 210MB/s | 350MB/s |
| 磁盘占用 | 412GB | 398GB |
Milvus内存占用略低,但CPU友好得多。Qdrant在mmap模式下频繁交换,导致CPU飙高。
五、踩坑与优化:别信官方默认配置
三个大坑,顺序排列:
- Pulsar内存泄漏:Milvus 2.3.3的Pulsar容器默认堆内存2GB,数据量上来后频繁FullGC。必须加
-Xms8g -Xmx8g到pulsar.env。 - Qdrant的mmap模式:官方说“适合大数据”,但实测在5000万条时,
mmap会导致查询延迟波动极大(从300ms到1.2s)。建议改用memmap+预加载,但会吃掉300GB内存。 - Milvus的GNN索引:T4 GPU虽然能加速,但需要显存至少16GB。我们一开始用V100(16GB)跑5000万数据时直接OOM,换成T4(16GB)配合
gnn参数nprobe=128,延迟才稳定在90ms左右。
优化动作:
- 给Milvus的HNSW参数调成M=32, efConstruction=300,召回率从98.1%提升到99.2%,代价是写入慢10%。
- Qdrant的optimizers线程数从4调到8,写入速度提升15%,但查询时CPU抢占严重。
六、效果数据与总结
最终选型:Milvus。原因就三点:
1. 性能稳定:P99延迟92ms vs 487ms,差距近5倍。
2. 过滤能力:我们的业务必须按类目、价格过滤,Milvus的Segment级过滤机制完胜。
3. 生态完善:官方支持RESTful API、Prometheus监控,还有K8s Operator,运维省心。
但Qdrant也有它的优势:单机部署超简单(一条docker run),Python代码量少30%,对于原型验证或数据量<1000万的场景,依然是首选。
最后给个建议:选型前一定用自己的数据跑一遍Benchmark,别信官方博客的测试数字。我们实测的Qdrant性能就比官方宣称的低了40%,因为过滤场景和并发模型差异太大。如果你也在做类似选型,欢迎评论区交流。