Appearance
向量库选型:PGVector 够用就别上 Milvus
1. 本节产出
一份可直接写进方案的选型结论,加上一套通过配置切换向量库的代码(PGVector / Milvus / Elasticsearch 三选一,业务代码不变),并用 docker-compose 一键起好依赖。
2. 前置依赖
- 02-05 Embedding 模型选型:已确定维度
- Docker 可用
3. 为什么选型焦虑是错的
团队在向量库上纠结两周,最后发现:换库带来的召回率提升通常不到 5%,而把切分做对能提升 20%+。
但选型确实有一个真正的意义:运维成本。
| 选型错误 | 后果 |
|---|---|
| 小项目上了 Milvus | 多一套分布式系统要维护,出问题排查不动 |
| 大项目用了 PGVector | 百万级以上检索变慢,且影响主库 |
| 已有 ES 又引入 PGVector | 两套存储,数据同步与一致性成本 |
选型的本质是选「你要背多少运维」,不是选性能。性能在绝大多数规模下都不是瓶颈。
4. 核心原理
4.1 三选一对比
| 维度 | PGVector | Milvus | Elasticsearch |
|---|---|---|---|
| 规模上限 | 百万级(< 500 万) | 亿级 | 千万级 |
| 部署复杂度 | 低(PG 插件) | 高(多组件) | 中(已有则低) |
| 混合检索 | 弱(需自己实现 BM25) | 中 | 强(原生 BM25) |
| 元数据过滤 | 强(SQL WHERE) | 中 | 强 |
| 事务与一致性 | 强(PG 事务) | 弱 | 中 |
| 团队已有 PG | 几乎零成本 | 需新增组件 | 需新增组件 |
4.2 决策规则
已有 PostgreSQL?
└─ 是 → 规模 < 500 万片段?
└─ 是 → PGVector(推荐)
└─ 否 → Milvus
已有 Elasticsearch 且需要强关键词检索?
└─ 是 → 直接用 ES(省一套存储)
全新、预期千万级以上、有专职运维?
└─ 是 → Milvus
其余情况 → PGVector默认答案是 PGVector。理由:
- 运维成本最低,DBA 已经会了;
- 元数据过滤用 SQL,表达能力最强(多租户、权限过滤全靠它);
- 事务保证灌库的原子性——这在做增量更新时非常重要(见 02-14)。
4.3 索引类型的选择
PGVector 有两种索引:
| 索引 | 特点 | 适用 |
|---|---|---|
| IVFFlat | 构建快、内存小、召回略低 | 数据量小、频繁重建 |
| HNSW | 查询快、召回高、构建慢、内存大 | 生产推荐 |
sql
-- HNSW 索引(推荐)
CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 查询时提高 ef_search 可提升召回(代价是变慢)
SET hnsw.ef_search = 100;ef_search 是调优主旋钮:默认 40,提到 100~200 能提升召回但增加延迟。用评测集找平衡点。
4.4 元数据存储:不要只存向量
sql
CREATE TABLE vector_store (
id UUID PRIMARY KEY,
content TEXT, -- 片段原文
metadata JSONB, -- 元数据(tenantId/version/权限/标题路径)
embedding VECTOR(1024), -- 向量
created_at TIMESTAMPTZ DEFAULT now()
);
-- 元数据上的常用索引:多租户与版本过滤全靠它
CREATE INDEX idx_metadata_tenant ON vector_store USING GIN (metadata);
CREATE INDEX idx_source ON vector_store ((metadata->>'sourceId'));metadata 用 JSONB 并建 GIN 索引是本节的关键建议。没有索引的元数据过滤会导致全表扫描,百万级时查询从 20ms 变成 2s。
5. 代码走查
5.1 依赖
xml
<!-- PGVector -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<!-- 或 Milvus -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-milvus</artifactId>
</dependency>5.2 通过配置切换实现
java
// ch02-rag/src/main/java/com/aitech/rag/config/VectorStoreConfig.java
@Configuration
public class VectorStoreConfig {
@Bean
@ConditionalOnProperty(name = "ai.vector-store.type", havingValue = "pgvector")
public VectorStore pgVectorStore(JdbcTemplate jdbc, EmbeddingModel model,
VectorProps props) {
return PgVectorStore.builder(jdbc, model)
.dimensions(props.dimensions())
.distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.initializeSchema(true)
.build();
}
@Bean
@ConditionalOnProperty(name = "ai.vector-store.type", havingValue = "milvus")
public VectorStore milvusStore(MilvusServiceClient client, EmbeddingModel model,
VectorProps props) {
return MilvusVectorStore.builder(client, model)
.databaseName("rag")
.collectionName("chunks")
.dimensions(props.dimensions())
.build();
}
}业务代码只依赖 VectorStore 接口,换库只改配置。这就是 Spring AI 抽象层的价值——也正是为什么不应该自己写向量库访问代码。
5.3 一键起依赖
yaml
# docker-compose.yml
services:
pgvector:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: rag
POSTGRES_USER: rag
POSTGRES_PASSWORD: rag
ports: ["5432:5432"]
volumes:
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
command: ["postgres", "-c", "hnsw.ef_search=100"]5.4 灌库与检索
java
// 灌库
vectorStore.accept(documents);
// 带元数据过滤的检索(多租户与权限的基础)
SearchRequest req = SearchRequest.builder()
.query("年假怎么算")
.topK(5)
.similarityThreshold(0.6)
.filterExpression("tenantId == 'acme' && version == '2026'")
.build();
List<Document> hits = vectorStore.similaritySearch(req);filterExpression 这一行是后面所有隔离能力的基础(02-12 展开)。选型时务必确认你选的库支持元数据过滤——这是 PGVector 的最大优势。
6. 跑起来
bash
git checkout ch02-06-vector-store
docker compose up -d
# 建索引(HNSW)
psql -h localhost -U rag -d rag -c "
CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);"
# 灌 10000 条测试片段
mvn -q test -Dtest=VectorStoreBenchmarkTest期望的基准结果(本地 Docker,1 万条 1024 维向量):
| 操作 | PGVector(HNSW) | 无索引全表扫描 |
|---|---|---|
| 单次检索(topK=5) | 15~40ms | 400~900ms |
| 灌库 10000 条 | 40~90s | — |
| 带 tenantId 过滤检索 | 20~50ms | 2s+ |
| 检查项 | 通过标准 |
|---|---|
| 索引生效 | 检索 P95 < 100ms |
| 过滤生效 | 带 tenantId 过滤后只返回该租户结果 |
| 维度一致 | 无「dimension mismatch」错误 |
| 切换可用 | 改配置切到另一实现,业务代码不变 |
最后一项演示方式:改 ai.vector-store.type 重启,跑同一个检索用例,结果一致。
7. 生产避坑
- 不要过早引入分布式向量库。Milvus 在百万级以下的优势体现不出来,但运维成本高得多。等你的片段数真的接近千万、且 P95 延迟成为问题时再迁移——而且因为用了
VectorStore抽象,迁移成本不高。 - PGVector 会和主库争资源。向量检索是 CPU 和内存密集型,放在业务主库上会影响在线事务。做法:要么独立部署一个 PG 实例专做向量,要么限制
ef_search与并发。别把向量表和订单表放同一个实例。 - 元数据过滤必须有索引。JSONB 上不建 GIN 索引时,过滤会退化成全表扫描,万级数据量可能还能忍,百万级直接不可用。这是上线后性能雪崩最常见的原因。
8. 延伸与锚点
- 思考题:向量库只做「语义相似」检索。用户搜「SO12345」这种精确编号时,向量检索表现很差。怎么办?(提示:关键词检索——答案在 02-09 混合检索)
- 代码锚点:
git checkout ch02-06-vector-store - 下一课时:02-07 Document 元数据设计
- 对应课件:L02-06 向量库选型