← 返回博客
2026-10-14 08:00:02

向量数据库选型:Milvus、Qdrant、pgvector 到底怎么挑

向量数据库选型:Milvus、Qdrant、pgvector 到底怎么挑

一个做企业知识库的团队,三个月前把 RAG demo 跑通了,两万份 PDF 检索效果不错。现在文档涨到四十万份,QPS 从个位数冲到几十,接口 P99 从 200ms 爬到 3 秒。问题不在模型,也不在 embedding,卡在向量检索这一层。换库还是加机器,成了绕不过去的决策。

选型不是比谁的 benchmark 好看。三个库的定位完全不同:Milvus 是分布式向量数据库,Qdrant 是带过滤能力的向量引擎,pgvector 是 Postgres 的一个扩展。把它们放在一张表里比"谁更快",等于拿卡车、SUV、轿车比零百加速。

先看数据规模和过滤需求

数据量是第一条分水岭。十万级向量、过滤条件简单,pgvector 够用,而且省掉一整套运维。千万级以上、还要水平扩容,pgvector 的单机索引会先撑不住,得考虑 Milvus 或 Qdrant。

过滤才是真正拉开差距的地方。RAG 里几乎每次检索都带条件:按租户隔离、按文档类型筛、按时间范围卡。pgvector 的做法是先用 B-tree 或 GIN 索引过滤,再走 HNSW,过滤率高的时候 planner 会直接放弃向量索引走全表扫。Qdrant 把过滤和 HNSW 图遍历揉在一起,边遍历边判断 payload 条件,过滤率高时召回率明显更稳。Milvus 支持分区键和标量字段过滤,大规模下靠 segment 裁剪控制扫描范围。

那位知识库团队的场景是"多租户 + 时间过滤 + 四十万文档",pgvector 在过滤率超过 30% 时延迟抖动最明显,Qdrant 的过滤下推正好对症。

Java 侧怎么接

选型定完,接入方式直接影响后面几个月的开发效率。三个库都有 Java 客户端,但用起来差别不小。

pgvector 最省事,复用现有连接池和事务:

// 建表时显式声明维度,向量列和业务列放同一张表
jdbcTemplate.execute("CREATE TABLE doc_chunk (" +
    "id BIGSERIAL PRIMARY KEY, tenant_id BIGINT, " +
    "embedding vector(1024))");
jdbcTemplate.execute("CREATE INDEX ON doc_chunk " +
    "USING hnsw (embedding vector_cosine_ops)");

// 查询:过滤和向量检索写在一条 SQL 里
String sql = "SELECT id, 1 - (embedding <=> ?) AS score " +
    "FROM doc_chunk WHERE tenant_id = ? " +
    "ORDER BY embedding <=> ? LIMIT 10";

关键点是索引类型和距离算子必须对齐:vector_cosine_ops 配 <=>,用错算子索引直接失效。另一个坑是 ef_search 和 m 这两个 HNSW 参数,默认值在几十万行下召回率会掉,得在会话级别调 SET hnsw.ef_search = 100。事务一致性是 pgvector 的独门优势,向量和业务数据能一起提交回滚。

Qdrant 的 Java 客户端走 gRPC,过滤条件用它的 DSL 表达:

import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Points.*;

QdrantClient client = new QdrantClient(
    QdrantGrpcClient.newBuilder("qdrant-host", 6334, false).build());

Filter filter = Filter.newBuilder()
    .addMust(Condition.newBuilder().setField(
        FieldCondition.newBuilder().setKey("tenant_id")
            .setMatch(Match.newBuilder().setInteger(42))))
    .build();

List<ScoredPoint> hits = client.searchAsync(
    SearchPoints.newBuilder()
        .setCollectionName("doc_chunk")
        .addAllVector(embedding)
        .setFilter(filter)
        .setLimit(10)
        .setWithPayload(WithPayloadSelector.newBuilder()
            .setEnable(true).build())
        .build()).get();

要点在 filter 是下推到图遍历里的,不是查完再筛,所以高过滤率下召回率不会崩。坑在于 withPayload 默认关,不显式打开返回的 payload 是空的,很多人第一次接会以为数据没写进去。集合的 quantization_config 建议一开始就配 scalar 量化,内存能省一大截,事后改要重建索引。

Milvus 的接入最重,但换来的是分布式能力:

MilvusClientV2 client = new MilvusClientV2(
    ConnectConfig.builder().uri("http://milvus:19530").build());

SearchResp resp = client.search(SearchReq.builder()
    .collectionName("doc_chunk")
    .data(List.of(new FloatVec(embedding)))
    .filter("tenant_id == 42 and ts > 1700000000")
    .limit(10)
    .outputFields(List.of("chunk_text"))
    .build());

Milvus 的过滤是字符串表达式,和 Qdrant 的 DSL 是两种风格,习惯 SQL 的人上手快。坑集中在一致性和索引:写入后要 flush 才可见,调 search 前最好确认 consistencyLevel;索引类型(HNSW、IVF、DiskANN)选错,内存和延迟差一个量级,DiskANN 适合超大规模但要求 SSD。

落到决策上

把三个库的选择浓缩成几句话:团队已经在用 Postgres、数据在百万以内、过滤不复杂,直接上 pgvector,别为了"以后可能扩容"提前引入分布式。过滤条件多、召回率要求高、单机能扛住千万级,Qdrant 是甜点区,运维复杂度可控。数据到亿级、要独立扩缩容、有多模态字段,Milvus 的分布式架构才值回票价。

下一步动作很具体:拿你真实的查询日志,抽出 1000 条带过滤的检索请求,在候选库上跑一遍,重点看过滤率 10%、50%、90% 三档下的 P99 和召回率。benchmark 报告不会告诉你这些,只有你自己的查询分布会。

本文关键词:Milvus、Qdrant、pgvector、向量数据库、RAG