一、问题背景:商品图搜背后的向量检索压力

我们做的是跨境电商的以图搜图功能,商品库有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飙高。

五、踩坑与优化:别信官方默认配置

三个大坑,顺序排列:

  1. Pulsar内存泄漏:Milvus 2.3.3的Pulsar容器默认堆内存2GB,数据量上来后频繁FullGC。必须加-Xms8g -Xmx8gpulsar.env
  2. Qdrant的mmap模式:官方说“适合大数据”,但实测在5000万条时,mmap会导致查询延迟波动极大(从300ms到1.2s)。建议改用memmap+预加载,但会吃掉300GB内存。
  3. 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%,因为过滤场景和并发模型差异太大。如果你也在做类似选型,欢迎评论区交流。