PgVector从入门到实战:用PostgreSQL做向量检索,附SpringBoot+MyBatisPlus整合


📝 本文首发于 栏轩·阁

欢迎访问阅读原文,获取更好的阅读体验。


为什么用 pgvector,而不是 Redis

我在初学的时候先用 Redis + RediSearch 实现向量检索,遇到了几个痛点:

  1. Redis 的向量能力是后加的:RediSearch 模块对向量的支持相对有限,索引类型少,精度和召回率不如专门的向量方案
  2. 数据类型受限:Redis 的 value 结构决定了存向量要么序列化 blob、要么拆字段,查询和调试都不直观
  3. 维护两个存储:业务数据在 PostgreSQL,向量在 Redis,两套存一起还得考虑数据一致性,架构复杂度翻倍

pgvector 的优势

  • 向量就是 PostgreSQL 的一个字段类型 ,跟 TEXTINTEGER 没区别
  • 一条 SQL 里可以同时查业务字段和向量相似度,不需要跨数据源
  • 支持 L2 欧氏距离、余弦距离、内积距离
  • 支持 IVFFlat 和 HNSW 索引,百万级数据毫秒响应

环境搭建:Docker Compose 一键部署

pgvector 是 PostgreSQL 的扩展插件,原生 PostgreSQL 镜像不带它。官方提供了 pgvector/pgvector 镜像,开箱即用:

yaml 复制代码
version: '3.8'
services:
  postgres:
    image: pgvector/pgvector:pg17
    container_name: pgvector-compose
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: 123456
      POSTGRES_DB: vectordb
    ports:
      - "5432:5432"
    volumes:
      - ./pgdata:/var/lib/postgresql/data

如果你已有 PostgreSQL,手动加插件

如果你是在已有的 PostgreSQL 上中途加向量能力,几步搞定:

bash 复制代码
# 1. 进入容器或服务器
docker exec -it your-postgres bash

# 2. 安装 pgvector 扩展(Debian/Ubuntu)
apt-get update && apt-get install -y postgresql-17-pgvector

# 3. 重启 PostgreSQL
pg_ctl restart

然后连上去启用扩展即可(见下一节)。


SQL 基本操作

我使用 DBX 作为数据库客户端。虽然是官方镜像,但扩展默认未启用,需要先手动开启:

sql 复制代码
CREATE EXTENSION IF NOT EXISTS vector;

建表

向量在 PostgreSQL 里就是一个特殊的字段类型 vector(dim)。假设我们要存一个 3 维向量(便于手算理解),建表如下:

sql 复制代码
-- 创建一个商品表,embedding 是 3 维向量
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT,
    price DECIMAL(10,2),
    embedding VECTOR(3)
);

插入数据

插入向量时,直接用 [ ] 包住浮点数即可,非常直观:

sql 复制代码
INSERT INTO products (name, price, embedding) VALUES 
('苹果', 5.00, '[1.0, 0.0, 0.0]'),
('香蕉', 3.50, '[0.0, 1.0, 0.0]'),
('樱桃', 8.00, '[0.0, 0.0, 1.0]'),
('苹果派', 12.00, '[0.9, 0.1, 0.1]');

核心操作------相似度查询

这是 pgvector 最精华的地方。两个最常用的距离运算符:

运算符 含义 值越小表示
<-> L2 欧氏距离 向量越接近
<=> 余弦距离 方向越相似(不受向量长度影响)

找与苹果([1,0,0])最相似的商品:

sql 复制代码
SELECT 
    name, 
    price, 
    embedding <=> '[1.0, 0.0, 0.0]' AS distance
FROM products
ORDER BY distance ASC;

输出:

复制代码
name	    price	distance
苹果	        5.00	0
苹果派	    12.00	0.1732050949521497
香蕉	        3.50	1.4142135623730951
樱桃	        8.00	1.4142135623730951

可以看到:

  • 苹果与自己距离为 0(完全匹配)
  • 苹果派[0.9,0.1,0.1])与苹果方向最接近,排在第二
  • 香蕉和樱桃距离都是 1.414,明显不相似

你还可以任意加 WHERE 条件,比如筛选价格低于 10 元的:

sql 复制代码
SELECT name, price, embedding <-> '[1.0, 0.0, 0.0]' AS distance
FROM products
WHERE price < 10.0
ORDER BY distance ASC;

性能优化:索引

当数据量增大到万级以上,全表扫描就不够快了。pgvector 提供了两种索引:

IVFFlat(倒排文件索引)

原理是把向量空间划分为多个(cluster),查询时只搜索最近的几个桶,而不是全部数据:

sql 复制代码
-- 先设置 probes(查询桶数,默认 1)
SET ivfflat.probes = 1;

-- 创建索引(lists = 4 表示分 4 个桶,建议 lists = 行数 / 1000)
CREATE INDEX ON products 
USING ivfflat (embedding vector_l2_ops) 
WITH (lists = 4);

如何理解 IVFFlat?

假设我有 1000 个商品,创建 IVFFlat 索引(lists = 4)后:

  1. pgvector 先将 1000 个向量按距离聚成 4 个桶
  2. 查询时,先算目标向量离哪 1 个桶最近(probes = 1
  3. 然后只在这个桶内做精确搜索

这种方式牺牲一点点精度来换取几十倍的性能提升。

选择合适的 operator class

  • vector_l2_ops --- 配合 <-> 欧氏距离
  • vector_cosine_ops --- 配合 <=> 余弦距离
  • vector_ip_ops --- 配合 <#> 内积距离

Java 实战:SpringBoot + MyBatisPlus 整合

理论讲完了,来看看怎么在 Java 项目里用 pgvector。我会以商品相似度检索为例,完整走一遍 CRUD + 向量查询。

项目环境

  • Spring Boot 4.1.0
  • MyBatis-Plus 3.5.17(使用 mybatis-plus-spring-boot4-starter
  • pgvector-java 0.1.6
  • PostgreSQL 17 + pgvector 插件
  • JDK 21

1. 引入依赖

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.0</version>
</parent>

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MyBatis-Plus Spring Boot 4 Starter -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
        <version>3.5.17</version>
    </dependency>

    <!-- PostgreSQL 驱动(注意:不要加 runtime scope) -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>

    <!-- pgvector Java 客户端 -->
    <dependency>
        <groupId>com.pgvector</groupId>
        <artifactId>pgvector</artifactId>
        <version>0.1.6</version>
    </dependency>
</dependencies>

⚠️ 注意 :PostgreSQL 驱动不要加 <scope>runtime</scope>,因为 pgvector 的 PGvector 类在编译时就依赖了 PostgreSQL JDBC 的接口 PGBinaryObject,设为 runtime 会导致编译失败。

2. 配置数据源

yaml 复制代码
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/vectordb
    username: postgres
    password: 123456
    driver-class-name: org.postgresql.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
  type-handlers-package: com.example.pgvectordemo.typehandler

3. 实体类 ------ 关键:向量字段的 TypeHandler

先看成品,再解释原理:

java 复制代码
@TableName("products")
public class Product {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private BigDecimal price;

    // 关键:指定自定义 TypeHandler
    @TableField(typeHandler = VectorTypeHandler.class)
    private float[] embedding;

    // getter / setter / constructor ...
}

embedding 字段的类型是 Java 的 float[],但数据库里是 PostgreSQL 的 vector(3)MyBatis 默认不认识 vector 类型,所以需要告诉它两者之间怎么转换------这就是 TypeHandler 的作用。

什么是 TypeHandler?

TypeHandler 是 MyBatis 的类型转换器,负责 Java 类型 ↔ JDBC 类型 的双向翻译:

复制代码
写入数据库:  Java 对象  ──→  TypeHandler  ──→  JDBC PreparedStatement
读取数据库:  JDBC ResultSet  ──→  TypeHandler  ──→  Java 对象

MyBatis 内置了很多常见类型的 TypeHandler(比如 StringTypeHandlerIntegerTypeHandler),但 float[] ↔ PostgreSQL vector 这种组合不存在,所以得手写一个。

TypeHandler 完整代码 + 逐行解析
java 复制代码
@MappedTypes(float[].class)        // ① 这个 Handler 处理哪个 Java 类型
@MappedJdbcTypes(JdbcType.OTHER)   // ② 对应的 JDBC 类型(OTHER 表示非标准类型)
public class VectorTypeHandler extends BaseTypeHandler<float[]> {
    //                     ↑ ③ 泛型参数:声明处理的是 float[]

BaseTypeHandler<float[]> 要求子类实现 4 个抽象方法,对应不同的读写场景:

方法 何时调用 做什么
setNonNullParameter INSERT / UPDATE 时 把 Java 值设到 SQL 的 ? 占位符上
getNullableResult(rs, String) 按列名查询结果时 从 ResultSet 读到 Java
getNullableResult(rs, int) 按列索引查询结果时 同上,按数字索引
getNullableResult(CallableStatement, int) 调用存储过程时 一般用不到,但必须实现
写入方法详解
java 复制代码
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
                                 float[] parameter, JdbcType jdbcType) throws SQLException {
    // 参数说明:
    //   ps        --- JDBC 预编译语句,就是那个带 ? 的 SQL
    //   i         --- 第几个 ? 占位符(从 1 开始)
    //   parameter --- Java 层的 float[] 值
    //   jdbcType  --- JDBC 类型(这里就是 JdbcType.OTHER)

    // 第一步:把 float[] 包装成 PGvector 对象
    PGvector vector = new PGvector(parameter);
    // 等效于做了:[1.0, 0.0, 0.0] → PGvector 实例

    // 第二步:通过 JDBC 的 setObject 传给 PostgreSQL
    ps.setObject(i, vector);
    // PGvector 内部实现了 PGobject 接口,
    // JDBC 驱动会自动调用它的 getValue() 得到 "[1.0,0.0,0.0]"
    // 然后 PostgreSQL 就能正确识别为 vector 类型
}

整个写入数据流:

复制代码
Java:    float[] {1.0, 0.0, 0.0}
  ↓ new PGvector(...)
PG:      PGvector 对象
  ↓ ps.setObject()
JDBC:    "[1.0,0.0,0.0]" ::vector
  ↓ 网络传输
PostgreSQL: INSERT INTO products (embedding) VALUES ('[1.0,0.0,0.0]')
读取方法详解
java 复制代码
@Override
public float[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
    // 参数说明:
    //   rs         --- 查询结果集,指向当前行
    //   columnName --- 列名,比如 "embedding"

    // 为什么不直接 (PGvector) rs.getObject(columnName) ?
    // 因为 JDBC 不认识 vector 类型,getObject() 返回的其实是个 String
    // 所以直接用 getString() 读取原始文本 "[1.0,0.0,0.0]"
    String value = rs.getString(columnName);
    return parseVector(value);
}

// getNullableResult(rs, int columnIndex) 逻辑完全一样,只是按数字取列
// getNullableResult(CallableStatement, int) 是给存储过程用的

关键问题:为什么读的时候不直接用 PGvector 对象?

PostgreSQL JDBC 驱动的 getObject() 方法默认不认识 vector 类型------除非你手动调用 PGvector.registerTypes(conn) 注册类型映射。但这样就要在每次获取连接时做额外处理,比较麻烦。

稳定的方案 是:直接读字符串 "[1.0,0.0,0.0]",然后手动解析。

解析方法详解
java 复制代码
private float[] parseVector(String value) {
    // value 格式:"[1.0, 0.0, 0.0]"
    if (value == null) return null;

    String trimmed = value.trim();
    // 去掉首尾的方括号
    if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
        trimmed = trimmed.substring(1, trimmed.length() - 1);
    }
    // 现在 trimmed = "1.0, 0.0, 0.0"

    if (trimmed.isEmpty()) return new float[0];

    String[] parts = trimmed.split(",");
    // parts = ["1.0", " 0.0", " 0.0"]

    float[] result = new float[parts.length];
    for (int i = 0; i < parts.length; i++) {
        result[i] = Float.parseFloat(parts[i].trim());
        // Float.parseFloat(" 0.0") → 0.0
    }
    return result;  // float[] {1.0, 0.0, 0.0}
}

整个读取数据流:

复制代码
PostgreSQL: 返回 '[1.0,0.0,0.0]'::vector
  ↓ 网络传输
JDBC:       PgObject.getValue() = "[1.0, 0.0, 0.0]"
  ↓ rs.getString("embedding")
String:     "[1.0, 0.0, 0.0]"
  ↓ parseVector() 手动解析
Java:       float[] {1.0, 0.0, 0.0}
TypeHandler 如何注册生效?

TypeHandler 有 3 种注册方式,我们用的事务最简单的一种:

yaml 复制代码
mybatis-plus:
  type-handlers-package: com.example.pgvectordemo.typehandler

只要在 application.yml 里配了这个路径,MyBatis 启动时就会自动扫描该包下的所有 @MappedTypes 注解,注册进去。

然后实体类里通过 @TableField(typeHandler = VectorTypeHandler.class) 指定这个字段用哪个 Handler,MyBatis 执行 SQL 时就会自动调用对应的方法。

小结:什么情况需要自定义 TypeHandler?

只要你的 Java 类型和数据库类型没法直接对应,就需要写 TypeHandler。常见场景:

场景 Java 类型 数据库类型
向量检索(本文) float[] PostgreSQL vector
JSON 字段 自定义对象 / Map PostgreSQL jsonb
枚举 Enum 对象 VARCHARINTEGER
数组 List<String> PostgreSQL TEXT[]
加密字段 String(密文) VARCHAR

原理都一样:继承 BaseTypeHandler<T>,实现 4 个方法,配好注解和扫描路径即可。

4. Mapper:基础 CRUD + 向量查询

MyBatis-Plus 的 BaseMapper 提供 insertselectByIdupdateById 等基础方法。我们额外写两个向量相似度查询:

java 复制代码
@Mapper
public interface ProductMapper extends BaseMapper<Product> {

    // 余弦相似度
    @Select("""
            SELECT id, name, price, embedding
            FROM products
            ORDER BY embedding <=> #{targetEmbedding}::vector
            LIMIT #{topN}
            """)
    List<Product> findSimilarByCosine(@Param("targetEmbedding") String targetEmbedding,
                                      @Param("topN") int topN);

    // 欧氏距离
    @Select("""
            SELECT id, name, price, embedding
            FROM products
            ORDER BY embedding <-> #{targetEmbedding}::vector
            LIMIT #{topN}
            """)
    List<Product> findSimilarByEuclidean(@Param("targetEmbedding") String targetEmbedding,
                                         @Param("topN") int topN);
}

注意参数要转成 '[1.0,0.0,0.0]'::vector 格式传入。

5. Service

继承 ServiceImpl 获得完整 CRUD,同时封装向量查询方法:

java 复制代码
@Service
public class ProductService extends ServiceImpl<ProductMapper, Product> {

    public List<Product> findSimilarByCosine(float[] targetVector, int topN) {
        return baseMapper.findSimilarByCosine(
                arrayToPgvectorString(targetVector), topN);
    }

    public List<Product> findSimilarByEuclidean(float[] targetVector, int topN) {
        return baseMapper.findSimilarByEuclidean(
                arrayToPgvectorString(targetVector), topN);
    }

    private String arrayToPgvectorString(float[] arr) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < arr.length; i++) {
            if (i > 0) sb.append(",");
            sb.append(arr[i]);
        }
        sb.append("]");
        return sb.toString();
    }
}

6. 启动运行验证

项目启动后自动运行 Demo,输出结果如下:

复制代码
========== pgvector + MyBatis-Plus Demo Start ==========
✅ 成功插入 4 条商品数据
📋 全部商品列表:
   Product{id=1, name='苹果', price=5.00, embedding=[1.0, 0.0, 0.0]}
   Product{id=2, name='香蕉', price=3.50, embedding=[0.0, 1.0, 0.0]}
   Product{id=3, name='樱桃', price=8.00, embedding=[0.0, 0.0, 1.0]}
   Product{id=4, name='苹果派', price=12.00, embedding=[0.9, 0.1, 0.1]}

🔍 余弦相似度查询:与「苹果」最相似的商品
   Top1: 苹果 (embedding=[1.0, 0.0, 0.0])
   Top2: 苹果派 (embedding=[0.9, 0.1, 0.1])
   Top3: 香蕉 (embedding=[0.0, 1.0, 0.0])

📐 欧氏距离查询:与「苹果」距离最近的商品
   Top1: 苹果 (embedding=[1.0, 0.0, 0.0])
   Top2: 苹果派 (embedding=[0.9, 0.1, 0.1])
   Top3: 香蕉 (embedding=[0.0, 1.0, 0.0])
========== pgvector + MyBatis-Plus Demo End ==========

完整的 demo 项目代码在博客同目录下的 pgvector-demo/ 文件夹中。


踩坑记录

问题 原因 解决
编译找不到 PGBinaryObject PostgreSQL driver scope 为 runtime 去掉 scope,改为默认 compile
PGvector.fromSqlType() 不存在 这是 0.0.x 版本的老 API,0.1.x 已移除 改用 rs.getString() 手解析字符串
ServiceImpl 类找不到 3.5.13+ 移到了新包 Spring Boot 4 请用 com.baomidou.mybatisplus.spring.service.impl.ServiceImpl

总结

  • pgvector 让 PostgreSQL 原生支持向量检索,不需要额外搭一套向量数据库,一条 SQL 搞定业务字段和相似度查询
  • 部署简单:官方 Docker 镜像开箱即用,已有 PG 也能手动加插件
  • 查询灵活:支持 L2 欧氏距离、余弦距离、内积,可以混合 WHERE 条件
  • 性能可靠:IVFFlat / HNSW 索引支持百万级规模
  • Java 整合不复杂 :核心就是写一个 TypeHandler 做 float[]vector 的转换

如果你正在做 RAG、图片相似搜索、推荐系统等需要向量检索的功能,不妨试试 pgvector------毕竟,能少维护一个中间件就少一个。

相关推荐
戴西软件1 小时前
戴西CAxWorks.VPG车辆工程仿真软件技术解析(上)——安全仿真体系的自动化构建
运维·网络·数据库·人工智能·算法·安全·自动化
灯澜忆梦2 小时前
【MySQL10】进阶篇 | 索引_#1
数据库·sql·mysql
敲上瘾2 小时前
redis常用数据类型与操作方法
数据结构·数据库·redis·缓存
OceanWaves19932 小时前
mysql 5.7.29 主从同步配置
数据库·mysql·adb
molaoye2 小时前
win10下切换MySQL版本
数据库·mysql
丁引3 小时前
《数据清洗的艺术:如何用20行核心逻辑优雅地删除无标签图片》
前端·数据库·python
雨辰AI3 小时前
K8s 数据库 Secret 加密实战|密码明文漏洞彻底修复,等保密评双合规(金仓 / 达梦双库适配)
数据库·容器·kubernetes
XR1234567883 小时前
学校图书馆网络改造,方案谁更优?
网络·数据库·php
梦想的旅途23 小时前
企业微信自动化:自动发送文本、图片、文件
前端·数据库·microsoft
IvorySQL3 小时前
PostgreSQL 日报|复制槽泄漏修复(8 月 6 日)
数据库·postgresql