文章目录
- [1. 过滤表达式](#1. 过滤表达式)
-
- [1.1 Filter.Expression](#1.1 Filter.Expression)
-
- [1.1.1 FilterExpressionBuilder](#1.1.1 FilterExpressionBuilder)
- [1.1.2 支持的运算符](#1.1.2 支持的运算符)
- [1.1.3 查询](#1.1.3 查询)
- [1.1.4 删除](#1.1.4 删除)
- [1.2 过滤字符串表达式](#1.2 过滤字符串表达式)
- [1.3 原生过滤表达式](#1.3 原生过滤表达式)
- [2. Schema 初始化](#2. Schema 初始化)
- [3. 批处理策略](#3. 批处理策略)
-
- [3.1 BatchingStrategy](#3.1 BatchingStrategy)
- [3.2 TokenCountBatchingStrategy](#3.2 TokenCountBatchingStrategy)
- [4. 文本超限处理](#4. 文本超限处理)
-
- [4.1 自动截断](#4.1 自动截断)
- [4.2 自动拆分](#4.2 自动拆分)
- [5. 读写操作分离](#5. 读写操作分离)
-
- [5.1 自定义读写服务类](#5.1 自定义读写服务类)
- [5.2 基于 VectorStoreRetriever](#5.2 基于 VectorStoreRetriever)
- [6. 文档版本管理](#6. 文档版本管理)
1. 过滤表达式
非结构化数据 (如文本、图像和音频)格式各异,蕴含丰富的潜在语义,因此分析起来极具挑战性。为了处理这种复杂性,使用嵌入模型将非结构化数据转换成能够捕捉其基本特征的数字向量。然后将这些向量存储在向量数据库中,从而实现快速、可扩展的搜索和分析。
AI 应用开发中,特别是 RAG 场景中肯定用不到向量数据库 所有的功能,所以 Spring AI 提供了 VectorStore 向量存储操作服务接口,提供了常用的增删改查能力,同时也提供了原生操作客户端 ,基于向量数据库官方 SDK 实现更复杂的数据操作。
在 VectorStore 中,支持通过表达式 进行复杂的查询、删除操作,如果无法支持特定场景,还可以使用原生操作客户端。
1.1 Filter.Expression
1.1.1 FilterExpressionBuilder
过滤表达式构建器(DSL,类型安全)构建 Filter.Expression 实例,支持流式 API 编程:
java
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = b.eq("country", "BG").build();
1.1.2 支持的运算符
比较运算符:
- 等于:
== - 大于:
> - 大于等于:
>= - 小于:
< - 小于等于:
<= - 不等于:
!=
逻辑运算符:
- 与:
AND/and/&& - 或:
OR/or/||
示例:
java
Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();
集合/非运算符:
- 包含:
IN/in - 不包含:
NIN/nin - 非:
NOT/not
示例:
java
Expression exp = b.and(b.in("genre", "drama", "documentary"), b.not(b.lt("year", 2020))).build();
空值判断:
- 为空:
IS NULL/is null - 非空:
IS NOT NULL/is not null
示例:
java
Expression exp = b.and(b.isNull("year")).build();
Expression exp = b.and(b.isNotNull("year")).build();
注意:
IS NULL和IS NOT NULL暂未在所有向量数据库中实现。
1.1.3 查询
构建 SearchRequest 是可以传入过滤表达式:
java
// 1. 通用检索请求
SearchRequest searchRequest = SearchRequest.builder()
.filterExpression(filterExpression)
.build();
// 2. 特定向量数据库检索请求
MilvusSearchRequest milvusSearchRequest = MilvusSearchRequest.milvusBuilder()
.filterExpression(filterExpression)
.build();
【单条件过滤】等于 (eq):
java
// 过滤条件:category == "技术"
var filter = filterBuilder.eq("category", "技术").build();
// 构建搜索请求
var request = MilvusSearchRequest.milvusBuilder()
.query("什么是向量数据库")
.topK(5)
.filterExpression(filter) // 传入过滤表达式
.build();
return vectorStore.similaritySearch(request);
【多条件组合】AND 且:
java
// 过滤条件:year >= 2024 AND category == "AI"
var filter = filterBuilder.and(
filterBuilder.gte("year", 2024),
filterBuilder.eq("category", "AI")
).build();
var request = MilvusSearchRequest.milvusBuilder()
.query("大模型应用")
.filterExpression(filter)
.build();
return vectorStore.similaritySearch(request);
【范围过滤】大于/小于 (gte/lt) :
java
// 过滤条件:price > 50 AND price < 200
var filter = filterBuilder.and(
filterBuilder.gt("price", 50),
filterBuilder.lt("price", 200)
).build();
return vectorStore.similaritySearch(
MilvusSearchRequest.milvusBuilder()
.query("教程")
.filterExpression(filter)
.build()
);
【集合过滤】IN / NIN:
java
// 过滤条件:city IN ["北京", "上海", "深圳"]
var filter = filterBuilder.in("city", "北京", "上海", "深圳").build();
return vectorStore.similaritySearch(
MilvusSearchRequest.milvusBuilder()
.query("科技公司")
.filterExpression(filter)
.build()
);
1.1.4 删除
按过滤表达式删除方法:
java
void delete(Filter.Expression filterExpression);
单条件删除:
java
private final FilterExpressionBuilder filterBuilder = new FilterExpressionBuilder();
var filter = filterBuilder.eq("category", "过时文档").build();
vectorStore.delete(filter);
多条件组合删除:
java
var filter = filterBuilder.and(
filterBuilder.lt("year", 2023),
filterBuilder.ne("category", "技术")
).build();
vectorStore.delete(filter);
复杂条件删除:
java
var filter = filterBuilder.and(
filterBuilder.group(
filterBuilder.or(
filterBuilder.eq("category", "AI"),
filterBuilder.gte("year", 2024)
)
),
filterBuilder.ne("author", "张三")
).build();
vectorStore.delete(filter);
1.2 过滤字符串表达式
如果不想用 DSL,可以直接写字符串过滤表达式进行操作。
删除支持:
java
/**
* Deletes documents from the vector store using a string filter expression. Converts
* the string filter to an Expression object and delegates to
* {@link #delete(Filter.Expression)}.
* @param filterExpression String representation of the filter criteria
* @throws IllegalArgumentException if the filter expression is null
* @throws IllegalStateException if the underlying delete causes an exception
*/
default void delete(String filterExpression) {
SearchRequest searchRequest = SearchRequest.builder().filterExpression(filterExpression).build();
Filter.Expression textExpression = searchRequest.getFilterExpression();
Assert.notNull(textExpression, "Filter expression must not be null");
this.delete(textExpression);
}
检索请求支持:
java
var filterExpression ="..........";
// 1. 通用检索请求
SearchRequest searchRequest = SearchRequest.builder()
.filterExpression(filterExpression)
.build();
// 2. 特定向量数据库检索请求
MilvusSearchRequest milvusSearchRequest = MilvusSearchRequest.milvusBuilder()
.filterExpression(filterExpression)
.build();
查询操作示例:
java
// 示例:year >= 2024 AND category != '娱乐'
var request = MilvusSearchRequest.milvusBuilder()
.query("大模型")
.filterExpression("year >= 2024 and category != '娱乐'")
.build();
List<Document> result = vectorStore.similaritySearch(request);
删除操作示例:
java
vectorStore.delete("year < 2020 and category in ['娱乐', '广告']");
1.3 原生过滤表达式
MilvusSearchRequest 专属参数:
nativeExpression:直接写Milvus原生过滤语法,优先级 > 通用过滤表达式searchParamsJson:配置向量索引的搜索参数,优化查询速度 / 精度
示例 1 :
java
// 1. 构建 Milvus 专属搜索请求
MilvusSearchRequest request = MilvusSearchRequest.milvusBuilder()
.query("人工智能教程")
.topK(10)
// 🔥 Milvus 原生过滤语法:支持 LIKE、数值比较、IN、AND/OR
.nativeExpression("city LIKE '上海%' AND price > 100 AND year >= 2024")
.build();
// 执行搜索
vectorStore.similaritySearch(request);
示例 2 :
java
MilvusSearchRequest request = MilvusSearchRequest.milvusBuilder()
.query("向量数据库")
.topK(5)
// 🔥 Milvus 索引搜索参数(JSON 字符串)
// 场景1:IVF_FLAT 索引 → nprobe 越大精度越高,速度越慢
.searchParamsJson("{\"nprobe\": 256}")
// 场景2:HNSW 索引 → ef 越大精度越高,速度越慢
// .searchParamsJson("{\"ef\": 100}")
.build();
vectorStore.similaritySearch(request);
2. Schema 初始化
向量数据库(如 Milvus)不是开箱即用的,在存储向量数据前,必须提前创建:
- 集合(
Collection):相当于MySQL的「表」; - 字段结构:固定字段(向量字段、
ID字段、元数据字段); - 向量索引:加速向量搜索的核心结构(没有索引无法高效搜索)。
Spring AI 对所有向量库默认关闭自动初始化,原因:
- 防止重复创建集合 / 索引导致报错;
- 防止覆盖生产环境已有的数据结构。
必须主动开启这个功能,才能正常使用向量库:
yml
spring:
ai:
vectorstore:
milvus:
# Milvus 连接信息
host: localhost
port: 19530
database: default
collection-name: spring_ai_collection
# ✅ 核心:开启自动初始化 schema
initialize-schema: true
3. 批处理策略
在把大量文档存入 Milvus 前,需要先给文档生成向量(嵌入),但嵌入模型(如 OpenAI、文心一言、通义千问)有严格限制:
- 模型有最大令牌数(
Token)上限(比如 OpenAI 是8191个Token); - 单次请求不能超过这个上限,否则直接报错,或者文本被截断、向量生成错误;
- 一次性传入几百篇文档,
100%会超出令牌限制。
Spring AI 批处理策略会动把大批量文档拆分成多个小批量,保证每一小批的总令牌数 ≤ 模型上限,安全生成向量。
3.1 BatchingStrategy
Spring AI 定义的分批规则标准,只有一个核心方法:
java
// 输入:一堆待处理的文档
// 输出:分好组的文档([[文档1,文档2], [文档3,文档4], ...])
List<List<Document>> batch(List<Document> documents);
3.2 TokenCountBatchingStrategy
基于令牌数自动分批 的实现,所有嵌入模型 + 向量库(包括 Milvus)通用。
默认配置:
| 参数 | 默认值 | 作用 |
|---|---|---|
| 最大令牌数 | 8191(OpenAI 标准) | 模型允许的单次最大令牌 |
| 预留百分比 | 10% | 安全缓冲,避免刚好超限 |
| 实际可用令牌 | 8191 × (1 - 0.1) ≈ 7372 |
真正用于文档的令牌数 |
| 令牌估算器 | JTokkit(开源精准估算) | 自动计算文档的令牌数量 |
核心规则:
- 自动累加每批文档的令牌数,不超限就继续加,超限就新开一批;
- 如果单个文档本身就超过最大令牌数,直接抛异常(必须手动拆分超长文档)。
如果你的嵌入模型不是 OpenAI(比如国产模型、本地模型),令牌上限不同,就需要自定义。
创建配置类,覆盖默认批处理策略,自动生效:
java
@Configuration
public class EmbeddingConfig {
/**
* 自定义批处理策略:适配你的嵌入模型
*/
@Bean
public BatchingStrategy customBatchingStrategy() {
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE, // 令牌编码格式(主流大模型通用)
8000, // 你的模型最大令牌数(按需修改)
0.15 // 预留缓冲比例(15%,更安全)
);
}
}
可以完全自定义批处理策略,只需定义 BatchingStrategy Bean:
java
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
自定义策略会自动被嵌入模型使用。
说明:
Spring AI支持的向量数据库默认使用TokenCountBatchingStrategy,SAP Hana 暂未配置批处理。
4. 文本超限处理
4.1 自动截断
先分清两个关键角色(必懂),这是理解整个逻辑的基础,对应两个文本超限处理的层级:
- Spring AI 批处理策略 (守门员):检查文档令牌数,超限直接抛异常,不让文档进入嵌入模型;
- 嵌入模型 自动截断 (处理器):模型自己检查文本长度,超限直接切掉多余内容,静默生成向量,不报错。
如果你开启了模型的自动截断 ,目的是让模型自己处理超长文本 , 但默认的批处理策略(TokenCountBatchingStrategy)有最大令牌上限 ,超长文档会被Spring AI 提前拦截报错 ,根本到不了模型层
,所以我们需要:关闭批处理的超限检查 → 让所有文档直接传给模型,由模型完成截断。
解决方案(自动截断做法) :把批处理策略的最大令牌数设置为一个极大值(远大于模型的实际上限),批处理永远不会触发超限报错,超长文档直接交给模型的自动截断功能处理。
正确首选方案 :超长文档 → 用 DocumentSplitter(文档拆分器) 拆分成短文档 → 完整生成向量
(而不是直接截断)。
核心配置类示例:
java
@Configuration
public class MilvusAutoTruncateConfig {
// ====================== 1. 启用模型自动截断(以通用嵌入模型为例)======================
// 如果你用OpenAI/通义千问/文心一言,在模型配置中开启 autoTruncate
// 例:OpenAI 模型配置(支持自动截断)
/*
@Bean
public EmbeddingModel embeddingModel() {
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
.model("text-embedding-3-small")
.autoTruncate(true) // 🔥 开启模型自动截断
.build();
return new OpenAiEmbeddingModel(new OpenAiClient(), options);
}
*/
// ====================== 2. 自定义批处理策略(核心:超大令牌上限)======================
@Bean
public BatchingStrategy batchingStrategy() {
// 🔥 关键:设置一个极大值(比如132900),远大于模型实际上限(如8191/20000)
// 让批处理策略永远不触发超限检查
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // 人为设置超大上限
0.1 // 预留比例
);
}
// ====================== 3. Milvus 向量库配置(自动使用上述策略)======================
@Bean
public VectorStore vectorStore(MilvusServiceClient milvusClient, EmbeddingModel embeddingModel) {
return MilvusVectorStore.builder(milvusClient, embeddingModel)
.initializeSchema(true) // 自动初始化Milvus表结构
.collectionName("spring_ai_docs")
.build();
}
}
4.2 自动拆分
嵌入模型有最大 Token 限制, 超长文本不拆分的情况下会截断。因此,需要将其拆分为小片段,安全生成向量。
Spring AI 支持分片读取,这块后续会再介绍。
5. 读写操作分离
使用独立的接口可以清晰区分哪些组件需要写入权限 ,哪些仅需读取权限 ,这种关注点分离的设计,仅向真正需要的组件开放数据修改权限,让应用更易维护、更安全。
5.1 自定义读写服务类
要完整权限的服务,执行写入操作:
java
// 需
@Service
class DocumentIndexer {
private final VectorStore vectorStore;
DocumentIndexer(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void indexDocuments(List<Document> documents) {
vectorStore.add(documents);
}
}
仅需检索权限的服务,执行只读操作:
java
@Service
class DocumentRetriever {
private final VectorStoreRetriever retriever;
DocumentRetriever(VectorStoreRetriever retriever) {
this.retriever = retriever;
}
public List<Document> findSimilar(String query) {
return retriever.similaritySearch(query);
}
}
5.2 基于 VectorStoreRetriever
VectorStoreRetriever 接口提供向量数据库的只读视图 ,仅暴露相似度搜索功能。它遵循最小权限原则,在**检索增强生成(RAG)**应用中尤为实用------这类场景仅需检索文档,无需修改底层数据。
使用 VectorStoreRetriever 的优势
- 关注点分离:清晰区分读操作与写操作;
- 接口隔离:仅需检索功能的客户端不会接触到数据修改方法;
- 函数式接口 :可通过
Lambda表达式或方法引用实现,适配简单场景; - 降低依赖 :仅执行搜索的组件无需依赖完整的
VectorStore接口。
当仅需执行相似度搜索时,可直接使用 VectorStoreRetriever ,该服务仅依赖 VectorStoreRetriever 接口,明确表示它仅执行检索操作,不会修改向量数据库:
java
@Service
public class DocumentRetrievalService {
private final VectorStoreRetriever retriever;
public DocumentRetrievalService(VectorStoreRetriever retriever) {
this.retriever = retriever;
}
// 基础相似度检索
public List<Document> findSimilarDocuments(String query) {
return retriever.similaritySearch(query);
}
// 带过滤条件的相似度检索
public List<Document> findSimilarDocumentsWithFilters(String query, String country) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(5)
.filterExpression("country == '" + country + "'")
.build();
return retriever.similaritySearch(request);
}
}
VectorStoreRetriever 在 RAG 应用中价值显著:需要检索相关文档为 AI 模型提供上下文时,该接口是最佳选择。
java
@Service
public class RagService {
private final VectorStoreRetriever retriever;
private final ChatModel chatModel;
public RagService(VectorStoreRetriever retriever, ChatModel chatModel) {
this.retriever = retriever;
this.chatModel = chatModel;
}
public String generateResponse(String userQuery) {
// 1. 检索相关文档
List<Document> relevantDocs = retriever.similaritySearch(userQuery);
// 2. 提取文档内容作为上下文
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// 3. 结合上下文生成回答
String prompt = "上下文信息:\n" + context + "\n\n用户问题: " + userQuery;
return chatModel.generate(prompt);
}
}
6. 文档版本管理
在向量数据库中:
- 同一份文档更新后(
V1→V2),如果直接添加新版本,旧版本不会自动消失; - 检索时会同时返回新旧版本,导致过时内容干扰搜索结果;
解决方案:上传新 → 删旧 ,删除必须精准定位旧版本,不能删错其他文档。
给每个 Document 绑定身份元数据,作为过滤/删除的依据:
| 元数据键 | 作用 |
|---|---|
docId |
文档唯一标识(同一份文档,ID 永远不变) |
version |
版本号(区分新旧) |
lastUpdated |
更新时间 |
- 创建并存储旧版本文档(
V1):
java
// 创建初始文档(v1),携带版本元数据
Document documentV1 = new Document(
"人工智能与机器学习最佳实践", // 文档内容(向量化的文本)
Map.of( // 元数据:用于过滤、删除、版本管理
"docId", "AIML-001", // 唯一文档ID
"version", "1.0", // 旧版本号
"lastUpdated", "2024-01-01"
)
);
// 将v1添加到向量库(Milvus)
vectorStore.add(List.of(documentV1));
- 创建新版本文档(
V2):
java
// 创建同一文档的更新版本(v2)
Document documentV2 = new Document(
"人工智能与机器学习最佳实践 - 修订版", // 更新后的内容
Map.of(
"docId", "AIML-001", // 同一份文档,docId不变
"version", "2.0", // 新版本号
"lastUpdated", "2024-02-01"
)
);
- 用过滤表达式删除旧版本(精准删除):
java
// 构建过滤条件:docId = AIML-001 且 version = 1.0(精准定位旧版本)
Filter.Expression deleteOldVersion = new Filter.Expression(
Filter.ExpressionType.AND, // 逻辑:两个条件同时满足
Arrays.asList(
// 条件1:docId 等于 AIML-001
new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("docId"), new Filter.Value("AIML-001")),
// 条件2:version 等于 1.0
new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("version"), new Filter.Value("1.0"))
)
);
// 执行删除:只删除满足条件的旧版本文档
vectorStore.delete(deleteOldVersion);
- 存储新版本:
java
vectorStore.add(List.of(documentV2));
- 验证结果(仅保留
V2):
java
// 搜索该文档
SearchRequest request = SearchRequest.builder()
.query("人工智能与机器学习")
.filterExpression("docId == 'AIML-001'")
.build();
// 结果仅包含 V2,旧版本已被删除
List<Document> results = vectorStore.similaritySearch(request);