一、问题背景:不是所有向量库都适合你的业务
我们团队负责一个跨境电商平台的语义搜索模块,需要将商品标题、描述转为向量并支持实时召回。最初用的是自建的FAISS + Redis缓存方案,但随着数据量涨到500万条,问题开始暴露:索引全量加载内存不够、增量更新要重建索引、过滤条件(如价格区间、类目)完全没法做。
于是我们启动了向量数据库选型。候选名单里有Milvus、Qdrant、Weaviate、Chroma,但最终锁定在Milvus和Qdrant之间——因为这两者都支持复杂的标量过滤和混合搜索,且社区活跃度最高。
我们的核心需求很明确:
- 500万条512维float向量
- 支持商品ID、价格、类目等标量字段的预过滤
- 写入吞吐要求不高(每天约10万增量),但查询P99需低于50ms
- 部署环境为单机,32G内存,SSD磁盘
二、环境与版本:把底牌先亮出来
测试环境统一使用同一台物理机,避免硬件差异影响判断:
CPU:Intel Xeon Gold 6248R @ 3.0GHz (16核分配)
内存:32GB DDR4
磁盘:NVMe SSD 1TB
OS:Ubuntu 20.04.6 LTS
Docker:25.0.3
向量数据库版本(均为各自最新稳定版):
- Milvus:2.4.1,部署方式为Docker Compose(含etcd、minio、standalone)
- Qdrant:1.9.2,部署方式为单节点Docker容器
数据集:从商品库中随机抽取500万条真实数据,使用bge-large-zh-v1.5模型生成512维向量,标量字段包括item_id(uint64)、price(float)、category(string)、status(int)。
三、方案设计:部署与参数配置的第一次博弈
3.1 Milvus部署
Milvus 2.x的部署比1.x复杂不少,因为它拆成了多个组件。官方推荐的standalone模式需要三个容器:etcd(元数据)、minio(存储)、milvus-standalone(计算)。docker-compose.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
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
standalone:
image: milvusdb/milvus:v2.4.1
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
ports:
- "19530:19530"
启动后需要设置索引参数。我们用的是HNSW索引,M=16(每个节点的最大连接数),efConstruction=200(构建时的动态列表大小)。这两个参数直接影响召回率和内存占用。
3.2 Qdrant部署
Qdrant的部署就简单多了,一个容器搞定:
docker run -d \
--name qdrant \
-p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.9.2
Qdrant的索引配置在创建collection时通过vectors和hnsw_config指定。它的HNSW参数和Milvus基本类似,但有一个额外的m参数(每个节点的最大连接数)和ef_construct。
四、核心实现:500万向量写入与查询实测
4.1 Milvus写入与索引构建
写入500万条数据用了约40分钟,主要是由于我们开启了sync=True(每次写入都强制落盘)。实际生产环境建议用批量插入,sync=False配合定时flush。
索引构建耗时:HNSW索引构建用了约35分钟。这里有个坑——Milvus构建索引期间会锁住collection的写入操作,官方文档说支持在线构建,但我们实测2.4.1版本在构建期间插入请求会超时。
4.2 Qdrant写入与索引构建
Qdrant支持wait=true和wait=false两种写入模式。我们测试时用wait=false(异步写入),500万条数据写入仅耗时22分钟。索引构建是即时完成的(HNSW在插入时就实时建索引),不需要额外的构建步骤,这是一个重要的架构差异。
4.3 查询性能对比
使用相同的数据分布和查询模式,预热10分钟后用并发工具压测:
| 指标 | Milvus 2.4.1 | Qdrant 1.9.2 |
|---|---|---|
| 平均延迟 | 18ms | 33ms |
| P99延迟 | 23ms | 41ms |
| 每秒查询数(QPS) | 420 | 260 |
| 召回率(10条内) | 0.93 | 0.91 |
查询代码(Milvus):
from pymilvus import connections, Collection
connections.connect(alias="default", host="localhost", port="19530")
collection = Collection("product_emb")
# 带标量过滤的搜索
results = collection.search(
data=[query_vector],
anns_field="embedding",
param={"metric_type": "IP", "params": {"ef": 128}},
limit=10,
expr='price > 100 and category == "electronics"',
output_fields=["item_id", "price"]
)
Qdrant查询代码(使用Python客户端):
from qdrant_client import QdrantClient
client = QdrantClient(host="localhost", port=6333)
results = client.search(
collection_name="product_emb",
query_vector=query_vector,
query_filter=models.Filter(
must=[
models.FieldCondition(key="price", range=models.Range(gte=100)),
models.FieldCondition(key="category", match=models.MatchValue(value="electronics")),
]
),
limit=10,
with_payload=True,
)
五、踩坑与优化:三次真实的生产事故
5.1 Milvus内存暴涨问题
第一次压测Milvus时,32G内存直接被吃满,系统开始疯狂swap。排查后发现是HNSW的efConstruction参数设置过大(我们一开始设了500),导致索引构建时内存峰值极高。调回200后内存降到22G左右。
优化方案:Milvus的dataCoord.segment.maxSize参数也影响内存,默认1024MB,我们调小到512MB,内存占用进一步降到19G。
5.2 Qdrant写入丢数据
Qdrant的异步写入(wait=false)确实快,但测试中发现如果并发过高,部分写入会返回accepted但实际丢失。查文档才发现Qdrant的异步写入在单机模式下有一个隐藏的队列上限,默认10000条,超出的请求会被静默丢弃。
优化方案:改用wait=true写入,或者增加write_consistency_factor参数。我们最终选择了wait=true批量提交(每批1000条),性能损失约15%,但数据完整性有保障。
5.3 Milvus与Qdrant的过滤性能倒挂
在做价格区间过滤时发现一个反直觉的现象:当过滤后数据量小于10万条时,Qdrant比Milvus快得多;但当过滤条件很宽泛(比如过滤后仍有300万条),Milvus反而更快。
原因是两者实现预过滤的方式不同:Milvus先走HNSW粗筛再按bitmap过滤,Qdrant则是先按标量索引过滤再走向量搜索。前者在过滤条件严格时浪费了大量向量距离计算,后者在过滤条件宽松时又产生了大量中间结果。
优化方案:针对我们的业务场景(价格+类目过滤后通常剩5-20万条),Qdrant的表现其实更符合预期。如果过滤条件非常宽泛,建议在Milvus中把expr里的索引字段改为in操作而非范围比较。
六、效果数据与最终选型
经过两周的对比测试,最终的决策依据如下:
资源占用对比(500万向量,16核32G环境):
- Milvus:内存峰值22.5G(含etcd+minio),磁盘占用约8GB(原始向量+索引)
- Qdrant:内存峰值13.2G(纯Qdrant进程),磁盘占用约10GB(因为Qdrant额外存了payload索引)
运维复杂度:
- Milvus:三个容器组件,升级时需同步更新版本,etcd和minio的备份恢复比较麻烦
- Qdrant:单容器,升级只需换镜像,存储目录直接打包即是备份
功能完整度:
- Milvus有完整的用户权限管理(RBAC),Qdrant在1.9版本还没有内置的认证机制(需要前置Nginx做basic auth)
- 两者都支持HNSW、IVF索引,但Milvus额外支持DiskANN(基于磁盘的索引),适合超大数据集
最终结论:我们的业务场景(过滤条件多、数据量适中、单机部署)选择了Qdrant。原因有三:
1. 内存占用少40%,可以在同一台机器上跑更多的模型服务
2. 部署简单,故障排查效率高
3. 过滤后的查询延迟虽比Milvus高,但仍在业务可接受的50ms以内
如果你们的场景是数据量过亿且需要水平扩展,那Milvus的分布式架构和DiskANN索引会是更好的选择。但如果只是千万级以下、单机或双机部署,Qdrant的生产力优势会明显胜出。
最后提醒一句:任何向量数据库的选型都别只看benchmark数字,用你们自己的数据和查询模式去实测,否则很容易踩到我们踩过的坑。