向量数据库用于将数据与人工智能模型进行整合。其使用流程的第一步是将数据加载到向量数据库中。当需要向AI模型发送用户查询时,系统会首先检索一组相似文档。这些文档将作为用户问题的上下文信息,与用户查询一起被发送至AI模型。这种技术被称为检索增强生成(Retrieval Augmented Generation, RAG)。
后续章节将介绍Spring AI框架中用于操作多种向量数据库实现的接口规范,以及相关高级用法示例。
最后一节旨在解析向量数据库中相似性搜索技术的底层实现原理。
本节作为 Spring AI 框架中 VectorStore 接口 及其关联类的指南。
Spring AI 通过 VectorStore 接口 提供了与向量数据库交互的抽象化 API。
以下是 VectorStore 接口 的定义:
public interface VectorStore extends DocumentWriter {
default String getName() {
return this.getClass().getSimpleName();
}
void add(List<Document> documents);
void delete(List<String> idList);
void delete(Filter.Expression filterExpression);
default void delete(String filterExpression) { ... };
List<Document> similaritySearch(String query);
List<Document> similaritySearch(SearchRequest request);
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
}
以及相关的 SearchRequest 构建器 :
public class SearchRequest {
public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;
public static final int DEFAULT_TOP_K = 4;
private String query = "";
private int topK = DEFAULT_TOP_K;
private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;
@Nullable
private Filter.Expression filterExpression;
public static Builder from(SearchRequest originalSearchRequest) {
return builder().query(originalSearchRequest.getQuery())
.topK(originalSearchRequest.getTopK())
.similarityThreshold(originalSearchRequest.getSimilarityThreshold())
.filterExpression(originalSearchRequest.getFilterExpression());
}
public static class Builder {
private final SearchRequest searchRequest = new SearchRequest();
public Builder query(String query) {
Assert.notNull(query, "查询内容不能为空。");
this.searchRequest.query = query;
return this;
}
public Builder topK(int topK) {
Assert.isTrue(topK >= 0, "TopK 必须为非负数。");
this.searchRequest.topK = topK;
return this;
}
public Builder similarityThreshold(double threshold) {
Assert.isTrue(threshold >= 0 && threshold <= 1, "相似度阈值必须在 [0,1] 范围内。");
this.searchRequest.similarityThreshold = threshold;
return this;
}
public Builder similarityThresholdAll() {
this.searchRequest.similarityThreshold = 0.0;
return this;
}
public Builder filterExpression(@Nullable Filter.Expression expression) {
this.searchRequest.filterExpression = expression;
return this;
}
public Builder filterExpression(@Nullable String textExpression) {
this.searchRequest.filterExpression = (textExpression != null)
? new FilterExpressionTextParser().parse(textExpression) : null;
return this;
}
public SearchRequest build() {
return this.searchRequest;
}
}
public String getQuery() {...}
public int getTopK() {...}
public double getSimilarityThreshold() {...}
public Filter.Expression getFilterExpression() {...}
}
要向向量数据库插入数据,需将其封装在 Document 对象 中。
Document 类 封装了来自数据源(如 PDF 或 Word 文档)的内容,包含以字符串形式表示的文本,以及以键值对形式存储的元数据(如文件名等)。
插入向量数据库时,文本内容会通过 嵌入模型 (如 Word2Vec、GLoVE、BERT 或 OpenAI 的 text-embedding-ada-002)转换为数值数组(float[]),即 向量嵌入 (vector embeddings)。
向量数据库的职责是存储这些嵌入并向其提供相似性搜索功能,但其本身不生成嵌入。生成向量嵌入需使用 EmbeddingModel 。
接口中的 similaritySearch 方法支持通过以下参数优化检索结果:
更多关于 Filter.Expression 的信息,请参考 元数据过滤器 章节。
某些向量存储需要先初始化后端模式后才能使用。默认情况下,系统不会自动完成此初始化操作。您需要通过以下方式主动启用:
在构造函数中传递一个布尔值参数,或
如果使用 Spring Boot,在 application.properties 或 application.yml 中将对应的 initialize-schema 属性设置为 true。
在处理向量存储时,通常需要嵌入大量文档。尽管一次性嵌入所有文档看似直接,但这种方法可能导致问题。嵌入模型以令牌(token)为单位处理文本,并存在最大令牌限制(即上下文窗口大小)。单次嵌入请求若超出此限制,可能引发错误或截断嵌入结果。
为解决令牌限制问题,Spring AI 实现了批处理策略 。该策略将大量文档拆分为较小批次,确保每个批次的令牌数不超过嵌入模型的最大上下文窗口限制。批处理不仅规避了令牌限制,还能提升性能并更高效地利用 API 速率限制。
Spring AI 通过 BatchingStrategy 接口提供此功能,支持基于令牌数对文档进行分批处理。
BatchingStrategy 接口定义如下:
public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
该接口定义了一个 batch 方法,接收文档列表并返回分批后的文档列表。
Spring AI 提供了默认实现 TokenCountBatchingStrategy,该策略根据文档的令牌数进行分批,确保每批不超过最大输入令牌限制。
核心特性 :
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customTokenCountBatchingStrategy() {
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE, // 指定编码类型
8000, // 最大输入令牌数
0.1 // 保留百分比
);
}
}
参数说明 :
TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
customEstimator,
8000, // 最大输入令牌数
0.1, // 保留百分比
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);
若需完全自定义批处理逻辑,可通过实现 BatchingStrategy 接口:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy(); // 自定义实现
}
}
此自定义策略将自动被 Spring AI 的 EmbeddingModel 实现使用。
注意事项
以下是 VectorStore 接口的可用实现:
未来版本可能会支持更多实现。
如果需要 Spring AI 支持您的向量数据库,可在 GitHub 提交问题(issue)或直接通过拉取请求(pull request)贡献实现代码。
要计算向量数据库的嵌入(embeddings),需选择与所使用的高层级 AI 模型相匹配的嵌入模型。
例如,使用 OpenAI 的 ChatGPT 时,需选用 OpenAiEmbeddingModel 及名为 text-embedding-ada-002 的模型。
Spring Boot 的 OpenAI starter 会通过自动配置,在 Spring 应用上下文中提供一个 EmbeddingModel 实现,用于依赖注入。
将数据加载到向量存储的操作通常以批处理模式完成:
示例代码 :
@Autowired
VectorStore vectorStore;
void load(String sourceFile) {
// 使用 JsonReader 加载 JSON 文件的指定字段(如 price、name 等)
JsonReader jsonReader = new JsonReader(
new FileSystemResource(sourceFile),
"price", "name", "shortDescription", "description", "tags"
);
List<Document> documents = jsonReader.get();
// 调用 VectorStore.add() 存储文档
this.vectorStore.add(documents); // [[4]]
}
过程说明 :
当用户提问传入 AI 模型时,流程如下:
String question = <用户问题>;
List<Document> similarDocuments = store.similaritySearch(question);
本节描述可用于对查询结果进行过滤的多种方式。
您可以通过 similaritySearch 方法的重载形式,传入类似 SQL 语法的字符串表达式作为过滤条件。
示例 :
"country == 'BG'" // 等值过滤
"genre == 'drama' && year >= 2020" // 组合条件
"genre in ['comedy', 'documentary', 'drama']" // 枚举过滤
您可以通过 FilterExpressionBuilder 创建 Filter.Expression 实例,以构建链式 API 表达式。
基础示例 :
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = b.eq("country", "BG").build(); // 等于条件
支持的操作符 :
复杂表达式示例 :
// 组合条件:genre 为 'drama' 且年份 ≥ 2020
Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();
技术细节
向量存储接口(VectorStore)提供了多种删除文档的方法,支持通过文档ID列表 或过滤表达式 删除数据。
最简单的删除方式是提供文档ID列表:
void delete(List<String> idList);
此方法会删除所有ID与列表匹配的文档。若列表中存在不存在的ID,将被忽略。
示例代码 :
// 创建并添加文档
Document document = new Document("The World is Big",
Map.of("country", "Netherlands")); // 元数据包含国家信息
vectorStore.add(List.of(document));
// 按ID删除文档
vectorStore.delete(List.of(document.getId()));
对于复杂删除条件,可使用 Filter.Expression 对象定义删除规则:
void delete(Filter.Expression filterExpression);
此方法适用于基于元数据属性删除文档,例如按国家、年份等条件过滤。
示例代码 :
// 创建不同元数据的测试文档
Document bgDocument = new Document("The World is Big",
Map.of("country", "Bulgaria")); // 保加利亚文档
Document nlDocument = new Document("The World is Big",
Map.of("country", "Netherlands")); // 荷兰文档
// 添加文档到存储
vectorStore.add(List.of(bgDocument, nlDocument));
// 使用过滤表达式删除保加利亚文档
Filter.Expression filterExpression = new Filter.Expression(
Filter.ExpressionType.EQ, // 等值操作符
new Filter.Key("country"), // 元数据键
new Filter.Value("Bulgaria") // 目标值
);
vectorStore.delete(filterExpression); // [[5]][[8]]
// 验证删除结果
SearchRequest request = SearchRequest.builder()
.query("World")
.filterExpression("country == 'Bulgaria'") // 过滤条件
.build();
List<Document> results = vectorStore.similaritySearch(request);
// 结果为空,因Bulgaria文档已被删除
为简化操作,支持直接传递字符串形式的过滤表达式:
void delete(String filterExpression);
此方法内部将字符串解析为 Filter.Expression 对象,适用于动态生成的过滤条件。
示例代码 :
// 添加文档
vectorStore.add(List.of(bgDocument, nlDocument));
// 使用字符串过滤表达式删除保加利亚文档
vectorStore.delete("country == 'Bulgaria'"); // [[8]]
// 验证剩余文档
SearchRequest request = SearchRequest.builder()
.query("World")
.topK(5)
.build();
List<Document> results = vectorStore.similaritySearch(request);
// 仅返回荷兰文档
所有删除方法可能因错误抛出异常,建议使用 try-catch 块包裹操作:
try {
vectorStore.delete("country == 'Bulgaria'");
} catch (Exception e) {
logger.error("无效的过滤表达式", e); // [[7]]
}
典型用例是管理文档版本 ,例如替换旧版本文档:
实现步骤 :
Document documentV1 = new Document(
"AI与机器学习最佳实践",
Map.of(
"docId", "AIML-001",
"version", "1.0",
"lastUpdated", "2024-01-01"
)
);
vectorStore.add(List.of(documentV1));
// 使用过滤表达式删除旧版本
Filter.Expression deleteOldVersion = new Filter.Expression(
Filter.ExpressionType.AND,
Arrays.asList(
new Filter.Expression(Filter.ExpressionType.EQ, "docId", "AIML-001"),
new Filter.Expression(Filter.ExpressionType.EQ, "version", "1.0")
)
);
vectorStore.delete(deleteOldVersion); // [[5]][[8]]
// 添加新版本文档
Document documentV2 = new Document(
"AI与机器学习最佳实践 - 更新版",
Map.of(
"docId", "AIML-001",
"version", "2.0",
"lastUpdated", "2024-02-01"
)
);
vectorStore.add(List.of(documentV2));
// 验证仅保留新版本
SearchRequest request = SearchRequest.builder()
.query("AI与机器学习")
.filterExpression("docId == 'AIML-001'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results 仅包含版本2.0的文档