org.springframework.ai.vectorstore.pgvector.PgVectorStore


一、前言

PgVectorStore 的本质,是 Spring AI VectorStore 抽象在 PostgreSQL + pgvector 场景下的一种具体实现。它不是一个“纯 JDBC 工具类”,也不是一个“只负责 SQL 拼接的 DAO”;它承担的是向量存储抽象层的职责:把 Document 写入 PostgreSQL 中的向量表,调用 EmbeddingModel 生成向量,借助 pgvector 距离运算符做相似度检索,再把结果重新组装回 Spring AI 的 Document。从类型层级上看,它继承 AbstractObservationVectorStore,同时实现 VectorStoreVectorStoreRetrieverDocumentWriterInitializingBean。这意味着它既是“可写”的向量库,也是“可检索”的向量库,同时还把 Micrometer 观测能力与 Spring Bean 生命周期初始化能力一起纳入了实现。

这一点非常重要,因为很多人第一次看到 PgVectorStore 时,会误以为“它只是把 pgvector 封装成一个 repository”。其实不是。PgVectorStore 的设计核心是:在 Spring AI 的统一向量数据库 API 之下,给 PostgreSQL/pgvector 提供一个实现。因此你平时调用的大多数方法,如 add(...)delete(...)similaritySearch(...),真正公开暴露给业务代码的是 VectorStore 抽象层方法;而 PgVectorStore 自己主要去实现其底层模板方法,如 doAdd(...)doDelete(...)doSimilaritySearch(...)。这也是为什么它的很多“核心实现方法”前面带 do:因为公共入口在父类,具体数据库行为落在子类。

如果继续往下看它的类说明,你会发现官方直接把它定义为**“基于 PostgreSQL 的向量存储实现,使用 pgvector 扩展”**,并强调它默认使用 public schema 下的 vector_store 表,但都可以配置。官方列出的特性很能说明它的定位:自动 schema 初始化、支持余弦/欧氏/负内积三种距离、支持 HNSW/IVFFlat/无索引三种索引策略、支持元数据过滤、支持可配置相似度阈值、支持批处理写入。这些特性连在一起看,就会明白它不是“只会查最近邻”的薄封装,而是一个完整的 Spring AI 向量库适配器。

再从接口角度说,VectorStore 的目标是屏蔽底层向量数据库差异,让调用方主要面对“加文档、删文档、按语义检索文档”这三类操作;而 PgVectorStore 则把这种抽象落实到 PostgreSQL 的数据模型与 SQL 语义上。比如,Spring AI 层面你说的是“按 query 做 similaritySearch”,落到 PgVectorStore 里就变成:先把 query 交给 EmbeddingModel 变成向量,再把向量作为 SQL 参数,配合 <=><-><#> 等 pgvector 运算符执行查询。所以理解 PgVectorStore 的关键,不只是记住它有哪些 setter,而是要明白它做了两层桥接:Spring AI 抽象 → PostgreSQL 实现,以及 Document/SearchRequest → SQL/JSONPath/pgvector operator。(Home)

还有一个很容易混淆但很值得指出的版本信息:你现在问的是 org.springframework.ai.vectorstore.pgvector.PgVectorStore,这对应的是当前 1.x 的包路径;而老一些的 0.8.x 资料里,确实能看到旧的 org.springframework.ai.vectorstore.PgVectorStore 路径。所以如果你在网上翻到旧文章、旧代码,发现 import 路径不一样,不一定是你写错,而很可能是文章基于旧版 Spring AI。这个版本背景很值得知道,否则你会在“为什么我的 IDE 找不到那个类”上浪费时间。

你可以先把它抽象成下面这个结构来记:

业务代码
   │
   ├─ 调用 VectorStore.add / similaritySearch / delete
   │
Spring AI 抽象层
   │
   ├─ AbstractObservationVectorStore(公共入口、观测封装)
   │        │
   │        └─ 调子类模板方法 doAdd / doDelete / doSimilaritySearch
   │
PgVectorStore
   │
   ├─ EmbeddingModel:把文本/查询变成向量
   ├─ JdbcTemplate:执行 PostgreSQL SQL
   ├─ pgvector 运算符:<=> / <-> / <#>
   └─ JSONPath 过滤:metadata::jsonb @@ '...'::jsonpath

把这个结构吃透后,后面所有方法你都会觉得“顺理成章”:它并不是一堆零散 API,而是在完成一个统一的职责闭环。


二、存储模型、数据库前提与初始化机制:这个类最终在 PostgreSQL 里创建了什么,以及 afterPropertiesSet() 到底做了什么

要理解 PgVectorStore,必须先理解它在数据库里假设的表模型。Spring AI 官方参考文档给出的默认 schema 非常清楚:需要 PostgreSQL 启用 vectorhstoreuuid-ossp 扩展,然后创建一个向量表,默认名为 vector_store,包含 idcontentmetadataembedding 四列,其中 embeddingvector(n),默认示例使用 1536 维;索引默认示例则是 HNSW + vector_cosine_ops。换句话说,PgVectorStore 在数据库层面实际上维护的是一个文档文本 + 元数据 JSON + 向量列的混合表,而不是只存一个孤立 embedding。

这一点对应到源码也完全一致。afterPropertiesSet() 是它的初始化入口:如果开启了 schema 初始化,它会先执行 CREATE EXTENSION IF NOT EXISTS vectorCREATE EXTENSION IF NOT EXISTS hstore,而当 idTypeUUID 时,还会执行 CREATE EXTENSION IF NOT EXISTS "uuid-ossp";随后 CREATE SCHEMA IF NOT EXISTS ...,必要时 DROP TABLE IF EXISTS ...,最后 CREATE TABLE IF NOT EXISTS ... 创建表,并在索引类型不是 NONE 时创建向量索引。也就是说,这个类的初始化逻辑不是“只校验一下配置”,而是可能真的去安装扩展、建 schema、删表、建表、建索引。所以它实现 InitializingBean 绝不是装饰性接口,而是行为非常实在的生命周期钩子。

这里最关键的一个版本变化,你一定要记住:现在 schema 初始化是显式 opt-in,而不是默认行为。Spring AI 官方文档明确说,必须通过构造/Builder 传 initializeSchema(true),或者通过配置项 spring.ai.vectorstore.pgvector.initialize-schema=true 开启;并且特别指出这是一个 breaking change,旧版本里默认会自动初始化。很多初学者在升级后最容易遇到的“类创建成功但表没建出来”的困惑,根源就在这里。不是 PgVectorStore 失效了,而是默认策略改了。

再说表结构。源码中的 CREATE TABLE 使用的是:

  • id <type> PRIMARY KEY
  • content text
  • metadata json
  • embedding vector(dimensions)

这里有几个细节很值得注意。第一,metadata 用的是 json,但查询过滤时会转成 jsonb 来走 JSONPath 表达式;第二,content 是纯文本列,所以从实现形态上看,PgVectorStore强烈偏向文本文档存储的;第三,id 列类型不是写死 UUID,而是由 PgIdType 决定。也就是说,这个类虽然默认像很多 AI 示例那样用 UUID,但它其实支持多种主键策略。

关于维度,也不能只停留在“默认 1536”这种表面理解。源码里的 embeddingDimensions() 逻辑是:如果手工设置了 dimensions 且大于 0,就优先用手工设置;否则尝试从 EmbeddingModel.dimensions() 获取;如果获取失败或返回无效值,才回退到 OPENAI_EMBEDDING_DIMENSION_SIZE = 1536。这意味着 1536 只是保底值,不是“神秘的固定真理”。很多教程把 1536 写得像定数一样,其实在当前实现里,它更像一个兼容性 fallback。

还有一个数据库层面的现实限制:Spring AI 官方文档明确提示 pgvector 的 HNSW 索引最多支持 2000 维。因此,当你把 indexType 设成 HNSW 且 embedding 维度更高时,就要格外警惕。更进一步,从 PgVectorStore 当前源码可见,它创建的是 vector(dim) 列,并未暴露 halfvec 一类更大维度方案的建模开关;而 pgvector 官方 README 也说明,距离运算符与索引操作符类是和具体向量类型/索引类型绑定的,高维场景通常要考虑半精度、二值量化或降维等策略。换言之,这个类当前的设计重心是标准 vector(n) 路线,而不是把 pgvector 全部高级特性都完全暴露出来。

如果把初始化过程画成一个简化流程,大概就是:

Spring 容器创建 PgVectorStore Bean
        │
        └─ afterPropertiesSet()
              │
              ├─ 可选:schema 校验
              ├─ 若 initializeSchema=false:直接返回
              ├─ CREATE EXTENSION vector / hstore / uuid-ossp(按需)
              ├─ CREATE SCHEMA IF NOT EXISTS <schema>
              ├─ 可选:DROP TABLE IF EXISTS <schema>.<table>
              ├─ CREATE TABLE IF NOT EXISTS ...
              └─ 可选:CREATE INDEX IF NOT EXISTS ...

所以,从“类/方法理解”的角度说,afterPropertiesSet() 不是边角料,而是这个类把 Java 世界里的 Builder 参数,真正落到数据库对象上的桥梁。如果你没把这一段吃透,就会对 initializeSchemaremoveExistingVectorStoreTableschemaNameidType 这些配置项的真正后果缺乏直觉。


三、Builder、属性项与构造参数:PgVectorStore.builder(...)

PgVectorStore 没有公开的普通构造器,对外推荐入口是静态方法 builder(JdbcTemplate, EmbeddingModel)。Javadoc 明确展示了这种用法,而源码也说明真正的构造器是 protected PgVectorStore(PgVectorStoreBuilder builder)。这意味着你平时不应该手写 new PgVectorStore(...),而是应当通过 Builder 让配置更明确、可链式组合。

先看 pgvector 专属配置项。从源码中的 PgVectorStoreBuilder 可以直接读出它有以下字段与链式方法:schemaName(...)vectorTableName(...)idType(...)vectorTableValidationsEnabled(...)dimensions(...)distanceType(...)removeExistingVectorStoreTable(...)indexType(...)initializeSchema(...)maxDocumentBatchSize(...),最后 build() 返回 PgVectorStore。这些方法基本就是这个类最核心的“控制面板”。其中默认值也值得记住:schema 默认 public,表默认 vector_store,ID 类型默认 UUID,schema 校验默认 false,dimensions 默认无效值(后续推断),距离类型默认 COSINE_DISTANCE,索引类型默认 HNSW,最大文档批量默认 10000

再看继承来的通用 Builder 能力PgVectorStoreBuilder 不是从零开始写的,它继承 AbstractVectorStoreBuilder,而 AbstractVectorStoreBuilder 又实现了 VectorStore.Builder。官方 Javadoc 明确说明 VectorStore.Builder 统一提供 observationRegistry(...)customObservationConvention(...)batchingStrategy(...)build() 这些方法。也就是说,PgVectorStoreBuilder 其实由两层配置组成:一层是 pgvector 专属的 schema / table / distance / index / batch size 等;另一层是所有向量库都通用的观测配置与嵌入批处理策略配置。很多人只记住 pgvector 那几项,却忽略 batchingStrategy(...) 也能配,这会导致对写入链路理解不完整。(Home)

maxDocumentBatchSizebatchingStrategy 特别容易混淆,我单独强调一下:它们不是一回事。源码里 doAdd(...) 先调用 embeddingModel.embed(documents, EmbeddingOptions.builder().build(), this.batchingStrategy) 生成嵌入,然后再调用 batchDocuments(documents)maxDocumentBatchSize 切分 JDBC 批量入库。也就是说:

  • batchingStrategy:作用在调用 EmbeddingModel 生成向量这一步;
  • maxDocumentBatchSize:作用在把结果写入 PostgreSQL 的 batchUpdate 这一步。

这两个批处理分别服务于两个不同阶段:一个是“模型侧请求分批”,一个是“数据库侧写入分批”。如果你把两者混为一谈,很多性能相关行为会看不懂。

再说自动配置。Spring Boot 场景下,你通常并不需要自己写 Builder;PgVectorStoreAutoConfiguration 会在满足条件时自动创建这个 Bean。Javadoc 显示它的自动配置条件包括:类路径中有 PgVectorStoreDataSourceJdbcTemplate,并启用 PgVectorStoreProperties,同时属性 spring.ai.vectorstore.type=pgvector(默认缺省也匹配)。自动配置方法 vectorStore(...) 会注入 JdbcTemplateEmbeddingModelPgVectorStorePropertiesObservationRegistry、自定义 VectorStoreObservationConvention、以及 BatchingStrategy。这说明:自动配置本质上也是在帮你调用 Builder,只不过参数来自配置属性与容器中的公共 Bean。 (Home)

PgVectorStoreProperties 则是自动配置与 application.yml 之间的桥。其配置前缀是 spring.ai.vectorstore.pgvector,并暴露了 dimensionsdistanceTypeidTypeindexTypemaxDocumentBatchSizeschemaNametableNameremoveExistingVectorStoreTableschemaValidation 等 getter/setter;而 initializeSchema 则继承自 CommonVectorStoreProperties。因此你在配置文件里写的:

spring:
  ai:
    vectorstore:
      pgvector:
        initialize-schema: true
        schema-name: knowledge
        table-name: docs
        distance-type: COSINE_DISTANCE
        index-type: HNSW
        dimensions: 1024

本质上就是在填充 PgVectorStoreProperties,再由自动配置把这些值喂给 PgVectorStore

这里还有两个很有价值的细节。第一,表名可以配,索引名当前不单独配。源码显示:若表名还是默认 vector_store,则索引名使用常量 spring_ai_vector_index;若表名自定义,则索引名自动变成 <tableName>_index。这对排查数据库对象名时很有帮助。第二,开启自定义 schema/table 时,官方文档建议同时开启 schema-validation=true,以保证对象名正确并降低 SQL 注入风险。也就是说,Spring AI 官方自己也知道这里涉及动态拼接对象名,所以给了一个防护型开关。

一个比较完整、同时又足够克制的手动配置示例如下:

@Bean
PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
    return PgVectorStore.builder(jdbcTemplate, embeddingModel)
            .schemaName("knowledge")
            .vectorTableName("article_vectors")
            .idType(PgVectorStore.PgIdType.UUID)
            .dimensions(1024) // 明确指定维度,避免依赖模型自动探测
            .distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
            .indexType(PgVectorStore.PgIndexType.HNSW)
            .initializeSchema(true) // 当前版本需要显式打开
            .vectorTableValidationsEnabled(true)
            .maxDocumentBatchSize(2000)
            .build();
}

这段代码的价值不在“会写”,而在于它帮助你建立 Builder 各配置项的语义坐标:数据库对象命名、主键类型、向量维度、距离度量、索引策略、初始化策略、写入批大小,这些正是 PgVectorStore 的核心。


四、写入链路:add(...) / doAdd(...) / insertOrUpdateBatch(...)

如果你只从表面看,vectorStore.add(documents) 很像一句平平无奇的“插入数据库”。但在 PgVectorStore 里,写入实际上分成三个阶段:公共入口 → 嵌入生成 → 批量 upsert。真正公开给业务层的是父类 AbstractObservationVectorStore.add(List<Document>);而 PgVectorStore 负责实现 doAdd(List<Document>)。这种模板方法结构的含义是:PgVectorStore 只关心“怎么对 pgvector 做 add”,而观测、公共契约、外围封装由父类处理。

源码里 doAdd(...) 的第一步不是写 SQL,而是:

  1. 调用 embeddingModel.embed(documents, EmbeddingOptions.builder().build(), this.batchingStrategy)
  2. 得到所有 Document 对应的 embeddings;
  3. 再按 maxDocumentBatchSize 把文档切分;
  4. 对每个 batch 调用 insertOrUpdateBatch(...)

这说明 PgVectorStore 写入的真正前置条件是:它自己并不接收“现成向量”,而是默认认为应该从 Document 生成向量。这也解释了它为什么必须依赖 EmbeddingModel。因此,如果有人问“为什么我只配置了数据库,没配置 embedding model 就不行”,答案很简单:因为这个类不是一个纯存储适配器,它的默认职责里包含了入库前做嵌入

insertOrUpdateBatch(...) 则揭示了更具体的写入语义。它构造的是 INSERT ... ON CONFLICT (id) DO UPDATE ... 形式的 SQL,也就是以 document id 为主键做 upsert。这点非常关键:add(...) 不是“只插不改”,而是“如果主键已存在,则更新 content、metadata、embedding”。因此,从业务语义上讲,PgVectorStore.add(...) 更接近“写入或覆盖当前版本的文档向量”,而不是传统意义上的 append-only。对于需要按固定文档 ID 反复重建索引的场景,这个行为恰好非常实用。

再往里看一层,setValues(...) 做的事情依次是:

  • Documentid
  • Documenttext
  • metadata 序列化成 JSON 字符串
  • 找到该文档对应的 embedding
  • com.pgvector.PGvector 封装向量
  • 通过 StatementCreatorUtils.setParameterValue(...) 依次绑定参数

这里有两个阅读源码时非常值得注意的点。第一,metadata 最终不是通过 ORM,而是由 ObjectMapper 序列化成 JSON 文本后再以 ?::jsonb 形式写入;所以它的元数据落库并不神秘,本质上就是 Map → JSON。第二,向量不是手写 '[1,2,3]' 这种字符串,而是通过 PGvector Java 类型绑定到 PreparedStatement 中,这也是它能和 pgvector 驱动生态正常协作的关键。

主键类型转换也值得重点看。convertIdToPgType(String id) 会根据 PgIdType 做分支:

  • UUIDUUID.fromString(id)
  • TEXT → 原样字符串
  • INTEGER / SERIALInteger.valueOf(id)
  • BIGSERIALLong.valueOf(id)

这说明 Document 层面的 ID 虽然统一表现为字符串,但一旦进入 PgVectorStore,就会被投影到 PostgreSQL 实际列类型。所以如果你把 idType 设成 INTEGER,那传入的 Document.id 也必须是能解析成整数的字符串;否则在写入前就会抛转换异常。这类问题表面上像“数据库报错”,实际上根本原因是 PgVectorStore 的类型投影规则。

还有一个源码级细节很容易被忽略:embedding 的顺序映射非常重要。Spring AI 的 BatchingStrategy Javadoc 明确说,批处理时必须保留 Document 列表顺序,因为 embeddings 是按顺序对应回原文档的;而 PgVectorStore 源码确实依赖这种顺序关系来把 Document 和 embedding 对上。换句话说,PgVectorStore 并不是靠文档内容去“猜向量是谁的”,而是靠原始列表顺序与后续 batch 子列表关系来建立映射。这一点有助于你理解:为什么框架如此强调 batching 顺序不能乱。

从“类设计”的角度看,doAdd(...) 至少体现了四个设计决策:

第一,嵌入生成属于 VectorStore 写入职责的一部分。
不是你先算好 embedding 再把它传进来,而是 PgVectorStore 默认直接接文档并自己去求 embedding。

第二,数据库写入是批处理的。
它不是每条文档一条 SQL,而是 JdbcTemplate.batchUpdate(...)

第三,写入语义是 upsert。
同一 ID 的内容、元数据、向量都会被覆盖更新。

第四,写入对象是“文本文档模型”。
因为入库列里写的是 content text,代码读取的是 document.getText()。结合 Document 当前 Javadoc 中“Document 可以是 text 或 media”的定义来看,可以合理推断:虽然 Spring AI 的 Document 抽象更广,但 PgVectorStore 当前实现形态明显是围绕文本内容设计的。这个结论是基于源码与 Document API 的推断,而不是单纯概念判断。

一个最小但有解释价值的示例:

List<Document> docs = List.of(
        Document.builder()
                .id("8fd0a5c4-0c35-4e60-a1f7-0b2f7a39d001")
                .text("Spring AI 中 PgVectorStore 使用 PostgreSQL + pgvector 做向量检索")
                .metadata("category", "spring")
                .metadata("level", "intro")
                .build(),
        Document.builder()
                .id("aa2f1d27-e8b1-48d4-b49d-76f75eb10211")
                .text("它会在 add 时自动调用 EmbeddingModel 生成向量")
                .metadata("category", "spring")
                .metadata("level", "core")
                .build()
);

// 对调用者来说只是 add;
// 对 PgVectorStore 来说是:算 embedding -> batch upsert -> 有同 ID 则更新
vectorStore.add(docs);

你真正要建立的理解不是“会用 add”,而是知道 add() 背后触发了嵌入计算、JSON 序列化、ID 类型转换、PGvector 参数绑定与 upsert 写入。掌握这一点,你才算真正读懂了 PgVectorStore 的写入面。


五、检索链路的核心:similaritySearch(...) / doSimilaritySearch(...) 如何把 SearchRequest 变成 PostgreSQL 向量查询

PgVectorStore 最值得吃透的方法,其实不是 add(...),而是 doSimilaritySearch(SearchRequest request)。因为这一步真正体现了 Spring AI 抽象如何翻译成 pgvector 查询语义。SearchRequest 是 Spring AI 的通用检索请求对象,官方 Javadoc 表明它包含最核心的几个要素:querytopKsimilarityThresholdfilterExpression。你可以用 SearchRequest.builder() 创建它,也可以在很多示例里看到快捷用法 SearchRequest.query(...)。无论哪种写法,PgVectorStore 最后面对的都是这四个核心条件。(Home)

doSimilaritySearch(...) 的执行顺序非常清楚。它先看 request.getFilterExpression() 是否为空;若有,则通过 filterExpressionConverter 转成 PostgreSQL JSONPath 过滤表达式,再拼出 AND metadata::jsonb @@ '...'::jsonpath 这段 SQL 条件。然后它计算一个名为 distance 的变量,其值是 1 - request.getSimilarityThreshold()。接下来把查询文本 request.getQuery() 送入 EmbeddingModel,得到查询向量 queryEmbedding,最后执行当前距离类型对应的 SQL 模板。你看到这里就会明白:SearchRequestPgVectorStore 中不是“到此为止的抽象对象”,而是已经被彻底翻译成了查询向量 + 距离阈值 + JSONPath 过滤 + LIMIT topK

这个 1 - similarityThreshold 的处理是全类最值得注意的细节之一。Spring AI 在 SearchRequest.Builder.similarityThreshold(...) 的通用 Javadoc 中,把阈值描述为“相似度分数的下界”,比如 0.0 表示接收全部结果,1.0 表示要求精确匹配;而 PgVectorStore 在实现上把它转成了 distance 上界,再下推到 SQL 的 WHERE ... < ? 条件里。对于余弦距离来说,这很好理解,因为 pgvector 官方说明余弦相似度可以由 1 - cosine distance 得到;所以当阈值是 0.7 时,等价于要求 cosine distance < 0.3。对负内积模式,PgVectorStore 也把 <#> 做了适配,使用 1 + (embedding <#> ?) 作为一种 distance-like 值,这样最终 score = 1 - distance 时,能回到更直观的相似度空间。

但这里也要说一个更深入的理解:这个“score = 1 - distance”并不是所有距离度量下都同样直观。 DocumentRowMapper 在组装结果时,会把查询得到的 distance 放进 metadata(key 是 DocumentMetadata.DISTANCE),然后统一调用 Document.builder().score(1.0 - distance)。对于 cosine distance,这个 score 基本可直观理解为 cosine similarity;对于 negative inner product 路径,它通过前面的 SQL 处理也能接近内积语义;但对于欧氏距离,1 - distance 只是框架层面的一种统一映射,并不天然等价于一个严格定义的“欧氏相似度”。因此,EUCLIDEAN_DISTANCE 模式下看 Document.score,一定要知道它是框架映射值,不是数学教材里的标准相似度定义。

三种距离模式的 SQL 模板,源码已经给出了核心思路:

  • EUCLIDEAN_DISTANCE<->,配 vector_l2_ops
  • NEGATIVE_INNER_PRODUCT<#>,配 vector_ip_ops
  • COSINE_DISTANCE<=>,配 vector_cosine_ops

而 pgvector 官方 README 也明确说明这些运算符与操作符类的对应关系:<-> 是 L2 distance,<#> 是 negative inner product,<=> 是 cosine distance;HNSW/IVFFlat 创建索引时则对应 vector_l2_opsvector_ip_opsvector_cosine_ops。这正好解释了为什么 PgDistanceType 不是一个只决定“排序方式”的枚举,而是同时决定了SQL 运算符、索引操作符类、以及 similarity search 模板

一个简化后的“余弦检索”心智模型,可以写成:

SELECT *, embedding <=> :queryVector AS distance
FROM knowledge.article_vectors
WHERE embedding <=> :queryVector < :maxDistance
  AND metadata::jsonb @@ '($.category == "spring")'::jsonpath
ORDER BY distance
LIMIT :topK;

如果你调用的是:

List<Document> results = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query("PgVectorStore 如何做相似度检索")
                .topK(5)
                .similarityThreshold(0.7)
                .filterExpression("category == 'spring'")
                .build()
);

那么 PgVectorStore 心里想的其实是:

  • 先把 "PgVectorStore 如何做相似度检索" 变成向量;
  • 因为 threshold=0.7,所以 distance 上限=0.3;
  • category == 'spring' 转成 JSONPath;
  • 最后让 PostgreSQL 用 pgvector 运算符去找 top 5。

此外,这个类还有一个常被忽略但很有分析价值的方法:embeddingDistance(String query)。它不是标准 similaritySearch 的替代品,因为源码显示它只是执行 SELECT embedding <op> ? AS distance FROM table,没有 ORDER BY、没有 LIMIT、没有 filter,也不返回 Document,只返回 List<Double>。它更像一个底层调试/分析工具方法:帮你看“某个 query 向量与表中每个向量的距离是多少”。所以你不应该把它当业务查询主入口,而应把它理解成 PgVectorStore 暴露出来的一个便于诊断距离分布的辅助接口。

最后再强调一句:理解 doSimilaritySearch(...) 的关键,不是会背 topKthreshold,而是要看穿这套映射:

SearchRequest
   ├─ query --------------------> EmbeddingModel.embed(query)
   ├─ similarityThreshold ------> 1 - threshold = maxDistance
   ├─ filterExpression ---------> PgVectorFilterExpressionConverter -> JSONPath
   └─ topK ---------------------> LIMIT k

一旦这个映射在你脑中成型,PgVectorStore 的检索面就彻底明白了。


六、删除与过滤表达式:delete(List<String>)delete(Filter.Expression)、文本过滤 DSL 到底是什么关系

很多人学习 PgVectorStore 时,过度关注相似度检索,却忽略了删除模型其实也很有设计含量。按照 VectorStore 接口的约定,它支持多种删除形式:按 ID 列表删除、按 Filter.Expression 删除,以及默认方法形式的字符串过滤表达式删除。AbstractObservationVectorStore 则把公共删除入口封装好,再由具体子类提供数据库侧实现。对 PgVectorStore 而言,这就落成了两种核心实现:doDelete(List<String> idList)doDelete(Filter.Expression filterExpression)

先说按 ID 删除。源码很直接:DELETE FROM <table> WHERE id = ?,通过 JdbcTemplate.batchUpdate(...) 批量执行,并同样走 convertIdToPgType(...)。所以这里的删除和写入一样,ID 字符串会按照 PgIdType 被转换成 UUID / Integer / Long / Text。因此如果你的表主键类型是 UUID,但你传了一个不合法 UUID 字符串,问题会发生在 Java 侧类型转换,而不是 PostgreSQL 侧模糊地报一个约束错误。理解这一点,排错效率会高很多。

再看按过滤条件删除。doDelete(Filter.Expression filterExpression) 会先用 filterExpressionConverter.convertExpression(...) 把 Spring AI 统一过滤表达式转成 PostgreSQL JSONPath,然后执行:

DELETE FROM <table>
WHERE metadata::jsonb @@ '<jsonpath>'::jsonpath

这说明:PgVectorStore 的过滤删除只作用于 metadata,而不是 content 列本身。换句话说,它不是一个“全文条件删除器”,而是一个“基于元数据 JSON 的条件删除器”。这与 similaritySearch(...) 中 filter 的语义是一致的:filter 从来不是针对 embedding 本身,也不是针对 content 文本做 SQL LIKE,而是针对 Document.metadata 做 JSONPath 过滤。

这里就引出一个很重要的理解:Spring AI 的 Filter.Expression 是可移植抽象,而 PgVectorStore 把它翻译成 PostgreSQL JSONPath。这不是 pgvector 原生 API 的概念,而是 Spring AI 自己为了跨向量库统一过滤语义做的抽象。官方向量数据库总览文档明确说,Spring AI 提供 portable 的 metadata filter API;而 PgVectorFilterExpressionConverter 的职责,就是把这一层通用表达式翻译成 PostgreSQL 能执行的那一种。也就是说,业务层说的是“country == ‘UK’ && year >= 2020”,数据库层最终看到的是 $.country == "UK" && $.year >= 2020 这类 JSONPath 语法。(Home)

SearchRequest.Builder 同时支持两种 filter 入口:

  • filterExpression(String textExpression)
  • filterExpression(Filter.Expression expression)

其中字符串形式是类 SQL/逻辑表达式文本,编程式形式则是 DSL 构造出来的 AST。官方 Javadoc 给了非常典型的例子:你可以用文本 "country == 'UK' && isActive == true && year >= 2020",也可以用 FilterExpressionBuilder 去组合 eqgteand 等调用。对 PgVectorStore 来说,这两种写法最终都会落到同一件事:转成统一的 Filter.Expression,再交给 PgVectorFilterExpressionConverter。因此,文本 DSL 与编程式 DSL 的差别只在调用体验,不在底层语义。

看一个删除示例最容易理解:

// 1) 按 ID 删除
vectorStore.delete(List.of(
        "8fd0a5c4-0c35-4e60-a1f7-0b2f7a39d001",
        "aa2f1d27-e8b1-48d4-b49d-76f75eb10211"
));

// 2) 按元数据条件删除(文本 DSL)
vectorStore.delete("category == 'spring' && level == 'intro'");

// 3) 按元数据条件删除(编程式 DSL)
FilterExpressionBuilder b = new FilterExpressionBuilder();
vectorStore.delete(
        b.and(
                b.eq("category", "spring"),
                b.eq("level", "intro")
        ).build()
);

这里你要形成的认识是:
第一种删除,直接命中主键;
第二、三种删除,本质上都在 metadata 上执行 JSONPath 条件匹配。(Home)

还有一个常见误解必须纠正:不少人看到 filterExpression("author in ['john', 'jill']"),会以为 PostgreSQL 真在原生支持这种写法。其实不是。Spring AI 文档与转换器源码一起看,你会发现 IN / NINPgVectorFilterExpressionConverter 中被专门展开成多个 ==||,或者在外面再套 !() 的形式。也就是说,Spring AI 允许你在 DSL 中写 in,但最终不是直接把 in 原封不动丢给 PostgreSQL JSONPath。这是框架层做的兼容性翻译。(GitHub)

总结这一节,你应该把删除与过滤理解成下面这个模型:

delete(List<String> ids)
    -> 主键删除
    -> 受 PgIdType 影响

delete(Filter.Expression) / delete(String)
    -> 先转统一 Filter.Expression
    -> 再转 PostgreSQL JSONPath
    -> 只匹配 metadata

理解它,能帮你在业务设计上少走很多弯路:比如不要幻想拿 filterExpression 去直接按 content 模糊删文档,那不是 PgVectorStore 的定位。(Home)


七、PgVectorFilterExpressionConverter

如果说 doSimilaritySearch(...) 是“检索链路的发动机”,那 PgVectorFilterExpressionConverter 就是“元数据过滤的翻译官”。官方 package summary 明确把它描述为:Filter.Expression 转换为 PgVector metadata filter expression format。而源码进一步告诉我们,这里的“format”具体就是 PostgreSQL JSONPath 表达式。(Home)

先看最核心的 doExpression(...)。它的逻辑非常直接:如果表达式类型是 IN,走 handleIn(...);如果是 NIN,走 handleNotIn(...);否则就是普通二元表达式,按“左操作数 + 运算符 + 右操作数”的方式输出。运算符映射表则由 getOperationSymbol(...) 提供,当前支持:

  • AND -> &&
  • OR -> ||
  • EQ -> ==
  • NE -> !=
  • LT -> <
  • LTE -> <=
  • GT -> >
  • GTE -> >=

这说明,Spring AI 的过滤 DSL 在落到 PostgreSQL 之前,并不是被翻译成 SQL AND/OR/=/>= 这种传统关系表达式,而是被翻译成 JSONPath 风格的逻辑与比较运算符。(GitHub)

doKey(...) 的实现更能说明问题:它把 key 渲染成 $.<key>。比如 country 变成 $.countryyear 变成 $.year。这恰好就是 PostgreSQL JSONPath 中“从当前 JSON 根对象取字段”的写法。因此你在业务层看到的:

b.eq("country", "UK")

到底层不会是 country = 'UK',而会更像:

$.country == "UK"

这就是“metadata filter”真正命中的对象:不是 SQL 表列,而是 metadata 这块 JSON 文档内部的字段路径。(GitHub)

IN/NIN 的处理尤其值得细看,因为它非常体现框架层适配思路。handleIn(...) 并不会输出一个 PostgreSQL 原生的 in (...),而是把右侧 List 逐个展开成:

($.author == "john" || $.author == "jill")

NIN 则是在外面包一层 !()。这说明 PgVectorFilterExpressionConverter 的目标并不是“语法长得像你写的一样”,而是“保证语义在 PostgreSQL JSONPath 下可执行”。你在上层 DSL 中享受到的简洁表达,本质上是转换器在下层替你做了等价展开。(GitHub)

值的序列化策略也很有含金量。源码里的 doSingleValue(...) 表示:普通值通过 JSON 序列化输出;Date 类型则先转成 ISO-8601 字符串,再写入 JSONPath 表达式。这意味着你在 metadata 里做布尔、数字、字符串比较时,转换器会尽量按 JSON 语义输出;而日期则采用统一字符串表示以保持一致性和可读性。所以它不是简单的字符串拼接器,而是带有一定类型意识的表达式序列化器。(GitHub)

把这个转换过程配上例子会更直观。假设你写:

FilterExpressionBuilder b = new FilterExpressionBuilder();

Filter.Expression exp = b.and(
        b.in("author", "john", "jill"),
        b.gte("year", 2024)
).build();

PgVectorFilterExpressionConverter 的思路大致会变成:

(($.author == "john" || $.author == "jill") && $.year >= 2024)

然后 PgVectorStore 再把它包进:

metadata::jsonb @@ '<jsonpath>'::jsonpath

所以完整 SQL 条件部分就成了:

... AND metadata::jsonb @@ '(($.author == "john" || $.author == "jill") && $.year >= 2024)'::jsonpath

你会发现,真正的核心不是“Spring AI 允许写 filter”,而是它把跨数据库通用表达式落地成了 PostgreSQL JSONPath。这正是 PgVectorStore 可移植性与数据库特异性结合的地方。(GitHub)

不过这里也要提醒一个很现实的边界:PgVectorFilterExpressionConverter 当前重点是元数据字段过滤,不是一个通用 SQL 表达式引擎。它不会帮你过滤 content 全文,也不会帮你表达任意 PostgreSQL 自定义函数调用。它服务的是一个非常具体的目标:让 Document.metadata 里的结构化字段,能以可移植方式参与检索与删除条件。这就是为什么它输出的是 $.key == value 这种 JSONPath,而不是任意 SQL。(Home)

如果你把这一层想明白,PgVectorStore 的过滤体系就彻底连上了:

业务代码
  └─ "author in ['john','jill'] && year >= 2024"
        │
        └─ SearchRequest.Builder.filterExpression(String)
                │
                └─ 解析成 Filter.Expression
                        │
                        └─ PgVectorFilterExpressionConverter
                                │
                                └─ ($.author == "john" || $.author == "jill") && $.year >= 2024
                                        │
                                        └─ metadata::jsonb @@ '...'::jsonpath

这就是为什么我说:不读转换器,你对 PgVectorStore 的理解永远会停留在“能过滤”,而达不到“知道怎么过滤、过滤的到底是什么、为什么这样实现”的层次。 (GitHub)


八、内部枚举与辅助方法:PgDistanceTypePgIndexTypePgIdTypeembeddingDistance()getNativeClient()

PgVectorStore 里最重要的三组嵌套枚举,分别对应三个维度的决策:

  • 怎么算距离PgDistanceType
  • 怎么建索引PgIndexType
  • 主键列用什么类型PgIdType

这三个枚举不是装饰性的“参数枚举”,而是会直接改变 SQL 生成、表结构生成和结果语义的。

先说 PgDistanceType。当前实现有三个值:EUCLIDEAN_DISTANCENEGATIVE_INNER_PRODUCTCOSINE_DISTANCE。源码显示,每个枚举值都绑定了三样东西:

  1. 一个 PostgreSQL 距离运算符;
  2. 一个索引操作符类;
  3. 一个 similarity search SQL 模板。

例如:

  • 欧氏距离:<-> + vector_l2_ops
  • 负内积:<#> + vector_ip_ops
  • 余弦距离:<=> + vector_cosine_ops

这意味着 distanceType(...) 一旦改变,变的不是“排序算法标签”,而是查询运算符、索引选择、阈值比较模板、观测 metric 映射这一整套行为。再结合 pgvector 官方 README 对运算符的说明,就更容易明白:PgDistanceTypePgVectorStore 里最核心的行为开关之一。

关于如何选它,源码和文档给了很清晰的暗示。类说明与参考文档都说:默认是 COSINE_DISTANCE;若向量已经归一化到长度 1,NEGATIVE_INNER_PRODUCT 往往性能更好;而 EUCLIDEAN_DISTANCE 则是标准 L2 距离。这里不要机械背诵“OpenAI 用 inner product”,而要真正理解其依据:pgvector README 明确写了 <#> 返回的是 negative inner product,而源码又专门把它变换成 1 + (embedding <#> ?) 的 distance-like 值,从而让阈值和 score 映射更统一。

再说 PgIndexType。Javadoc 和源码都表明它当前支持:

  • NONE
  • IVFFLAT
  • HNSW

其中 NONE 表示不创建近似索引,相当于保留 exact nearest neighbor 路径;IVFFLAT 是典型近似最近邻索引,构建更快、内存更省,但查询速度与召回折中较明显;HNSW 是当前默认值,构建更慢、内存更高,但查询性能通常更优,而且不像 IVFFlat 那样需要训练步骤,可在空表上创建。这里的关键不是死记“哪个好”,而是认识到:PgVectorStore 默认在“更偏搜索性能”的一侧做了选择

PgIdType 也不能小看。它支持 UUIDTEXTINTEGERSERIALBIGSERIAL。源码中的 getColumnTypeName() 会把它翻译为建表 SQL 里的主键列定义,其中 UUID 模式是 uuid DEFAULT uuid_generate_v4()SERIALBIGSERIAL 则使用 PostgreSQL 自增类型。也就是说,PgVectorStore 并没有强制你一定使用 UUID 文档主键;相反,它允许你把向量表更好地融入现有数据库主键风格。只是你要意识到,一旦改了 idType,写入和删除时的 ID 字符串解析规则也会一起变化。

除了枚举,几个辅助方法也很值得认识。

1)getDistanceType()

这个方法表面上只是 getter,但它常用来确认当前 PgVectorStore 使用的距离度量。因为在实际排查“为什么返回分数怪怪的”“为什么索引 SQL 这样生成”时,第一件事就是确认 distance type。

2)embeddingDistance(String query)

前面已经提过,它返回的是表中各向量到 query 向量的距离列表,不做结果文档封装,也没有排序和 topK。它更适合作为诊断工具而不是主业务入口。

3)getNativeClient()

这是一个非常实用的“逃生舱”方法。Javadoc 明确说它返回底层 native client;对 PgVectorStore 来讲,这个 native client 就是 JdbcTemplate。源码里也是把 this.jdbcTemplate 包装成 Optional 返回。其意义在于:当 VectorStore 通用 API 不足以覆盖你要做的 PostgreSQL 特性时,你仍然可以拿到底层 JDBC 客户端做原生操作。比如你要加自定义 SQL、做一些 pgvector 特定管理命令、或者查询额外统计信息,这个方法就很有用。

4)createObservationContextBuilder(String operationName)

这不是业务开发最常直接调用的方法,但它对理解类的“观测友好性”很重要。源码表明它会在观测上下文里写入 provider=PG_VECTOR、collectionName=表名、namespace=schema 名、dimensions=向量维度、similarityMetric=当前距离类型映射值。也就是说,PgVectorStore 并不是一个“黑箱数据库适配器”,而是把自己运行时的重要上下文也暴露给了 Micrometer 观测体系。

如果把这几个点合起来,你会发现 PgVectorStore 的内部设计相当完整:

PgDistanceType  -> 决定运算符、索引 ops、查询模板、观测 metric
PgIndexType     -> 决定是否建 ANN 索引以及建哪种索引
PgIdType        -> 决定主键列类型与 ID 解析方式
getNativeClient -> 暴露 JdbcTemplate,允许原生 PostgreSQL 操作
embeddingDistance -> 暴露底层距离诊断能力

这几个元素加在一起,才构成了 PgVectorStore 的“可调行为面”。如果只把它理解为一个 add + search 的二方法类,那就太低估这个类了。


九、自动配置与手动配置怎么选

从使用方式上,PgVectorStore 有两条路:Spring Boot 自动配置,或者手动 Builder 配置。前者适合常规项目,后者适合你需要更明确地控制 Bean 构建细节。Javadoc 已经说明,PgVectorStoreAutoConfiguration 会在类路径存在 PgVectorStoreDataSourceJdbcTemplate 等条件下生效,并注入 EmbeddingModelPgVectorStorePropertiesObservationRegistryVectorStoreObservationConventionBatchingStrategy 等依赖来创建 Bean。也就是说,只要你的依赖与配置正确,绝大多数情况下根本不用自己 newbuilder()。(Home)

最简自动配置通常长这样:

<!-- 向量库存储 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>

<!-- 任选一种 EmbeddingModel,这里只是示意 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres

  ai:
    vectorstore:
      pgvector:
        initialize-schema: true
        schema-name: public
        table-name: vector_store
        distance-type: COSINE_DISTANCE
        index-type: HNSW
        dimensions: 1536
        max-document-batch-size: 10000

然后你直接注入 VectorStorePgVectorStore 即可。上面这些依赖名和配置键名都来自当前官方文档与 Javadoc,而不是旧版本博客中的历史写法。

但如果你的目标是“彻底理解类/方法”,我反而建议你先看一版手动配置,因为它最能暴露 PgVectorStore 的真实构成:

@Configuration
public class VectorStoreConfig {

    @Bean
    PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
        return PgVectorStore.builder(jdbcTemplate, embeddingModel)
                .schemaName("knowledge")
                .vectorTableName("docs")
                .idType(PgVectorStore.PgIdType.UUID)
                .dimensions(1024)
                .distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
                .indexType(PgVectorStore.PgIndexType.HNSW)
                .initializeSchema(true)
                .vectorTableValidationsEnabled(true)
                .maxDocumentBatchSize(2000)
                .build();
    }
}

这段代码不是为了“实战炫技”,而是为了让你把 PgVectorStore 的构造参数全部在一眼内看清楚:它既需要数据库访问能力JdbcTemplate),也需要嵌入能力EmbeddingModel),再加一组围绕 schema / table / id / distance / index / init / batch 的配置,最终才构成一个完整的向量库实例。

再配合一段写入与检索代码,你就会更容易把“方法职责”串起来:

@Service
public class KnowledgeService {

    private final PgVectorStore pgVectorStore;

    public KnowledgeService(PgVectorStore pgVectorStore) {
        this.pgVectorStore = pgVectorStore;
    }

    public void indexDocs() {
        List<Document> docs = List.of(
                Document.builder()
                        .id("8fd0a5c4-0c35-4e60-a1f7-0b2f7a39d001")
                        .text("PgVectorStore 会在 add 时自动调用 EmbeddingModel")
                        .metadata("topic", "spring-ai")
                        .metadata("level", "basic")
                        .build(),
                Document.builder()
                        .id("aa2f1d27-e8b1-48d4-b49d-76f75eb10211")
                        .text("similaritySearch 会把 query 文本先转成 embedding")
                        .metadata("topic", "spring-ai")
                        .metadata("level", "advanced")
                        .build()
        );

        pgVectorStore.add(docs);
    }

    public List<Document> search(String question) {
        return pgVectorStore.similaritySearch(
                SearchRequest.builder()
                        .query(question)
                        .topK(5)
                        .similarityThreshold(0.7)
                        .filterExpression("topic == 'spring-ai'")
                        .build()
        );
    }
}

这段代码的“教学价值”在于:

  • add(...) 触发嵌入生成与 upsert;
  • similaritySearch(...) 触发 query embedding、distance threshold 转换、metadata filter 转换;
  • SearchRequest 是检索控制面的核心对象;
  • Document 是写入/返回的统一载体。

如果你还想更直观看底层结果,可以再补一个辅助方法:

public void printNativeJdbcTemplate() {
    pgVectorStore.getNativeClient().ifPresent(jdbc -> {
        JdbcTemplate nativeJdbc = (JdbcTemplate) jdbc;
        Integer count = nativeJdbc.queryForObject("select count(*) from knowledge.docs", Integer.class);
        System.out.println("当前向量文档数: " + count);
    });
}

这段代码恰好能帮你理解 getNativeClient() 的意义:它不是鼓励你绕过 VectorStore,而是在需要 PostgreSQL 原生能力时给你一个正式出口。

因此,自动配置与手动配置并不是“谁更高级”的关系,而是:

  • 自动配置:更省事,更符合 Spring Boot 习惯;
  • 手动配置:更有助于建立对 PgVectorStore 类构造面的理解。

对于你这次的目标——把类和方法吃透——我建议脑中优先保留手动 Builder 图景,再把它映射回自动配置属性。这样最稳。


十、总结

第一,PgVectorStore 不是单纯的 SQL Repository,而是“嵌入生成 + 向量写入 + 相似检索 + 元数据过滤”的组合实现。
这也是为什么它同时依赖 EmbeddingModelJdbcTemplate,并继承了 AbstractObservationVectorStore。只从数据库角度看它,会低估这个类;只从 AI 角度看它,又会忽略它其实非常 PostgreSQL-centric。它本质上是两者的桥。

第二,当前版本里,schema 初始化不是默认行为。
这件事太重要,值得再说一遍。你必须显式打开 initializeSchema,否则 afterPropertiesSet() 会直接跳过建扩展、建表、建索引。很多“配置明明没错却搜不到结果”的问题,不是检索逻辑错了,而是表根本没初始化。

第三,maxDocumentBatchSize 不等于 batchingStrategy
前者是 JDBC 入库批大小,后者是 EmbeddingModel 请求批策略。把这两个区分开,是理解 doAdd(...) 的关键。

第四,similarityThresholdPgVectorStore 里会被翻译成 distance 上界,而不是事后纯 Java 过滤。
对 cosine distance 来说,这个映射很自然:similarity = 1 - distance;对 inner product 路径,源码也做了适配;对 euclidean 模式,则要更谨慎地理解 score。所以别把不同距离模式下的 Document.score 一概当作同一种数学含义。

第五,metadata filter 命中的是 Document.metadata,不是 content。
你写的 filterExpression(...) 最终进入的是 metadata::jsonb @@ '...'::jsonpath。因此它是“结构化元数据约束”,不是“正文全文条件”。这个边界不分清,使用姿势就会错。

第六,add(...) 是 upsert,不是 append-only。
相同 ID 会覆盖内容、元数据和 embedding。若你在做重建索引,这很方便;但若你误以为每次 add 都会保留历史版本,就会理解错误。

第七,表名可配,但当前源码里索引名是自动派生,不是独立 Builder 项。
默认表名配默认索引名 `spring_ai_vect

第八,PgVectorStore 当前实现形态明显是文本导向的。
虽然 Spring AI 的 Document 抽象支持 text 或 media,但 PgVectorStore 的表结构有 content text,写入代码取的是 document.getText()。因此若你把它当“任意模态文档统一向量仓”来理解,就会偏离这个类当前实现的真实重心。这个判断来自 Document API 与 PgVectorStore 源码的合并阅读。

第九,想读旧资料时,要意识到旧版包路径不同。
老版本资料里可能是 org.springframework.ai.vectorstore.PgVectorStore;你现在讨论的则是 org.springframework.ai.vectorstore.pgvector.PgVectorStore。查资料时一定要带着版本意识。

如果把整篇内容最终压缩成一句“读懂这个类的总纲”,我会这样说:

PgVectorStore 是 Spring AI 向量存储抽象在 PostgreSQL/pgvector 上的完整落地:它用 Builder 决定表/主键/索引/距离策略,用 EmbeddingModel 负责入库前与查询前的向量生成,用 JdbcTemplate 执行 upsert/删除/近邻检索,用 JSONPath 承接 metadata filter,再把数据库结果还原成 Spring AI Document


十一、先从“调用链”看方法分层:为什么你看到的是 add(...),源码里却重点实现 doAdd(...)

如果只看业务代码,你通常写的是:

vectorStore.add(documents);
vectorStore.delete(ids);
vectorStore.similaritySearch(searchRequest);

但如果你去读 PgVectorStore 源码,会发现真正被这个类重点实现的是 doAdd(...)doDelete(...)doSimilaritySearch(...),而不是这些“看起来最常用”的公共方法。这不是命名习惯问题,而是 Spring AI VectorStore 的模板方法设计:公共入口主要由 AbstractObservationVectorStore 负责,它统一封装观测、上下文、外围约束;而像 PgVectorStore 这样的具体存储实现,只需要把数据库相关的核心动作落在 doXxx(...) 里。AbstractObservationVectorStore 的 Javadoc 直接把这些 doAdddoDeletedoSimilaritySearch 说明为“实际执行”的模板方法。

这层分工的意义很大。它意味着:你在业务上调用的是统一抽象接口 VectorStore,而数据库特性是通过子类模板方法注入进去的。所以理解 PgVectorStore 时,不要只盯着“这个类有哪些 public 方法”,而要看它在模板方法层面究竟实现了什么。你可以把它想成两层:
上层是“通用向量库契约”,也就是 VectorStoreVectorStoreRetriever
下层是“PostgreSQL/pgvector 的具体落地”,也就是 PgVectorStore。Spring AI 的向量数据库总览文档也明确说明,VectorStore 是抽象 API,而 VectorStoreRetriever 是只读检索接口。

把这个思路再具体一点,你可以用下面的流程图来理解:

业务代码
   │
   ├─ vectorStore.add(docs)
   ├─ vectorStore.delete(ids / filter)
   └─ vectorStore.similaritySearch(request)
            │
            ▼
AbstractObservationVectorStore
   │
   ├─ 做统一入口封装
   ├─ 记录 observation 上下文
   └─ 调子类模板方法
            │
            ▼
PgVectorStore
   │
   ├─ doAdd(...)                -> 生成 embedding + 批量 upsert
   ├─ doDelete(List<String>)    -> 按主键批量删除
   ├─ doDelete(Filter.Expression) -> 按 metadata 过滤删除
   └─ doSimilaritySearch(...)   -> query 向量化 + pgvector SQL 检索

这样一来,很多源码细节都好解释了。比如为什么 PgVectorStore 会有 createObservationContextBuilder(...) 这种你平时几乎不直接调用的方法?因为它不是一个孤立组件,而是挂在 AbstractObservationVectorStore 这套“带观测能力的模板框架”之下。源码里它会把 provider、namespace、collectionName、dimensions、similarityMetric 等信息填进 VectorStoreObservationContext.Builder。这不是“顺手写个 getter”,而是为了让所有向量库实现都能被同一套观测体系感知。

对“方法理解”来说,最重要的结论是:

第一,真正决定 PostgreSQL 行为的,是 doAdd / doDelete / doSimilaritySearch
第二,真正决定运行期观测与统一调用语义的,是父类 AbstractObservationVectorStore
第三,PgVectorStore 不是一个只暴露 SQL 方法的小类,而是一个被放在 Spring AI 向量抽象体系中的数据库实现。

如果你以后阅读别的 Spring AI 向量库实现,比如 Elasticsearch、Milvus、OpenSearch,也会看到非常相似的方法结构:都实现同样的模板方法,但各自落到自己的数据库/搜索引擎语义里。这恰好说明 PgVectorStore 的读法应该是:先看模板分层,再看 PostgreSQL 细节。([Home][5])


十二、公开入口与关键方法:builder(...)getDistanceType()doAdd(...)doDelete(...)doSimilaritySearch(...)embeddingDistance(...)

这一节我直接按“方法说明书”的方式来讲。

1)public static PgVectorStoreBuilder builder(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel)

这是 PgVectorStore 最重要的实例构造入口。Javadoc 与源码都表明,PgVectorStore 对外推荐通过 builder(...) 创建,而不是直接 new。Builder 至少需要两个核心依赖:JdbcTemplateEmbeddingModel。前者代表它会直接执行 PostgreSQL SQL;后者代表它在写入文档和处理查询文本时,会自己去生成 embedding。这个方法的意义不是“语法糖”,而是明确表达:PgVectorStore 的存在前提,是同时拥有数据库访问能力和嵌入能力。 ([Home][1])

你可以把它理解为“类的根入口”。因为 Builder 里几乎集中了这个类所有重要配置:schema、table、idType、distanceType、indexType、dimensions、initializeSchema、maxDocumentBatchSize 等。也正因为此,读懂 builder(...),等于读懂 PgVectorStore 的构造面。


2)public PgDistanceType getDistanceType()

表面看只是一个 getter,但它的重要性远高于一般 getter。因为 distanceTypePgVectorStore 中不是“展示信息”,而是直接影响:

  • 使用哪个 pgvector 距离运算符;
  • 使用哪个索引操作符类;
  • similarity search 的 SQL 模板长什么样;
  • observation 里上报什么 similarity metric。

也就是说,getDistanceType() 返回的不是一个无关痛痒的字段,而是当前这个 PgVectorStore 的“检索数学语义”。如果你的项目出现“为什么分数看起来不直观”“为什么 HNSW 建索引时生成了 vector_ip_ops 而不是 vector_cosine_ops”之类的问题,最先确认的就应该是这个值。


3)public void doAdd(List<Document> documents)

这是写入链路的核心方法。源码清楚地显示,它做的第一件事不是插入数据库,而是调用:

this.embeddingModel.embed(documents, EmbeddingOptions.builder().build(), this.batchingStrategy)

然后它会把得到的 embeddings 与原始 Document 列表对应起来,再按 maxDocumentBatchSize 切分文档,最后调用 insertOrUpdateBatch(...) 执行 JDBC 批量 upsert。换句话说,doAdd(...) 的本质是“先嵌入,再批量写入”,而不是“直接插文档”。

这个方法至少体现了四层语义。

第一层,PgVectorStore 默认自己负责 embedding 生成
调用者传进来的不是“文档+向量对”,而是 Document;向量由它自己通过 EmbeddingModel 算出来。

第二层,它区分 embedding 批处理和数据库写入批处理
源码里同时存在 batchingStrategymaxDocumentBatchSize:前者给 EmbeddingModel.embed(...) 用,后者给 JDBC batch 用。这说明“模型请求分批”与“数据库写入分批”是两个阶段,不是一回事。VectorStore.Builder 的通用 Javadoc 也明确列出 batchingStrategy(...) 是一个通用可配置项。

第三层,它的落库语义是 upsert,不是 append-only
insertOrUpdateBatch(...) 用的是 INSERT ... ON CONFLICT (id) DO UPDATE ...。因此同一个文档 ID 再写一次,会覆盖 content、metadata 和 embedding,而不是新追加一条历史版本。

第四层,它显式把 Document 投影为 id + text + metadata + embedding 四个要素
也就是说,这个类的“文档模型”不是抽象到无边界的,它当前实现非常明确地围绕文本和 metadata JSON 来建模。

一个有解释价值的最小示例是:

List<Document> docs = List.of(
        Document.builder()
                .id("8fd0a5c4-0c35-4e60-a1f7-0b2f7a39d001")
                .text("PgVectorStore 会在 add 时先调用 EmbeddingModel")
                .metadata("topic", "spring-ai")
                .build()
);

pgVectorStore.add(docs);

对业务代码来说这就是一句 add
但对 PgVectorStore 来说,这等价于:

1. 使用 EmbeddingModel 为 docs 生成 embedding
2. 按 maxDocumentBatchSize 切分文档
3. 将 metadata 序列化为 JSON
4. 将向量包装成 PGvector
5. 执行 INSERT ... ON CONFLICT ... DO UPDATE

所以真正把 doAdd(...) 读懂,才算把“入库”理解透。


4)public void doDelete(List<String> idList)

这个方法看起来最简单,但其实也有两个关键点。源码中它执行的是:

DELETE FROM <schema.table> WHERE id = ?

并通过 JdbcTemplate.batchUpdate(...) 做批量删除。第一关键点是:它是按主键批量删,不是循环单条删。第二关键点是:ID 会先经过 convertIdToPgType(...)PgIdType 做类型转换。如果 idTypeUUID,会转成 UUID.fromString(...);如果是 INTEGER / SERIAL,会转成 IntegerBIGSERIAL 则是 Long

因此,这个方法真正的语义不是“接收一组字符串然后删除”,而是“接收一组 Document 层面的字符串 ID,并将它们投影到 PostgreSQL 实际主键类型后批量删除”。这也是为什么你在业务代码里感觉一切都是 String,但数据库层却能自然对应 UUID、serial、bigserial 等不同主键设计。


5)protected void doDelete(Filter.Expression filterExpression)

这是很多人第一次读源码时会忽略的重要方法。它的存在表明:PgVectorStore 不仅支持按主键删,也支持按元数据过滤条件删。实现上,它会先调用 filterExpressionConverter.convertExpression(filterExpression),把 Spring AI 统一的 Filter.Expression 转成 PostgreSQL JSONPath 表达式,然后执行:

DELETE FROM <schema.table>
WHERE metadata::jsonb @@ '<jsonpath>'::jsonpath

这说明两件事:

第一,PgVectorStore 的 filter 删除目标是 metadata 字段,不是正文 content。
第二,Spring AI 的 portable filter API 到 PostgreSQL 这里,真正的落地形态是 JSONPath

也就是说,当你写:

vectorStore.delete("topic == 'spring-ai' && level == 'basic'");

真正被 PostgreSQL 执行的并不是 SQL WHERE topic = ...,而是“metadata 这个 JSON 文档里,$.topic$.level 满足相应条件”。这点如果不分清,会误以为 PgVectorStore 的 filter 是对整张表任意列的 SQL 过滤;其实不是,它是 metadata JSON 过滤


6)public List<Document> doSimilaritySearch(SearchRequest request)

这是全类最核心的方法,没有之一。因为它真正定义了“Spring AI 抽象检索请求如何变成 PostgreSQL 向量检索 SQL”。源码流程是:

  1. request.getFilterExpression() 不为空,则先转成 PostgreSQL JSONPath;
  2. 若过滤表达式存在,则拼出 AND metadata::jsonb @@ '...'::jsonpath
  3. 计算 double distance = 1 - request.getSimilarityThreshold()
  4. EmbeddingModel.embed(query) 把查询文本转成向量;
  5. 套用当前 PgDistanceType 对应的 SQL 模板执行查询。

这里最值得注意的是第 3 步。SearchRequest.Builder 的通用 Javadoc 把 similarityThreshold 解释为相似度阈值,并提到它是“客户端后处理”的概念;但当前 PgVectorStore 实现里,阈值被直接转换成了 SQL 距离上界,也就是 pushed down 到数据库查询条件里。换句话说,在通用抽象上它是“相似度阈值”,而在 PgVectorStore 实现上它被翻译成了服务器端 distance filter。这是一个很重要的实现细节,也说明具体向量库实现可以比抽象接口文档做得更激进、更高效。([Home][8])

再看三种距离类型对应的 SQL 模板,源码给得非常直接:

  • EUCLIDEAN_DISTANCEembedding <-> ?
  • NEGATIVE_INNER_PRODUCTembedding <#> ?,但模板里做了 1 + (...) 处理
  • COSINE_DISTANCEembedding <=> ?

这和 pgvector 官方对运算符及索引操作符类的说明是对应的:pgvector 支持精确与近似最近邻检索,并支持 L2、inner product、cosine distance 等距离/相似度路线;vector_l2_opsvector_ip_opsvector_cosine_ops 也是对应的索引操作符类。([GitHub][9])

一个最小但足够说明问题的检索示例:

List<Document> docs = pgVectorStore.similaritySearch(
        SearchRequest.builder()
                .query("PgVectorStore 如何执行相似度搜索")
                .topK(5)
                .similarityThreshold(0.7)
                .filterExpression("topic == 'spring-ai'")
                .build()
);

它在 PgVectorStore 中等价于:

1. 把 query 文本嵌入成向量
2. 计算 maxDistance = 1 - 0.7 = 0.3
3. 把 "topic == 'spring-ai'" 转成 metadata JSONPath
4. 用当前 distanceType 的 SQL 模板检索 top 5
5. 用 DocumentRowMapper 把结果装回 Document

所以,理解 doSimilaritySearch(...),本质上就是理解 SearchRequest -> 向量 + 距离阈值 + JSONPath + LIMIT 的映射。


7)public List<Double> embeddingDistance(String query)

这个方法很容易被误解成“另一种 search”。其实不是。源码中它只执行:

SELECT embedding <operator> ? AS distance FROM <table>

然后返回一组 Double 距离值。它没有 ORDER BY,没有 LIMIT,没有 filter,也不返回 Document。因此更准确地说,它是一个底层距离诊断方法。当你想看“这个 query 和表中所有向量的距离分布大概怎样”时,它有价值;但它不是常规业务检索入口。


十三、结果是如何被“还原成 Document”的:DocumentRowMapperdistancescore、metadata 注入

如果只看 doSimilaritySearch(...),你会知道数据库查出来了;但还差最后一块拼图:数据库结果是如何变成 Spring AI Document 的? 这一步由 DocumentRowMapper 完成。它是 PgVectorStore 的一个私有静态内部类,实现了 RowMapper<Document>。源码中它读取四类值:

  • id
  • content
  • metadata
  • distance

这一步很关键,因为它说明检索结果里除了原文档字段,PgVectorStore 还显式把“本次查询得到的距离值”带回来了。更具体地说,DocumentRowMapper.mapRow(...) 会把 metadata 列取出来当作 PGobject 读取,再反序列化成 Map;然后把一个额外条目放进去:metadata.put(DocumentMetadata.DISTANCE.value(), distance)。随后它调用:

Document.builder()
    .id(id)
    .text(content)
    .metadata(metadata)
    .score(1.0 - distance)
    .build();

也就是说,距离既会被塞回 metadata,也会被再映射成 Document.score

这件事非常值得深挖,因为它直接关系到“为什么我拿到的 Document 里有 score,它到底是什么意思”。答案是:在 PgVectorStore 中,score 并不是数据库原生直接返回的某个字段,而是由 distance 经过 1.0 - distance 计算出来的。这对余弦距离最直观,因为余弦相似度本来就常写作 1 - cosine distance;对负内积路径,源码在 SQL 模板里也做了调整;但对欧氏距离,你必须知道这只是框架统一映射值,并不等于一个教科书式定义的“欧氏相似度”。

换句话说,如果你在业务层做:

for (Document doc : docs) {
    System.out.println(doc.getScore());
    System.out.println(doc.getMetadata());
}

那么你看到的通常是:

  • score:框架级统一相似度样式分数;
  • metadata["distance"]:数据库查询时算出来的原始 distance。

这对于调试非常有帮助。因为当你怀疑“为什么某些文档虽然进了 topK,但 score 看着很低”时,可以同时看 distancescore,再结合当前 distanceType 判断是不是数学语义理解错了。尤其在 EUCLIDEAN_DISTANCE 模式下,这种区分会特别重要。

另一个很有价值的细节是 metadata 的处理方式。写入时,PgVectorStoreDocument.metadata 序列化成 JSON;读取时,DocumentRowMapper 再把 PostgreSQL 的 PGobject 反序列化回 Map。这意味着你在 Java 层看到的 metadata,和 PostgreSQL 中存储的 JSON 文档是一一对应的,中间没有某种复杂 ORM 魔法。也因此,Spring AI 的 filter 表达式之所以能落到 JSONPath 上,是因为 metadata 在库里本来就是 JSON 文档。

你可以用下面这个流程来记忆:

similaritySearch SQL 结果集
   │
   ├─ id ---------> Document.id
   ├─ content ----> Document.text
   ├─ metadata ---> 反序列化为 Map
   ├─ distance ---> metadata["distance"]
   └─ score ------> 1.0 - distance

这段流程看似简单,但它恰恰解释了 PgVectorStore 检索输出为何仍能统一成 Spring AI 的 Document:因为它不是把数据库行原样扔给调用者,而是通过 DocumentRowMapper 做了一次框架语义还原


十四、初始化与内部辅助方法:afterPropertiesSet()embeddingDimensions()getNativeClient()createObservationContextBuilder(...)

1)public void afterPropertiesSet()

这是 InitializingBean 的实现,也是这个类最“有后果”的生命周期方法。源码显示,它的执行过程大致是:

  1. 记录初始化日志;
  2. 若开启了 schemaValidation,则调用 schemaValidator.validateTableSchema(...)
  3. initializeSchema 为 false,则直接返回;
  4. 否则执行 CREATE EXTENSION IF NOT EXISTS vector
  5. 执行 CREATE EXTENSION IF NOT EXISTS hstore
  6. idType == UUID,则额外执行 CREATE EXTENSION IF NOT EXISTS "uuid-ossp"
  7. CREATE SCHEMA IF NOT EXISTS <schema>
  8. removeExistingVectorStoreTable 为 true,则 DROP TABLE IF EXISTS <table>
  9. CREATE TABLE IF NOT EXISTS ...
  10. indexType != NONE,则 CREATE INDEX IF NOT EXISTS ...

这说明 afterPropertiesSet() 不是轻量钩子,而是一个能真实改动数据库对象的初始化过程。也因此,Spring AI 官方文档才特别强调,当前版本的 schema 初始化需要显式开启 initialize-schema=true;否则即使 Bean 成功创建,也不会自动替你建扩展、建表、建索引。

这个方法最容易被误解的地方有两个。
第一,很多人以为 initializeSchema=false 只是“不删表”,其实它是整个初始化路径直接跳过
第二,很多人以为建表时索引总会创建,但源码明确写了:indexType == NONE 时不会创建索引。也就是说,“是否有 ANN 索引”是这个生命周期方法里动态决定的,而不是写死行为。


2)int embeddingDimensions()

这个方法虽然不是公开 API,但非常关键,因为它决定了表结构里的 vector(n) 维度。源码逻辑是:

  • 若手工设置的 dimensions > 0,直接用它;
  • 否则尝试调用 embeddingModel.dimensions()
  • 若仍拿不到合法值,则回退到 OPENAI_EMBEDDING_DIMENSION_SIZE = 1536

这意味着“1536 默认维度”其实只是兜底值,不是某种不容置疑的固定真理。真正优先级是:手工指定 > 模型自报 > 1536 fallback。这个细节对你理解为什么 Builder 里既有 dimensions(...),又还可以不写 dimensions 非常重要。它们不是冲突设计,而是一个有优先级的容错机制。

如果你想让配置更可控,最稳妥的做法通常是显式写出维度:

PgVectorStore.builder(jdbcTemplate, embeddingModel)
        .dimensions(1024)
        .build();

这样建表时的 vector(1024) 就由你掌控,而不是依赖模型实现是否准确暴露 dimensions()


3)public <T> Optional<T> getNativeClient()

这个方法在 VectorStore 通用接口里就存在,默认是 Optional.empty();而 PgVectorStore 覆盖后返回底层 JdbcTemplate。这点非常重要,因为它给了你一个正式的“下潜通道”:当抽象 API 不够时,你可以拿到底层 PostgreSQL 客户端,执行原生 SQL 或其他数据库管理逻辑。Spring AI 的向量数据库概览中也把 getNativeClient() 明确列为通用 API 的一部分。

在理解上,你应该把这个方法看成“抽象不足时的逃生舱”,而不是鼓励你日常都绕开 VectorStore。正常增删查检索仍然应优先使用统一接口;只有当你需要 PostgreSQL 特定功能时,才去拿 JdbcTemplate


4)public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName)

这个方法大多数业务开发者几乎不会显式调用,但它很值得读。源码表明,它会构造一个 VectorStoreObservationContext.Builder,填入:

  • provider:PG_VECTOR
  • operationName:当前操作名
  • collectionName:表名
  • dimensions:向量维度
  • namespace:schema 名
  • similarityMetric:由 distanceType 映射而来。

这说明 PgVectorStore 在运行时不是“不可观测黑盒”,而是把自己作为一个向量存储实现的关键元信息交给了 Micrometer 观测体系。换句话说,它既是数据库实现,也是一个“会说出自己是谁、正在做什么”的观测友好组件。对于理解类设计来说,这说明它不是只为演示代码写的,而是考虑了生产级监控与可观测性。


十五、Builder 链式方法

PgVectorStore.PgVectorStoreBuilder 的 Javadoc 列出了它自己的专属链式方法:schemaName(...)vectorTableName(...)idType(...)vectorTableValidationsEnabled(...)dimensions(...)distanceType(...)removeExistingVectorStoreTable(...)indexType(...)initializeSchema(...)maxDocumentBatchSize(...)build();同时它继承自 AbstractVectorStoreBuilder,还带有通用的 batchingStrategy(...)observationRegistry(...)customObservationConvention(...) 等方法。

下面我按“控制什么”来解释,而不是机械抄签名。

schemaName(String schemaName)

控制 PostgreSQL schema 名。默认是 public。这不是一个只影响显示的字符串,而是会直接参与 CREATE SCHEMACREATE TABLEDELETESELECTINSERT 等所有 SQL 中的全限定表名拼接。源码中的 getFullyQualifiedTableName() 就是简单地返回 schemaName + "." + vectorTableName。因此,改它相当于改变了整个向量表的命名空间。

vectorTableName(String vectorTableName)

控制向量表名。默认是 vector_store。同时它还会影响默认索引名:若表名仍是默认 vector_store,索引名走常量 spring_ai_vector_index;若表名自定义,则索引名自动派生为 <table>_index。这意味着改表名不仅改表,也会间接改索引对象名。

idType(PgIdType idType)

控制主键列类型。它会影响:

  • 建表 SQL 中 id 列类型;
  • uuid-ossp 扩展是否启用;
  • Java 字符串 ID 如何被转换成 PostgreSQL 实际类型。

因此它不是单纯“主键偏好”,而是一个同时影响 schema 与运行时参数绑定的配置项。

vectorTableValidationsEnabled(boolean enabled)

控制是否在初始化时做表/schema 校验。源码显示,若该值为 true,afterPropertiesSet() 会先调用 schemaValidator.validateTableSchema(...)。这说明它属于一个偏安全/偏校验的开关,而不是查询性能或功能性开关。

dimensions(int dimensions)

控制 vector(n) 的 n。若设了正数,优先于模型自动探测;若不设,则走 embeddingDimensions() 的自动/回退逻辑。它影响的是建表时向量列的静态维度定义,因此是一个非常核心的 schema 级参数。

distanceType(PgDistanceType distanceType)

控制距离/相似度路线。它会联动影响:

  • 检索运算符 <-> / <#> / <=>
  • 索引操作符类 vector_l2_ops / vector_ip_ops / vector_cosine_ops
  • similarity search SQL 模板
  • observation 里的 similarity metric。

这绝不是“一个枚举标签”那么简单,而是检索行为总开关。

removeExistingVectorStoreTable(boolean removeExistingVectorStoreTable)

控制初始化时是否先 DROP TABLE IF EXISTS ...。源码清楚表明,只有在 initializeSchema=true 的前提下,这个开关才会产生效果;否则初始化流程整个跳过。它适合开发环境快速重建,但不能误以为它平时会自动“清理旧数据”。

indexType(PgIndexType indexType)

控制索引策略:NONE / IVFFLAT / HNSW。它直接影响 afterPropertiesSet() 最后是否建索引,以及建的是 USING NONE?(不会建)、USING ivfflat 还是 USING hnsw。源码和类注释都指出,NONE 表示精确最近邻,IVFFLATHNSW 都是近似最近邻,各有构建速度、内存占用、查询性能取舍。pgvector 官方也明确区分 exact 与 approximate nearest neighbor search。

initializeSchema(boolean initializeSchema)

控制是否在 Bean 初始化时自动建扩展、建 schema、建表、建索引。当前 Spring AI 参考文档明确要求显式开启它。它是 PgVectorStore 中最“有副作用”的 Builder 参数之一。

maxDocumentBatchSize(int maxDocumentBatchSize)

控制单次 JDBC batchUpdate 的文档上限。它只影响数据库写入分批,不影响 EmbeddingModel 的批量调用策略。这个区别非常重要。

继承来的 batchingStrategy(...)

这是通用 VectorStore.Builder 方法,不是 pgvector 专属。但在 PgVectorStore 中一样生效,因为 doAdd(...) 直接把 this.batchingStrategy 传给了 embeddingModel.embed(...)。所以它控制的是文档嵌入生成时的批量策略

如果把所有 Builder 方法按职责归类,你会更容易记:

数据库对象命名
  - schemaName
  - vectorTableName

主键与表结构
  - idType
  - dimensions

检索数学与索引
  - distanceType
  - indexType

初始化行为
  - initializeSchema
  - removeExistingVectorStoreTable
  - vectorTableValidationsEnabled

吞吐/批处理
  - maxDocumentBatchSize
  - batchingStrategy

可观测性
  - observationRegistry
  - customObservationConvention

这张“职责图”其实就是 PgVectorStore 的完整控制面板。你一旦把这些配置项与它们真正影响的运行时行为对应起来,这个类就基本被你读透了。


十六、总结

到这里,如果把 PgVectorStore 当成一道面试题、源码题或框架理解题,最值得你稳定记住的是下面这组结论。

一,PgVectorStore 的核心不是“有个 similaritySearch 方法”,而是它通过模板方法机制,把 Spring AI 的通用 VectorStore 抽象落到了 PostgreSQL + pgvector 上。 公共入口在 AbstractObservationVectorStore,数据库行为在 doAdd / doDelete / doSimilaritySearch

二,它是“嵌入生成 + 数据库存储 + 检索还原”一体化实现。 doAdd(...) 会自己调 EmbeddingModeldoSimilaritySearch(...) 也会先把 query 文本向量化,然后用 DocumentRowMapper 再把数据库结果还原为 Document。它不是一个只接收现成向量的薄 DAO。

三,filter 的本质是 metadata JSON 过滤。 不管是删除还是检索,只要是 filter,最终都会通过转换器落成 PostgreSQL JSONPath,并作用于 metadata::jsonb。这决定了它过滤的是结构化元数据,而不是正文内容。

四,distanceType 是行为总开关。 它决定运算符、索引 ops、SQL 模板和 score/distance 语义,不只是“选择一个相似度名字”。

五,Builder 不是附属品,而是这个类的主要构造界面。 通过它你控制 schema、表名、主键、维度、索引、初始化、批处理、观测等几乎所有关键行为。

六,afterPropertiesSet() 非常“重”。 只要开启初始化,它就会真的建扩展、建 schema、建表、建索引;而当前版本必须显式开启 initializeSchema

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐