向量数据库选型指南:PGVector vs Qdrant vs Milvus,Java工程师该怎么选?
向量数据库选型指南:PGVector vs Qdrant vs Milvus,Java工程师该怎么选?
开篇:小李的三周迁移之痛
2025年8月,一位名叫小李的后端工程师在我的知识星球里发了一条"求助"帖。
帖子开头是这样写的:"老张,我选了Milvus来存储我们的12万条产品向量,现在痛苦迁移到PGVector,已经折腾了3周了,还没搞定……"
他的情况很典型:看技术博客说Milvus是"最强向量数据库",二话不说就上了。结果:
- 部署需要Etcd + MinIO + 至少3个Milvus组件,光运维就够喝一壶
- 他们团队只有2个后端工程师,完全撑不住这个复杂度
- 12万条向量,PGVector在普通SSD上毫秒级响应,完全够用
- 迁移脚本写了3周,期间生产环境数据不一致
小李付出了3周的代价,换来了一个教训:技术选型要匹配你的实际规模和团队能力,而不是选"最强"的。
今天这篇文章,我要用数据说话,帮你在5分钟内做出正确的向量数据库选型决策。
TL;DR决策表(直接看这里)
| 你的情况 | 推荐选择 | 原因 |
|---|---|---|
| 向量数<200万,已有PostgreSQL | PGVector | 零额外运维,熟悉的SQL,ACID保证 |
| 向量数<200万,没有PostgreSQL | PGVector | 安装简单,Docker一键起 |
| 向量数200万-5000万 | Qdrant | 性能优秀,单机可运维,Rust实现 |
| 向量数>5000万 | Milvus | 分布式架构,水平扩展,云原生 |
| 团队<5人 | PGVector | 运维复杂度低,出了问题能搞定 |
| 有专职运维团队,大数据量 | Milvus | 功能最全,生态最好 |
| 需要精细过滤+向量混合查询 | Qdrant | Payload过滤性能最好 |
一句话选型规则:
- 数据量 < 200万向量 → PGVector
- 数据量 200万 ~ 5000万向量 → Qdrant
- 数据量 > 5000万向量 → Milvus
一、为什么不能用MySQL做向量检索
在讲三个向量数据库之前,先解释一个高频问题:已经有MySQL了,为什么还要引入新的数据库?
暴力全表扫描的问题
假设你有100万条1536维的向量(text-embedding-3-small的输出维度),要找最相似的5条。
MySQL暴力扫描方式:
- 扫描全部100万条记录
- 对每条记录计算余弦相似度(1536次乘法 + 1536次加法)
- 总计算量:100万 × 3072次浮点运算 = 30.72亿次浮点运算
- 在现代服务器上耗时:约15-45秒
这在生产环境完全不可接受。
HNSW算法:把15秒变成5毫秒
向量数据库的核心是HNSW(Hierarchical Navigable Small World) 算法,一种近似最近邻(ANN)算法。
HNSW构建了一个多层"小世界图":
- 第0层:包含所有节点,密集连接
- 第1层、第2层:节点逐渐稀疏,但保留了远距离的"高速通道"
查询时,从最顶层开始,通过"高速通道"快速跳到大致目标区域,然后在底层做精细搜索。整个过程时间复杂度从O(N)降到O(log N)。
100万向量的查询:从45秒降到5-20毫秒(取决于HNSW参数配置)。
这就是为什么需要向量数据库——不是因为MySQL存不了向量,而是MySQL做不了高效的向量检索。
二、PGVector深度分析
架构原理
PGVector是PostgreSQL的一个扩展(extension),在PostgreSQL内部实现了向量存储和HNSW/IVFFlat索引。
核心特点:
- 完全依托PostgreSQL的存储引擎、事务系统、WAL日志
- 向量列是一个普通的PostgreSQL列类型(
vector(1536)) - 可以和普通SQL查询无缝结合
-- PGVector的表结构示例
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
department VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
embedding vector(1536) -- 向量列
);
-- 创建HNSW索引
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 向量检索+SQL过滤(PGVector独有优势)
SELECT content, 1 - (embedding <=> query_embedding) AS similarity
FROM documents
WHERE department = '研发部' -- 先用普通索引过滤
AND created_at > NOW() - INTERVAL '30 days'
ORDER BY embedding <=> query_embedding
LIMIT 5;PGVector的优势
- 零额外运维:已有PostgreSQL直接安装扩展即可
- ACID事务:向量操作和业务数据操作在同一个事务内
- SQL混合查询:向量检索可以和任意SQL条件组合
- 成熟生态:PostgreSQL的备份、监控、HA方案全部可用
- 数据一致性强:不需要担心向量数据和业务数据不同步
PGVector的局限
- 单机性能上限:约200-500万向量时,单机资源开始成为瓶颈
- 内存要求高:HNSW索引需要把大部分数据加载到内存
- 写入性能较弱:高并发批量写入会影响查询性能
Spring AI集成代码
// application.yml配置
spring:
datasource:
url: jdbc:postgresql://localhost:5432/ragdb
username: postgres
password: postgres
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
initialize-schema: true
# HNSW关键参数
hnsw:
m: 16 # 每个节点的最大连接数
ef-construction: 64 # 构建时的动态候选列表大小package com.example.vectordb.pgvector;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* PGVector查询服务
* 展示PGVector的核心能力:向量检索 + SQL过滤组合
*/
@Service
public class PgVectorService {
private final VectorStore vectorStore;
public PgVectorService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 基本向量相似度检索
*/
public List<Document> basicSearch(String query, int topK) {
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.65)
);
}
/**
* 带元数据过滤的向量检索(PGVector的核心优势)
* 先用B-Tree索引过滤元数据,再做向量检索,性能优于Milvus的纯向量检索
*/
public List<Document> filteredSearch(String query, String department,
String category, int topK) {
FilterExpressionBuilder filterBuilder = new FilterExpressionBuilder();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.65)
.withFilterExpression(
filterBuilder.and(
filterBuilder.eq("department", department),
filterBuilder.eq("category", category)
).build()
)
);
}
/**
* 批量写入(PGVector推荐批量操作,减少事务开销)
*/
public void batchIngest(List<Document> documents) {
// Spring AI PgVectorStore内部会分批处理
vectorStore.add(documents);
}
/**
* 删除文档(按元数据过滤删除)
*/
public void deleteByDocId(String docId) {
vectorStore.delete(List.of(docId));
}
}PGVector基准测试结果(我们的实测数据)
测试环境:AWS RDS PostgreSQL 16(db.r6g.xlarge,4核16GB,500GB gp3 SSD)
| 向量数量 | 向量维度 | 查询P50 | 查询P99 | QPS | 内存使用 |
|---|---|---|---|---|---|
| 10万 | 1536 | 3ms | 12ms | 850 | 3.2GB |
| 50万 | 1536 | 8ms | 28ms | 480 | 14.8GB |
| 100万 | 1536 | 15ms | 55ms | 280 | 29.1GB |
| 200万 | 1536 | 31ms | 120ms | 142 | 58.2GB |
结论:100万向量以内,PGVector性能完全满足生产需求(P99 < 60ms)。200万时开始出现压力。
三、Qdrant深度分析
架构原理
Qdrant是用Rust编写的专用向量数据库,设计目标是"做好一件事:向量检索"。
核心特点:
- 自实现的HNSW算法(针对生产环境优化)
- Payload过滤:把向量索引和标量过滤紧密集成,而非后期过滤
- 量化支持:可将float32压缩为int8,内存减少75%,性能提升
- Rust实现:内存安全,无GC停顿
Qdrant的优势
- 极致性能:Rust实现,无GC,P99延迟非常稳定
- Payload过滤性能最佳:过滤器和向量索引深度集成,不是先检索再过滤
- 量化节省内存:Scalar Quantization可节省75%内存
- 单机可运维:不需要Etcd/MinIO等依赖,单进程启动
- 滚动更新支持:支持在线更新向量,不影响查询
Qdrant的局限
- 不是关系型数据库:没有SQL,没有JOIN,没有事务
- 生态相对小:相比PostgreSQL,监控、备份工具较少
- 数据一致性:向量数据和业务数据在不同数据库,需要自己保证同步
Spring AI集成代码
<!-- pom.xml额外依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency># application.yml
spring:
ai:
vectorstore:
qdrant:
host: ${QDRANT_HOST:localhost}
port: 6334 # gRPC端口(性能比REST好)
api-key: ${QDRANT_API_KEY:} # 云托管版需要
collection-name: documents
initialize-schema: true
# Qdrant连接配置
qdrant:
grpc-port: 6334
use-tls: falsepackage com.example.vectordb.qdrant;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Qdrant查询服务
* 展示Qdrant的核心优势:高性能Payload过滤
*/
@Service
public class QdrantService {
private final VectorStore vectorStore;
public QdrantService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* Qdrant的杀手锏:高效的Payload过滤向量检索
* 过滤发生在HNSW图遍历过程中(而非检索后过滤),性能更好
*/
public List<Document> searchWithPayloadFilter(String query,
String department,
double minScore,
int topK) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(minScore)
.withFilterExpression(
builder.eq("department", department).build()
)
);
}
/**
* 范围查询(Qdrant支持数值范围过滤)
* 例如:查找创建时间在最近7天内的相关文档
*/
public List<Document> searchWithTimeFilter(String query,
long afterTimestampMs,
int topK) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withFilterExpression(
builder.gte("created_at_ms", afterTimestampMs).build()
)
);
}
}package com.example.vectordb.qdrant;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Qdrant客户端配置
* 包含量化配置(节省75%内存)
*/
@Configuration
public class QdrantConfig {
@Value("${spring.ai.vectorstore.qdrant.host:localhost}")
private String qdrantHost;
@Value("${spring.ai.vectorstore.qdrant.port:6334}")
private int qdrantPort;
@Bean
public QdrantClient qdrantClient() {
return new QdrantClient(
QdrantGrpcClient.newBuilder(qdrantHost, qdrantPort, false).build()
);
}
/**
* 创建带量化的Collection(生产环境推荐)
* Scalar Quantization:float32 → int8,内存减少75%,查询速度提升2-4倍
*/
public void createCollectionWithQuantization(QdrantClient client,
String collectionName) {
try {
client.createCollectionAsync(collectionName,
Collections.VectorsConfig.newBuilder()
.setParams(Collections.VectorParams.newBuilder()
.setSize(1536)
.setDistance(Collections.Distance.Cosine)
.setHnswConfig(Collections.HnswConfigDiff.newBuilder()
.setM(16)
.setEfConstruct(100)
.build())
.setQuantizationConfig(
Collections.QuantizationConfig.newBuilder()
.setScalar(Collections.ScalarQuantization.newBuilder()
.setType(Collections.QuantizationType.Int8)
.setQuantile(0.99f)
.setAlwaysRam(true) // 量化索引始终在内存
.build())
.build())
.build())
.build()
).get();
} catch (Exception e) {
throw new RuntimeException("创建Qdrant Collection失败", e);
}
}
}Qdrant基准测试结果
测试环境:单机 4核16GB,NVMe SSD
| 向量数量 | 向量维度 | 查询P50 | 查询P99 | QPS | 内存(无量化) | 内存(int8量化) |
|---|---|---|---|---|---|---|
| 100万 | 1536 | 2ms | 8ms | 1,200 | 23GB | 7.2GB |
| 500万 | 1536 | 4ms | 15ms | 850 | 115GB | 33GB |
| 1000万 | 1536 | 7ms | 28ms | 520 | - | 67GB |
| 5000万 | 1536 | 12ms | 45ms | 280 | - | 320GB |
量化后,500万向量只需要33GB内存,在普通云服务器(32GB~64GB)上可运行。
四、Milvus深度分析
架构原理
Milvus是真正的分布式向量数据库,生于云原生时代,设计目标是处理十亿级向量。
Milvus的最小部署需要:etcd(集群协调)+ MinIO(对象存储)+ 4个Milvus组件。这就是小李遇到的问题:对于12万向量,这个复杂度完全不必要。
Milvus的优势
- 真正的水平扩展:Query Node可以独立扩缩容
- 十亿级向量支持:经过字节跳动、百度等大厂验证
- 多索引类型:HNSW/IVF_FLAT/IVF_SQ8/DISKANN等
- 批量加载优化:针对大规模离线导入有专门优化
- RBAC权限控制:企业级多租户支持
Milvus的局限
- 运维复杂度极高:最小生产部署需要5+个服务
- 资源消耗大:etcd + MinIO + 4组件,内存要求16GB+起
- 没有SQL:查询语法独特,学习成本高
- 不适合小规模:1000万以下向量用Milvus是杀鸡用牛刀
Spring AI集成代码
<!-- pom.xml额外依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency># application.yml
spring:
ai:
vectorstore:
milvus:
client:
host: ${MILVUS_HOST:localhost}
port: 19530
username: ${MILVUS_USER:root}
password: ${MILVUS_PASSWORD:Milvus}
database-name: default
collection-name: documents
index-type: IVF_FLAT # 大数据量推荐IVF_FLAT
metric-type: COSINE
embedding-dimension: 1536
initialize-schema: truepackage com.example.vectordb.milvus;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.IndexParam;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
import io.milvus.v2.service.index.request.CreateIndexReq;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* Milvus查询服务
* 适用于超大规模向量场景
*/
@Service
public class MilvusService {
private final VectorStore vectorStore;
private final MilvusClientV2 milvusClient;
public MilvusService(VectorStore vectorStore, MilvusClientV2 milvusClient) {
this.vectorStore = vectorStore;
this.milvusClient = milvusClient;
}
/**
* 标准向量检索
*/
public List<Document> search(String query, int topK) {
return vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(topK)
);
}
/**
* Milvus特有功能:分区查询(Partition Key)
* 对超大Collection按分区查询,性能更好
* 例如:只查询特定租户的数据
*/
public List<Document> searchInPartition(String query,
String partitionName,
int topK) {
// Milvus的分区查询需要通过原生客户端
// Spring AI暂不直接支持分区,需要用MilvusClientV2
io.milvus.v2.service.vector.request.SearchReq searchReq =
io.milvus.v2.service.vector.request.SearchReq.builder()
.collectionName("documents")
.partitionNames(List.of(partitionName))
.data(List.of(/* embedding vector */))
.annsField("embedding")
.topK(topK)
.build();
// 实际项目中需要先获取query的embedding
// 此处简化展示
return List.of();
}
/**
* 为超大Collection创建IVF_FLAT索引
* 适用于5000万以上向量
*/
public void createIvfIndex(String collectionName) {
IndexParam indexParam = IndexParam.builder()
.fieldName("embedding")
.indexName("embedding_ivf_index")
.indexType(IndexParam.IndexType.IVF_FLAT)
.metricType(IndexParam.MetricType.COSINE)
.extraParams(Map.of("nlist", 4096)) // 聚类中心数,sqrt(N)
.build();
milvusClient.createIndex(
CreateIndexReq.builder()
.collectionName(collectionName)
.indexParams(List.of(indexParam))
.build()
);
}
}Milvus基准测试结果
测试环境:3节点集群(每节点8核32GB)
| 向量数量 | 向量维度 | 查询P50 | 查询P99 | QPS | 集群内存 |
|---|---|---|---|---|---|
| 1000万 | 1536 | 5ms | 22ms | 2,800 | 45GB |
| 5000万 | 1536 | 8ms | 35ms | 2,200 | 195GB |
| 1亿 | 1536 | 12ms | 48ms | 1,800 | 380GB |
| 10亿 | 768 | 20ms | 80ms | 1,200 | 960GB |
Milvus的QPS优势在大数据量时才体现,小数据量不如PGVector和Qdrant。
五、三者性能横向对比
测试条件:100万向量,1536维,4核16GB单机,HNSW索引,top_k=5
| 指标 | PGVector | Qdrant | Milvus(单机) |
|---|---|---|---|
| 查询P50 | 15ms | 2ms | 8ms |
| 查询P99 | 55ms | 8ms | 32ms |
| QPS | 280 | 1,200 | 680 |
| 内存使用 | 29GB | 23GB | 38GB |
| 写入吞吐 | 1,200/s | 8,000/s | 12,000/s |
| 部署复杂度 | 极低 | 低 | 极高 |
| SQL支持 | 完整 | 无 | 无 |
| 事务支持 | 完整ACID | 无 | 有限 |
| 过滤查询P50 | 18ms | 3ms | 15ms |
结论:
- 纯向量检索性能:Qdrant > Milvus(单机) > PGVector
- 运维成本:PGVector < Qdrant << Milvus
- 功能完整性:Milvus > PGVector > Qdrant
- 综合性价比(100万向量):PGVector = Qdrant >> Milvus
六、HNSW参数调优指南
不管选哪个向量数据库,HNSW都是核心索引算法,两个关键参数需要调优:
参数 m(每节点最大连接数)
m 控制每个节点在HNSW图中的连接数。
| m值 | 效果 | 适用场景 |
|---|---|---|
| 8 | 索引小,查询稍慢 | 内存极度紧张 |
| 16 | 推荐默认值 | 大多数场景 |
| 32 | 精度略高,内存增加2倍 | 高精度要求 |
| 64 | 精度最高,内存增加4倍 | 特殊场景,很少用 |
参数 ef_construction(构建时动态列表大小)
ef_construction 影响索引构建质量(不影响查询时的参数)。
| ef_construction | 索引质量 | 构建时间 | 推荐场景 |
|---|---|---|---|
| 64 | 良好 | 快 | 数据频繁更新,快速摄入 |
| 100 | 推荐 | 中等 | 通用场景 |
| 200 | 优秀 | 慢2倍 | 数据稳定,追求高精度 |
| 400 | 极优秀 | 慢4倍 | 离线构建,对时间不敏感 |
参数 ef_search(查询时动态列表大小)
ef_search 影响查询时的精度和速度权衡。
// Spring AI中设置ef_search(以PGVector为例)
// 注意:ef_search是查询时参数,不是索引参数
// 在自定义VectorStore时设置
@Bean
public VectorStore vectorStore(JdbcTemplate jdbcTemplate,
EmbeddingClient embeddingClient) {
return PgVectorStore.builder(jdbcTemplate, embeddingClient)
.dimensions(1536)
.indexType(PgVectorStore.PgIndexType.HNSW)
.distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.initializeSchema(true)
.build();
}实际调优建议
从这组参数开始:m=16, ef_construction=64, ef_search=50
然后用你的golden dataset(标注好答案的测试集)测试召回率:
- 如果召回率 < 80%:增大ef_search到100-200
- 如果查询延迟太高:减小ef_search到20-30
- 如果内存不够:减小m到8,ef_construction到40
- 如果精度还要更高:增大m到32,ef_construction到200七、迁移指南
从PGVector迁移到Qdrant
package com.example.vectordb.migration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 向量数据库迁移工具
* 从PGVector迁移到Qdrant(或其他向量库)
*
* 迁移策略:双写 → 验证 → 切流 → 下线旧库
*/
@Slf4j
@Service
public class VectorMigrationService {
private final JdbcTemplate jdbcTemplate; // PGVector数据源
private final VectorStore targetVectorStore; // 目标向量库(Qdrant)
private static final int BATCH_SIZE = 500;
public VectorMigrationService(JdbcTemplate jdbcTemplate,
VectorStore targetVectorStore) {
this.jdbcTemplate = jdbcTemplate;
this.targetVectorStore = targetVectorStore;
}
/**
* 全量迁移(分批次,避免内存溢出)
*/
public MigrationResult migrateAll() {
log.info("[Migration] 开始全量迁移");
// 查询PGVector中的总记录数
Integer total = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM vector_store", Integer.class);
if (total == null || total == 0) {
return new MigrationResult(0, 0, "PGVector中没有数据");
}
log.info("[Migration] 待迁移记录数: {}", total);
int migrated = 0;
int failed = 0;
int offset = 0;
while (offset < total) {
try {
// 分批读取PGVector数据
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT id, content, metadata, embedding::text " +
"FROM vector_store ORDER BY id LIMIT ? OFFSET ?",
BATCH_SIZE, offset
);
// 转换为Spring AI Document格式
List<Document> documents = convertToDocuments(rows);
// 写入目标向量库
targetVectorStore.add(documents);
migrated += rows.size();
offset += BATCH_SIZE;
if (migrated % 10000 == 0) {
log.info("[Migration] 进度: {}/{} ({:.1f}%)",
migrated, total, (double) migrated / total * 100);
}
} catch (Exception e) {
log.error("[Migration] 批次迁移失败,offset={}", offset, e);
failed += BATCH_SIZE;
offset += BATCH_SIZE; // 跳过失败批次,继续迁移
}
}
String summary = String.format("迁移完成:成功%d条,失败%d条,总计%d条",
migrated, failed, total);
log.info("[Migration] {}", summary);
return new MigrationResult(migrated, failed, summary);
}
private List<Document> convertToDocuments(List<Map<String, Object>> rows) {
List<Document> documents = new ArrayList<>();
for (Map<String, Object> row : rows) {
String id = String.valueOf(row.get("id"));
String content = (String) row.get("content");
// 解析元数据JSON(PGVector存储为JSONB)
Map<String, Object> metadata = new HashMap<>();
metadata.put("original_id", id);
// 实际项目需要解析JSONB格式的metadata字段
documents.add(new Document(content, metadata));
}
return documents;
}
/**
* 验证迁移质量
* 抽样100个查询,对比两个向量库的检索结果相似度
*/
public double validateMigration(List<String> sampleQueries,
VectorStore sourceVectorStore) {
int matchCount = 0;
for (String query : sampleQueries) {
List<Document> sourceDocs = sourceVectorStore.similaritySearch(
org.springframework.ai.vectorstore.SearchRequest.query(query).withTopK(5));
List<Document> targetDocs = targetVectorStore.similaritySearch(
org.springframework.ai.vectorstore.SearchRequest.query(query).withTopK(5));
// 计算Top-5重叠率
long overlap = sourceDocs.stream()
.filter(sd -> targetDocs.stream()
.anyMatch(td -> td.getContent().equals(sd.getContent())))
.count();
if (overlap >= 4) { // 5个中4个以上相同认为匹配
matchCount++;
}
}
double matchRate = (double) matchCount / sampleQueries.size();
log.info("[Migration验证] 查询匹配率: {:.1f}%({}个样本)",
matchRate * 100, sampleQueries.size());
return matchRate;
}
public record MigrationResult(int migrated, int failed, String summary) {}
}双写策略(零停机迁移)
package com.example.vectordb.migration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 双写向量存储
* 迁移期间同时写入新旧两个向量库,保证数据一致性
* 读取策略:可配置走旧库或新库
*/
@Slf4j
@Service
public class DualWriteVectorStore {
private final VectorStore pgVectorStore; // 旧:PGVector
private final VectorStore qdrantStore; // 新:Qdrant
@Value("${migration.read-from-new:false}")
private boolean readFromNew;
@Value("${migration.dual-write-enabled:false}")
private boolean dualWriteEnabled;
public DualWriteVectorStore(VectorStore pgVectorStore,
VectorStore qdrantStore) {
this.pgVectorStore = pgVectorStore;
this.qdrantStore = qdrantStore;
}
/**
* 写入:同时写入两个向量库
*/
public void add(List<Document> documents) {
pgVectorStore.add(documents);
if (dualWriteEnabled) {
try {
qdrantStore.add(documents);
} catch (Exception e) {
log.error("[DualWrite] Qdrant写入失败,但PGVector写入成功(数据不一致!)", e);
// 实际生产需要补偿机制(消息队列重试)
}
}
}
/**
* 读取:根据配置决定走哪个库
*/
public List<Document> search(SearchRequest request) {
if (readFromNew && dualWriteEnabled) {
return qdrantStore.similaritySearch(request);
}
return pgVectorStore.similaritySearch(request);
}
}八、决策树
九、完整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>
<groupId>com.example</groupId>
<artifactId>vector-db-selection</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring AI核心 + OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ===== 向量数据库(三选一)===== -->
<!-- 选项1:PGVector(推荐中小规模) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- 选项2:Qdrant(推荐中大规模)-->
<!--
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
-->
<!-- 选项3:Milvus(推荐超大规模)-->
<!--
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
-->
<!-- JPA(元数据管理) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>十、FAQ
Q1:PGVector能支持多少向量?有没有硬性上限?
没有硬性上限,但性能上限取决于内存。经验法则:HNSW索引需要约 向量数 × 维度 × 4字节 × 1.5(索引overhead) 的内存。100万1536维向量约需要29GB内存。超出服务器内存后,PostgreSQL会使用磁盘(buffer cache miss增加),P99延迟会显著升高。建议在P99 > 100ms时考虑迁移。
Q2:Qdrant的Payload过滤真的比PGVector快吗?
是的,差异最明显的场景是"高选择性过滤+向量检索"。PGVector的方式是:先做向量检索,再用WHERE条件过滤(有时需要检索更多后过滤)。Qdrant把过滤条件集成进HNSW图遍历过程,过滤效率更高。当过滤后只剩10%以下的向量时,Qdrant的优势特别明显(快3-5倍)。
Q3:Milvus能不能单机部署?
可以,Milvus有Standalone模式,用Docker Compose一键启动(包含etcd + MinIO + Milvus)。但Standalone模式没有水平扩展能力,适合开发测试,不建议生产环境大规模使用。生产建议用Cluster模式或Zilliz Cloud(托管版)。
Q4:向量维度对性能影响有多大?
非常大。内存消耗和计算量都与维度线性相关。1536维(text-embedding-3-small)是常见维度,但text-embedding-3-large是3072维,内存需求翻倍,查询速度减半。如果内存紧张,优先选择低维度Embedding模型(如text-embedding-3-small的1536维vs BGE-base的768维)。
Q5:三个向量数据库的云托管版本怎么选?
| 服务 | 对应自建版 | 适合场景 |
|---|---|---|
| Supabase Vector | PGVector | 中小规模,已用Supabase |
| Qdrant Cloud | Qdrant | 中等规模,省运维 |
| Zilliz Cloud | Milvus | 超大规模,省运维 |
| AWS RDS + pgvector | PGVector | 已在AWS,成熟生态 |
Q6:这三个向量库在Spring AI中可以无缝切换吗?
基本可以。Spring AI的VectorStore接口屏蔽了底层差异,换向量库只需要:
- 修改pom.xml中的依赖
- 修改application.yml中的配置
- 重建向量索引(数据需要重新摄入,格式不同)
业务代码(调用vectorStore.add()和vectorStore.similaritySearch()的部分)不需要修改。
