首页 / Spring AI 入门教程 / VectorStore 统一抽象

Spring AI 入门教程

VectorStore 统一抽象

本教程共 45 篇 · 第 34 篇 · 更新于 2026-08-16 · 约 8 分钟阅读

Spring AIVectorStore向量数据库相似度搜索SearchRequest元数据过滤BatchingStrategy

本节目标:掌握 VectorStore 接口的增删查方法、SearchRequest 的检索参数、与 EmbeddingModel 的配合方式,理解 schema 初始化、批量嵌入和读写分离这些工程细节。

34.1 为什么需要统一抽象

第 35、36 章会看到,Spring AI 支持 20 余种向量库实现。如果没有抽象层,每种库一套 API,业务代码会被供应商锁死。

VectorStore 接口解决的就是这个问题。它把”存向量、查向量”这两个动作抽象成统一方法,底层换成哪个向量库,业务代码一行不用改。

这套抽象在 2.0 里是核心设计。S3 Vector Store、统一命名的 starter、VectorStoreRetriever 只读接口,都围绕它展开。

34.2 两个接口:读写分离

Spring AI 提供两个接口,按职责拆开。

VectorStoreRetriever 是只读接口,只有一个相似度搜索方法。它遵循最小权限原则:只需要查询的组件,不暴露任何写方法。

@FunctionalInterface
public interface VectorStoreRetriever {

    List<Document> similaritySearch(SearchRequest request);

    default List<Document> similaritySearch(String query) {
        return this.similaritySearch(SearchRequest.builder().query(query).build());
    }
}

VectorStore 继承 VectorStoreRetriever,再加上写操作:

public interface VectorStore extends DocumentWriter, VectorStoreRetriever {

    void add(List<Document> documents);

    void delete(List<String> idList);

    void delete(Filter.Expression filterExpression);

    default void delete(String filterExpression) { ... }

    default <T> Optional<T> getNativeClient() { return Optional.empty(); }
}

注意 VectorStore 同时继承 DocumentWriter。上一章的 vectorStore.write(...) 能成立,靠的就是这层关系。getNativeClient() 返回底层原生客户端,需要用到各家特有功能时再取。

Tip

查询服务注入 VectorStoreRetriever,写入服务注入 VectorStore。接口一拆,代码里谁有权写数据一目了然,也方便测试时替换成假实现。

34.3 相似度是怎么算的

向量检索的底层是距离计算。两个向量离得近,语义就相近。

最常见的度量是余弦相似度,算两个向量的夹角,取值在 -1 到 1 之间,越接近 1 越相似。还有欧氏距离(L2)和内积(IP)两种,一般要求向量先归一化。Spring AI 的 similarityThreshold 统一按 0 到 1 的相似度语义来,越接近 1 越相似。各实现内部会把自己的度量归一化到这一语义,所以业务代码不用关心底层用的是余弦还是欧氏。

向量库只负责存和算,不负责生成向量。入库时用哪个嵌入模型,检索时就用哪个。 模型不一致,向量空间都不一样,检索结果没有意义。这一点在换模型、换维度时要特别小心。

34.4 SearchRequest:检索参数

相似度搜索的参数封装在 SearchRequest 里,用 Builder 构造:

SearchRequest request = SearchRequest.builder()
    .query("退款政策是什么")
    .topK(5)                    // 返回最相似的 5 条
    .similarityThreshold(0.7)   // 相似度低于 0.7 的不要
    .build();

List<Document> docs = vectorStore.similaritySearch(request);

四个参数各有讲究。

query 是要检索的文本,接口内部会用 EmbeddingModel 把它转成向量。

topK 是返回条数,默认 4。K 越大上下文越全,但噪声和 token 成本也越高。

similarityThreshold 是相似度下限,取值 0 到 1,默认 0.0(不设限)。设了阈值,检索结果不够像的直接丢弃,宁可少给也不给错的。

filterExpression 是元数据过滤,类似 SQL 的 WHERE。可以直接传字符串表达式:

SearchRequest request = SearchRequest.builder()
    .query("The World")
    .topK(5)
    .filterExpression("country == 'UK' && year >= 2020")
    .build();

也可以用 FilterExpressionBuilder 写类型安全的 DSL:

FilterExpressionBuilder b = new FilterExpressionBuilder();
SearchRequest request = SearchRequest.builder()
    .query("The World")
    .filterExpression(b.and(
        b.in("country", "UK", "NL"),
        b.gte("year", 2020)).build())
    .build();

过滤表达式支持 ==!=>>=<<=innotis null 等操作,&&|| 组合条件。它是跨向量库可移植的,各实现内部会翻译成自己的查询语法。

过滤条件写得太宽会拖慢检索。能用等值匹配的别用范围匹配,能在入库前筛掉的别留到检索时筛。索引设计各库不同,复杂查询先看目标库的文档。

34.5 与 EmbeddingModel 的配合

向量库只负责存储和检索,不负责生成向量。add 时,VectorStore 内部会调用配置好的 EmbeddingModel 给每个文档算向量,再连同正文、元数据一起入库。所以装配 VectorStore 时必须同时提供 EmbeddingModel。

@Configuration
public class VectorStoreConfig {

    @Bean
    public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
        return PgVectorStore.builder(jdbcTemplate, embeddingModel)
            .dimensions(1536)
            .initializeSchema(true)
            .build();
    }
}

用 Spring Boot starter 时连这个 Bean 都不用写,加依赖加配置,VectorStore 自动装配好,直接注入即可。

自动装配的关键是配置前缀。每种向量库一套前缀,比如 spring.ai.vectorstore.pgvector.*spring.ai.vectorstore.redis.*spring.ai.vectorstore.milvus.*。加了对的 starter、写了对应的配置、提供了 EmbeddingModel,容器里就会出现 VectorStore Bean。第 35 章会逐个演示。

没有 starter 的场景,手工装配也简单:找到对应实现的 Builder,传入客户端和嵌入模型即可。两种方式产物相同,业务代码无差别。

大批量入库有个坑:嵌入模型有 token 上限,一次塞几万条文本会报错。Spring AI 用 BatchingStrategy 解决,默认实现是 TokenCountBatchingStrategy,按 token 数把文档分批,每批不超过上限,默认按 8191 token 预留 10% 缓冲计算。

需要自定义时,注册一个 BatchingStrategy Bean 即可全局替换:

@Bean
public BatchingStrategy batchingStrategy() {
    return new TokenCountBatchingStrategy(
        EncodingType.CL100K_BASE,  // tokenizer 编码
        8000,                      // 每批最大 token 数
        0.1                        // 预留比例
    );
}
Note

2.0 起 schema 初始化默认关闭。PGvector 建表、Redis 建索引这些动作都需要显式开启,比如 spring.ai.vectorstore.pgvector.initialize-schema=true。这是 1.x 时代默认行为变更过来的,升级项目时最容易踩。

34.6 删除与版本管理

删除有两个入口:按 ID 删,或按过滤表达式删。

// 按 ID 删
vectorStore.delete(List.of(document.getId()));

// 按过滤条件删(字符串或 DSL 表达式)
vectorStore.delete("country == 'Bulgaria'");

按过滤条件删很有用,最常见的场景是文档版本管理:新版本入库前,先把旧版本删掉。

// 写入时给元数据打上文档 ID 和版本号
Document docV1 = new Document("旧版内容",
    Map.of("docId", "AIML-001", "version", "1.0"));

// 更新时先删旧版,再写新版
vectorStore.delete("docId == 'AIML-001' && version == '1.0'");
vectorStore.add(List.of(docV2));

删除操作可能抛异常,比如过滤表达式非法,记得用 try-catch 包住。

删除的性能因库而异:按 ID 删一般最快,按过滤表达式删可能要扫描索引。大批量删除分批进行,避免压垮数据库。

删除不可逆,生产环境先确认过滤条件再执行。误删后重新入库要重新生成向量,时间和成本都是双份的。

34.7 完整增删查示例

把接口方法串起来看一遍:

@Service
public class KnowledgeService {

    private final VectorStore vectorStore;

    public KnowledgeService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void addDocument(String content, String category) {
        Document doc = new Document(content, Map.of("category", category));
        vectorStore.add(List.of(doc));
    }

    public List<Document> search(String query, String category) {
        SearchRequest request = SearchRequest.builder()
            .query(query)
            .topK(5)
            .similarityThreshold(0.6)
            .filterExpression("category == '" + category + "'")
            .build();
        return vectorStore.similaritySearch(request);
    }

    public void removeByCategory(String category) {
        vectorStore.delete("category == '" + category + "'");
    }
}

增、查、删各一个方法,底层是 PGvector 还是 Milvus,调用方完全无感。这就是统一抽象的价值。

如果某个向量库的能力接口覆盖不了,比如要执行原生 SQL 或调专用 API,用 getNativeClient() 取底层客户端,绕过抽象直接操作。取不到会返回空 Optional,用前记得判空。这是抽象层预留的后门,常规场景用不到,但知道它在,心里有底。

34.8 小结

VectorStore 把 20 余种向量库收敛成一套 API:add 写入、similaritySearch 查询、delete 删除。SearchRequest 用 topK、相似度阈值、过滤表达式三件套控制检索质量。读写分离的 VectorStoreRetriever 让权限边界更清晰。下一章挑三个主流向量库,看它们怎么接进来。