
本文首发于 栏轩·阁欢迎访问阅读原文获取更好的阅读体验。为什么用 pgvector而不是 Redis我在初学的时候先用 Redis RediSearch 实现向量检索遇到了几个痛点Redis 的向量能力是后加的RediSearch 模块对向量的支持相对有限索引类型少精度和召回率不如专门的向量方案数据类型受限Redis 的 value 结构决定了存向量要么序列化 blob、要么拆字段查询和调试都不直观维护两个存储业务数据在 PostgreSQL向量在 Redis两套存一起还得考虑数据一致性架构复杂度翻倍pgvector 的优势向量就是 PostgreSQL 的一个字段类型跟TEXT、INTEGER没区别一条 SQL 里可以同时查业务字段和向量相似度不需要跨数据源支持 L2 欧氏距离、余弦距离、内积距离支持 IVFFlat 和 HNSW 索引百万级数据毫秒响应环境搭建Docker Compose 一键部署pgvector 是 PostgreSQL 的扩展插件原生 PostgreSQL 镜像不带它。官方提供了pgvector/pgvector镜像开箱即用version:3.8services:postgres:image:pgvector/pgvector:pg17container_name:pgvector-composeenvironment:POSTGRES_USER:postgresPOSTGRES_PASSWORD:123456POSTGRES_DB:vectordbports:-5432:5432volumes:-./pgdata:/var/lib/postgresql/data如果你已有 PostgreSQL手动加插件如果你是在已有的 PostgreSQL 上中途加向量能力几步搞定# 1. 进入容器或服务器dockerexec-ityour-postgresbash# 2. 安装 pgvector 扩展Debian/Ubuntuapt-getupdateapt-getinstall-ypostgresql-17-pgvector# 3. 重启 PostgreSQLpg_ctl restart然后连上去启用扩展即可见下一节。SQL 基本操作我使用DBX作为数据库客户端。虽然是官方镜像但扩展默认未启用需要先手动开启CREATEEXTENSIONIFNOTEXISTSvector;建表向量在 PostgreSQL 里就是一个特殊的字段类型vector(dim)。假设我们要存一个 3 维向量便于手算理解建表如下-- 创建一个商品表embedding 是 3 维向量CREATETABLEproducts(idSERIALPRIMARYKEY,nameTEXT,priceDECIMAL(10,2),embedding VECTOR(3));插入数据插入向量时直接用[ ]包住浮点数即可非常直观INSERTINTOproducts(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]最相似的商品SELECTname,price,embedding[1.0, 0.0, 0.0]ASdistanceFROMproductsORDERBYdistanceASC;输出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 元的SELECTname,price,embedding-[1.0, 0.0, 0.0]ASdistanceFROMproductsWHEREprice10.0ORDERBYdistanceASC;性能优化索引当数据量增大到万级以上全表扫描就不够快了。pgvector 提供了两种索引IVFFlat倒排文件索引原理是把向量空间划分为多个桶cluster查询时只搜索最近的几个桶而不是全部数据-- 先设置 probes查询桶数默认 1SETivfflat.probes1;-- 创建索引lists 4 表示分 4 个桶建议 lists 行数 / 1000CREATEINDEXONproductsUSINGivfflat(embedding vector_l2_ops)WITH(lists4);如何理解 IVFFlat假设我有 1000 个商品创建 IVFFlat 索引lists 4后pgvector 先将 1000 个向量按距离聚成4 个桶查询时先算目标向量离哪 1 个桶最近probes 1然后只在这个桶内做精确搜索这种方式牺牲一点点精度来换取几十倍的性能提升。选择合适的 operator classvector_l2_ops— 配合-欧氏距离vector_cosine_ops— 配合余弦距离vector_ip_ops— 配合#内积距离Java 实战SpringBoot MyBatisPlus 整合理论讲完了来看看怎么在 Java 项目里用 pgvector。我会以商品相似度检索为例完整走一遍 CRUD 向量查询。项目环境Spring Boot 4.1.0MyBatis-Plus 3.5.17使用mybatis-plus-spring-boot4-starterpgvector-java 0.1.6PostgreSQL 17 pgvector 插件JDK 211. 引入依赖parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion4.1.0/version/parentdependencies!-- Spring Boot Web --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- MyBatis-Plus Spring Boot 4 Starter --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot4-starter/artifactIdversion3.5.17/version/dependency!-- PostgreSQL 驱动注意不要加 runtime scope --dependencygroupIdorg.postgresql/groupIdartifactIdpostgresql/artifactId/dependency!-- pgvector Java 客户端 --dependencygroupIdcom.pgvector/groupIdartifactIdpgvector/artifactIdversion0.1.6/version/dependency/dependencies⚠️ 注意PostgreSQL 驱动不要加scoperuntime/scope因为 pgvector 的PGvector类在编译时就依赖了 PostgreSQL JDBC 的接口PGBinaryObject设为 runtime 会导致编译失败。2. 配置数据源spring:datasource:url:jdbc:postgresql://localhost:5432/vectordbusername:postgrespassword:123456driver-class-name:org.postgresql.Drivermybatis-plus:configuration:log-impl:org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case:truetype-handlers-package:com.example.pgvectordemo.typehandler3. 实体类 —— 关键向量字段的 TypeHandler先看成品再解释原理TableName(products)publicclassProduct{TableId(typeIdType.AUTO)privateIntegerid;privateStringname;privateBigDecimalprice;// 关键指定自定义 TypeHandlerTableField(typeHandlerVectorTypeHandler.class)privatefloat[]embedding;// getter / setter / constructor ...}embedding字段的类型是 Java 的float[]但数据库里是 PostgreSQL 的vector(3)。MyBatis 默认不认识vector类型所以需要告诉它两者之间怎么转换——这就是 TypeHandler 的作用。什么是 TypeHandlerTypeHandler 是 MyBatis 的类型转换器负责 Java 类型 ↔ JDBC 类型 的双向翻译写入数据库 Java 对象 ──→ TypeHandler ──→ JDBC PreparedStatement 读取数据库 JDBC ResultSet ──→ TypeHandler ──→ Java 对象MyBatis 内置了很多常见类型的 TypeHandler比如StringTypeHandler、IntegerTypeHandler但float[]↔ PostgreSQLvector这种组合不存在所以得手写一个。TypeHandler 完整代码 逐行解析MappedTypes(float[].class)// ① 这个 Handler 处理哪个 Java 类型MappedJdbcTypes(JdbcType.OTHER)// ② 对应的 JDBC 类型OTHER 表示非标准类型publicclassVectorTypeHandlerextendsBaseTypeHandlerfloat[]{// ↑ ③ 泛型参数声明处理的是 float[]BaseTypeHandlerfloat[]要求子类实现 4 个抽象方法对应不同的读写场景方法何时调用做什么setNonNullParameterINSERT / UPDATE 时把 Java 值设到 SQL 的?占位符上getNullableResult(rs, String)按列名查询结果时从 ResultSet 读到 JavagetNullableResult(rs, int)按列索引查询结果时同上按数字索引getNullableResult(CallableStatement, int)调用存储过程时一般用不到但必须实现写入方法详解OverridepublicvoidsetNonNullParameter(PreparedStatementps,inti,float[]parameter,JdbcTypejdbcType)throwsSQLException{// 参数说明// ps — JDBC 预编译语句就是那个带 ? 的 SQL// i — 第几个 ? 占位符从 1 开始// parameter — Java 层的 float[] 值// jdbcType — JDBC 类型这里就是 JdbcType.OTHER// 第一步把 float[] 包装成 PGvector 对象PGvectorvectornewPGvector(parameter);// 等效于做了[1.0, 0.0, 0.0] → PGvector 实例// 第二步通过 JDBC 的 setObject 传给 PostgreSQLps.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])读取方法详解Overridepublicfloat[]getNullableResult(ResultSetrs,StringcolumnName)throwsSQLException{// 参数说明// rs — 查询结果集指向当前行// columnName — 列名比如 embedding// 为什么不直接 (PGvector) rs.getObject(columnName) // 因为 JDBC 不认识 vector 类型getObject() 返回的其实是个 String// 所以直接用 getString() 读取原始文本 [1.0,0.0,0.0]Stringvaluers.getString(columnName);returnparseVector(value);}// getNullableResult(rs, int columnIndex) 逻辑完全一样只是按数字取列// getNullableResult(CallableStatement, int) 是给存储过程用的关键问题为什么读的时候不直接用PGvector对象PostgreSQL JDBC 驱动的getObject()方法默认不认识vector类型——除非你手动调用PGvector.registerTypes(conn)注册类型映射。但这样就要在每次获取连接时做额外处理比较麻烦。更稳定的方案是直接读字符串[1.0,0.0,0.0]然后手动解析。解析方法详解privatefloat[]parseVector(Stringvalue){// value 格式[1.0, 0.0, 0.0]if(valuenull)returnnull;Stringtrimmedvalue.trim();// 去掉首尾的方括号if(trimmed.startsWith([)trimmed.endsWith(])){trimmedtrimmed.substring(1,trimmed.length()-1);}// 现在 trimmed 1.0, 0.0, 0.0if(trimmed.isEmpty())returnnewfloat[0];String[]partstrimmed.split(,);// parts [1.0, 0.0, 0.0]float[]resultnewfloat[parts.length];for(inti0;iparts.length;i){result[i]Float.parseFloat(parts[i].trim());// Float.parseFloat( 0.0) → 0.0}returnresult;// 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 种注册方式我们用的事务最简单的一种mybatis-plus:type-handlers-package:com.example.pgvectordemo.typehandler只要在application.yml里配了这个路径MyBatis 启动时就会自动扫描该包下的所有MappedTypes注解注册进去。然后实体类里通过TableField(typeHandler VectorTypeHandler.class)指定这个字段用哪个 HandlerMyBatis 执行 SQL 时就会自动调用对应的方法。小结什么情况需要自定义 TypeHandler只要你的 Java 类型和数据库类型没法直接对应就需要写 TypeHandler。常见场景场景Java 类型数据库类型向量检索本文float[]PostgreSQLvectorJSON 字段自定义对象 /MapPostgreSQLjsonb枚举Enum对象VARCHAR或INTEGER数组ListStringPostgreSQLTEXT[]加密字段String密文VARCHAR原理都一样继承BaseTypeHandlerT实现 4 个方法配好注解和扫描路径即可。4. Mapper基础 CRUD 向量查询MyBatis-Plus 的BaseMapper提供insert、selectById、updateById等基础方法。我们额外写两个向量相似度查询MapperpublicinterfaceProductMapperextendsBaseMapperProduct{// 余弦相似度Select( SELECT id, name, price, embedding FROM products ORDER BY embedding #{targetEmbedding}::vector LIMIT #{topN} )ListProductfindSimilarByCosine(Param(targetEmbedding)StringtargetEmbedding,Param(topN)inttopN);// 欧氏距离Select( SELECT id, name, price, embedding FROM products ORDER BY embedding - #{targetEmbedding}::vector LIMIT #{topN} )ListProductfindSimilarByEuclidean(Param(targetEmbedding)StringtargetEmbedding,Param(topN)inttopN);}注意参数要转成[1.0,0.0,0.0]::vector格式传入。5. Service继承ServiceImpl获得完整 CRUD同时封装向量查询方法ServicepublicclassProductServiceextendsServiceImplProductMapper,Product{publicListProductfindSimilarByCosine(float[]targetVector,inttopN){returnbaseMapper.findSimilarByCosine(arrayToPgvectorString(targetVector),topN);}publicListProductfindSimilarByEuclidean(float[]targetVector,inttopN){returnbaseMapper.findSimilarByEuclidean(arrayToPgvectorString(targetVector),topN);}privateStringarrayToPgvectorString(float[]arr){StringBuildersbnewStringBuilder([);for(inti0;iarr.length;i){if(i0)sb.append(,);sb.append(arr[i]);}sb.append(]);returnsb.toString();}}6. 启动运行验证项目启动后自动运行 Demo输出结果如下 pgvector MyBatis-Plus Demo Start ✅ 成功插入 4 条商品数据 全部商品列表 Product{id1, name苹果, price5.00, embedding[1.0, 0.0, 0.0]} Product{id2, name香蕉, price3.50, embedding[0.0, 1.0, 0.0]} Product{id3, name樱桃, price8.00, embedding[0.0, 0.0, 1.0]} Product{id4, name苹果派, price12.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/文件夹中。踩坑记录问题原因解决编译找不到PGBinaryObjectPostgreSQL driver scope 为 runtime去掉 scope改为默认 compilePGvector.fromSqlType()不存在这是 0.0.x 版本的老 API0.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——毕竟能少维护一个中间件就少一个。