Skip to content

向量库选型:PGVector 够用就别上 Milvus ​

1. 本节产出 ​

一份可直接写进方案的选型结论,加上一套通过配置切换向量库的代码(PGVector / Milvus / Elasticsearch 三选一,业务代码不变),并用 docker-compose 一键起好依赖。

2. 前置依赖 ​

3. 为什么选型焦虑是错的 ​

团队在向量库上纠结两周,最后发现:换库带来的召回率提升通常不到 5%,而把切分做对能提升 20%+。

但选型确实有一个真正的意义:运维成本。

选型错误后果
小项目上了 Milvus多一套分布式系统要维护,出问题排查不动
大项目用了 PGVector百万级以上检索变慢,且影响主库
已有 ES 又引入 PGVector两套存储,数据同步与一致性成本

选型的本质是选「你要背多少运维」,不是选性能。性能在绝大多数规模下都不是瓶颈。

4. 核心原理 ​

4.1 三选一对比 ​

维度PGVectorMilvusElasticsearch
规模上限百万级(< 500 万)亿级千万级
部署复杂度低(PG 插件)高(多组件)中(已有则低)
混合检索弱(需自己实现 BM25)中强(原生 BM25)
元数据过滤强(SQL WHERE)中强
事务与一致性强(PG 事务)弱中
团队已有 PG几乎零成本需新增组件需新增组件

4.2 决策规则 ​

已有 PostgreSQL?
  └─ 是 → 规模 < 500 万片段?
           └─ 是 → PGVector(推荐)
           └─ 否 → Milvus

已有 Elasticsearch 且需要强关键词检索?
  └─ 是 → 直接用 ES(省一套存储)

全新、预期千万级以上、有专职运维?
  └─ 是 → Milvus

其余情况 → PGVector

默认答案是 PGVector。理由:

  1. 运维成本最低,DBA 已经会了;
  2. 元数据过滤用 SQL,表达能力最强(多租户、权限过滤全靠它);
  3. 事务保证灌库的原子性——这在做增量更新时非常重要(见 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~40ms400~900ms
灌库 10000 条40~90s—
带 tenantId 过滤检索20~50ms2s+
检查项通过标准
索引生效检索 P95 < 100ms
过滤生效带 tenantId 过滤后只返回该租户结果
维度一致无「dimension mismatch」错误
切换可用改配置切到另一实现,业务代码不变

最后一项演示方式:改 ai.vector-store.type 重启,跑同一个检索用例,结果一致。

7. 生产避坑 ​

  1. 不要过早引入分布式向量库。Milvus 在百万级以下的优势体现不出来,但运维成本高得多。等你的片段数真的接近千万、且 P95 延迟成为问题时再迁移——而且因为用了 VectorStore 抽象,迁移成本不高。
  2. PGVector 会和主库争资源。向量检索是 CPU 和内存密集型,放在业务主库上会影响在线事务。做法:要么独立部署一个 PG 实例专做向量,要么限制 ef_search 与并发。别把向量表和订单表放同一个实例。
  3. 元数据过滤必须有索引。JSONB 上不建 GIN 索引时,过滤会退化成全表扫描,万级数据量可能还能忍,百万级直接不可用。这是上线后性能雪崩最常见的原因。

8. 延伸与锚点 ​

  • 思考题:向量库只做「语义相似」检索。用户搜「SO12345」这种精确编号时,向量检索表现很差。怎么办?(提示:关键词检索——答案在 02-09 混合检索)
  • 代码锚点:git checkout ch02-06-vector-store
  • 下一课时:02-07 Document 元数据设计
  • 对应课件:L02-06 向量库选型