向量数据库选型与踩坑实录:Milvus vs Qdrant vs pgvector 生产横评
在 LLM + RAG(检索增强生成)技术全面落地的今天,向量数据库(Vector DB)已成为企业知识库、智能客服等场景的核心基础设施。然而,当数据量级从十万级飙升至千万级(1536 维度,如 OpenAI 的 text-embedding-3-large),很多团队在选型时因为盲目看重 Benchmark 跑分,在生产环境中交了高昂的学费。
写入吞吐雪崩、HNSW 索引构建导致 OOM、混合查询(向量 + 标量过滤)延迟飙升、多组件运维成本高企……这些都是一线的真实痛点。本文将基于我主导的千万级企业知识库项目,对市面上最主流的三款向量数据库 Milvus、Qdrant 和 pgvector (PostgreSQL) 进行全方位的生产级横评。
一、 问题背景与业务痛点
在我们的千万级企业知识库项目中,文档切片(Chunks)总量达到了 1500 万。每个切片通过 Embedding 模型转化为 1536 维的浮点数向量。业务上要求支持多租户隔离、实时增删改以及混合检索(例如:在指定 tenant_id = 9527 且 security_level >= 3 的条件下,检索相似度前 5 的知识点)。
在早期技术选型和压测阶段,我们遭遇了以下三个致命痛点:
- 混合查询的“伪索引”塌陷:
传统的“先过滤、后检索(Post-filtering)”会导致召回不足(Limit 5 过滤后可能只剩 1 个);而“先检索、后过滤(Pre-filtering)”如果不支持高效的标量联合索引,在千万级数据下会导致全表扫描。 - HNSW 索引构建的内存黑洞:
HNSW(Hierarchical Navigable Small World)是目前召回率和耗时平衡最好的索引算法。但它极其消耗内存。在千万级 1536 维向量下,构建索引时内存直接飙升了数倍,导致容器频繁被 OOM Killer 杀掉。 - 运维复杂度与高可用代价:
Milvus 采用微服务架构,包含 QueryNode、IndexNode、DataNode、Proxy,还要依赖 MinIO、Pulsar/Kafka、Etcd 等一众组件。对于中小型研发团队,维护这套架构的成本甚至超过了业务开发本身。
二、 核心设计与解决思路
为了解决上述痛点,我们需要设计一套高并发、低延迟且具备多租户隔离能力的 RAG 向量检索架构。
1. 核心架构设计与组件拓扑图
在系统设计上,我们采用统一的向量网关屏蔽底层差异,通过路由策略支持多租户的数据分发。
▲ 架构图 1:系统核心组件交互拓扑与数据流向
2. 端到端请求执行时序图
以下是用户发起提问到向量检索、标量过滤,再到最终召回的完整端到端链路。
▲ 时序图 2:端到端请求处理与调用时序链路
3. 核心方案选型对比
基于我们在生产环境(千万级 1536 维向量,HNSW 索引,M=16, EF=64)的压测数据,三者的横向对比如下:
| 对比维度 | Milvus (v2.4.x) | Qdrant (v1.9.x) | pgvector (v0.7.x) |
|---|---|---|---|
| 开发语言 | Go / C++ | Rust | C (PostgreSQL 插件) |
| 写入吞吐 (TPS) | 约 8,500 (批量导入极强) | 约 12,000 (高并发写入优秀) | 约 3,200 (受限 PG 事务与 WAL) |
| HNSW 索引构建速度 | 较快 (支持多 IndexNode 并行) | 极快 (Rust 线程调度优秀) | 较慢 (构建时单 CPU 瓶颈明显) |
| 内存占用 (1000万数据) | 约 32GB (组件多,静态开销大) | 约 18GB (内存管理极度精细) | 约 28GB (共享缓冲区开销大) |
| 标量过滤性能 | 优秀 (支持 Field Index) | 极佳 (Payload Index 深度优化) | 优秀 (利用 PG 原生 B-Tree 索引) |
| 运维复杂度 | 极高 (需要维护 K8s/Helm/多个组件) | 极低 (单二进制文件/开箱即用) | 低 (已有 PG 运维经验即可) |
| 适用场景 | 亿级以上、有专业运维团队的场景 | 千万级、追求高性能与低运维成本 | 十万到百万级、已有 PG 且不想引入新组件 |
三、 完整实战代码与配置
在千万级 RAG 场景中,Qdrant 凭借 Rust 的极致性能、极低的内存占用以及优秀的标量过滤设计,成为了我们最终的生产选型。下面给出基于 Spring Boot 3.x、JDK 17 以及 Qdrant Java SDK 实现的高性能混合检索实战代码。
1. 依赖配置 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<groupId>com.mrwu.rag</groupId>
<artifactId>vector-db-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<qdrant.client.version>1.9.0</qdrant.client.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Qdrant gRPC 客户端 -->
<dependency>
<groupId>io.qdrant</groupId>
<artifactId>client</artifactId>
<version>${qdrant.client.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
2. 配置文件 application.yml
server:
port: 8080
spring:
application:
name: rag-vector-service
vector:
qdrant:
host: "192.168.11.20"
port: 6334 # gRPC 默认端口
use-tls: false
api-key: "your-super-secret-api-key"
collection-name: "enterprise_knowledge"
3. 向量检索核心服务 QdrantSearchService.java
package com.mrwu.rag.service;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.JsonWithDouble;
import io.qdrant.client.grpc.Points.*;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static io.qdrant.client.ValueFactory.value;
@Slf4j
@Service
public class QdrantSearchService {
@Value("${vector.qdrant.host}")
private String host;
@Value("${vector.qdrant.port}")
private int port;
@Value("${vector.qdrant.use-tls}")
private boolean useTls;
@Value("${vector.qdrant.api-key}")
private String apiKey;
@Value("${vector.qdrant.collection-name}")
private String collectionName;
private QdrantClient client;
@PostConstruct
public void init() {
log.info("Initializing Qdrant Client to {}:{}", host, port);
QdrantGrpcClient.Builder grpcClientBuilder = QdrantGrpcClient.newBuilder(host, port, useTls);
if (apiKey != null && !apiKey.isBlank()) {
grpcClientBuilder.withApiKey(apiKey);
}
this.client = new QdrantClient(grpcClientBuilder.build());
}
/**
* 执行高性能混合检索(向量相似度 + 标量多租户过滤)
*
* @param queryVector 1536维查询向量
* @param tenantId 租户ID(标量过滤条件)
* @param minScore 最低相似度阈值
* @param limit 返回数量
* @return 检索出的文档片段
*/
public List<Map<String, Object>> hybridSearch(List<Float> queryVector, String tenantId, double minScore, int limit) {
long startTime = System.currentTimeMillis();
List<Map<String, Object>> results = new ArrayList<>();
try {
// 1. 构建标量过滤器 (Filter: tenant_id == tenantId)
Filter filter = Filter.newBuilder()
.addMust(Condition.newBuilder()
.setField(FieldCondition.newBuilder()
.setKey("tenant_id")
.setMatch(Match.newBuilder().setKeyword(tenantId).build())
.build())
.build())
.build();
// 2. 构建 SearchPoints 请求
SearchPoints searchPoints = SearchPoints.newBuilder()
.setCollectionName(collectionName)
.addAllVector(queryVector)
.setFilter(filter)
.setLimit(limit)
.setWithPayload(WithPayloadSelector.newBuilder().setEnable(true).build()) // 召回 Payload
.setScoreThreshold((float) minScore) // 过滤低相关度结果
.build();
// 3. 执行 gRPC 调用
List<ScoredPoint> scoredPoints = client.searchAsync(searchPoints).get();
// 4. 解析结果
for (ScoredPoint point : scoredPoints) {
Map<String, JsonWithDouble.Value> payloadMap = point.getPayloadMap();
// 转换 Payload 数据
log.debug("Point ID: {}, Score: {}", point.getId().getUuid(), point.getScore());
results.add(Map.of(
"doc_id", point.getId().getUuid(),
"score", point.getScore(),
"content", payloadMap.get("content").getStringValue(),
"tenant_id", payloadMap.get("tenant_id").getStringValue()
));
}
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to execute hybrid search in Qdrant", e);
Thread.currentThread().interrupt();
throw new RuntimeException("Vector database query error", e);
} finally {
log.info("Hybrid search executed in {} ms", System.currentTimeMillis() - startTime);
}
return results;
}
@PreDestroy
public void close() {
if (client != null) {
log.info("Closing Qdrant Client...");
client.close();
}
}
}
四、 避坑指南与总结验证
在千万级生产落地过程中,我们踩过了不少深坑。以下是提炼出的黄金避坑指南:
1. pgvector:maintenance_work_mem 导致的索引构建崩溃
- 痛点:在 PostgreSQL 中使用
CREATE INDEX ... USING hnsw创建千万级 1536 维向量索引时,数据库频繁发生Connection reset或 OOM 崩溃。 - 原因:pgvector 在构建 HNSW 索引时,需要将大量的图节点缓存在内存中。默认的
maintenance_work_mem通常只有 64MB,这会导致频繁的磁盘交换,甚至因为内存不足直接 Crash。 - 解决办法:
在构建索引前,必须调大该会话的内存限制。对于 16GB 内存的数据库实例,建议将其临时调大至 4GB:
sql SET maintenance_work_mem = '4GB'; CREATE INDEX ON enterprise_knowledge USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
2. Qdrant:Mmap 内存暴涨与磁盘 I/O 恶化
- 痛点:Qdrant 运行一段时间后,系统物理内存几乎被占满,同时伴随大量的磁盘读 I/O,导致查询延迟从 10ms 飙升至 500ms 以上。
- 原因:Qdrant 默认开启了
mmap(内存映射文件),允许将索引直接映射到虚拟内存中。当数据量远超物理内存大小时,操作系统会频繁进行 Page Fault(缺页中断),导致磁盘 I/O 成为致命瓶颈。 - 解决办法:
在 Qdrant 的配置文件config.yaml中,合理配置memmap_threshold_kb。对于千万级数据,建议将向量数据强制加载到内存中,而将 Payload(标量数据)留在磁盘:
yaml storage: # 当向量数量较多时,避免将所有向量索引都丢到 mmap performance: max_search_threads: 0 vector_index: enable_memmap: false # 保证 HNSW 索引完全在内存中,换取极致检索速度
3. Milvus:标量过滤的“隐式类型转换”全表扫描
- 痛点:在 Milvus 中执行
tenant_id == "9527"过滤时,明明为tenant_id建立了INVERTED(倒排)索引,但查询耗时依然长达数百毫秒。 - 原因:Milvus 的 Schema 定义中,如果
tenant_id被定义为VARCHAR类型,但在 Java 查询中传入了Long型的9527,Milvus 在解析 DSL 表达式时会发生隐式类型转换,导致索引失效,退化为全表扫描。 - 解决办法:
严格保证客户端入参与 Milvus Schema 定义的数据类型完全一致。在 Java 代码中,显式将参数转换为String:
java // 错误写法:filter = "tenant_id == 9527" // 正确写法: String filter = "tenant_id == \"9527\"";
总结
- Qdrant 是千万级 RAG 知识库的 性价比之王。它用极低的内存开销(Rust 强项)和极简的单机/集群运维成本,扛住了我们日均千万次的混合检索请求,P99 延迟稳定在 15ms 以内。
- Milvus 适合亿级以上、拥有专职 DBA/运维团队的大厂场景。它的分布式架构上限极高,但小规模团队引入它无异于“大炮打蚊子”。
- pgvector 则是 PostgreSQL 忠实拥趸的福音。在千万级以下、对事务一致性要求极高的业务中,pgvector 能让你少维护一个数据库组件,但在超大规模下,其性能和内存开销确实略逊于纯 Rust/C++ 编写的专业向量库。

本文针对千万级企业 RAG 知识库场景,从写入吞吐、HNSW 索引构建、内存占用、标量过滤及运维复杂度等维度,深度横评 Milvus、Qdrant 与 pgvector,并提供 Spring Boot 3.x 实战集成方案与生产避坑指南。
浙公网安备 33010602011771号