|
最新快照版本请使用Spring AI 1.1.0! |
向量数据库
向量数据库是一种在人工智能应用中起着关键作用的专业数据库类型。
在向量数据库中,查询不同于传统的关系型数据库。它们不是精确匹配,而是进行相似性搜索。当向量作为查询时,向量数据库返回的向量与查询向量“相似”。关于如何在高层次计算这种相似度的更多细节,详见向量相似度。
向量数据库用于将你的数据与AI模型集成。使用它们的第一步是将你的数据加载到向量数据库中。然后,当用户查询要发送到AI模型时,首先会检索一组类似的文档。这些文档随后作为用户问题的上下文,并与用户查询一同发送到AI模型。这种技术被称为检索增强生成(RAG)。
以下章节介绍了Spring AI接口用于多种向量数据库实现及一些高级示例用例。
最后一节旨在揭开向量数据库中相似性搜索的基本方法。
API 概述
本节作为指南VectorStoreSpring AI 框架中的接口及其相关类。
Spring AI 提供了一个抽象的 API,用于通过VectorStore接口。
这是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();
}
}
以及相关的搜索请求架构工人:
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, "Query can not be null.");
this.searchRequest.query = query;
return this;
}
public Builder topK(int topK) {
Assert.isTrue(topK >= 0, "TopK should be positive.");
this.searchRequest.topK = topK;
return this;
}
public Builder similarityThreshold(double threshold) {
Assert.isTrue(threshold >= 0 && threshold <= 1, "Similarity threshold must be in [0,1] range.");
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() {...}
}
要将数据插入向量数据库,将其封装在公文对象。 这公文类封装来自数据源的内容,如 PDF 或 Word 文档,并包含以字符串表示的文本。它还包含以键值对形式出现的元数据,包括文件名等细节。
插入向量数据库后,文本内容会被转换为数值数组,或float[],称为向量嵌入,使用嵌入模型。嵌入模型,如Word2Vec、GLoVE和BERT,或OpenAI的文本嵌入-ada-002用于将单词、句子或段落转换为这些向量嵌入。
向量数据库的作用是存储并促进这些嵌入的相似性搜索。它本身并不生成嵌入。对于创建向量嵌入,需要嵌入模型应该被利用。
这相似性搜索接口中的方法允许检索类似于给定查询字符串的文档。这些方法可以通过以下参数进行微调:
-
k:一个整数,指定了返回最多可返回的类似文档数量。这通常被称为“顶K”搜索,或“K最近邻”(KNN)。 -
门槛:一个双重值,范围从0到1,接近1的值表示相似度更高。默认情况下,如果你设置阈值为0.75,只有相似度高于该值的文档才会返回。 -
滤波。表达:一个用于传递流畅DSL(领域特定语言)表达式的类,其功能类似于SQL中的“where”子句,但仅适用于公文. -
filter表达式:基于ANTLR4的外部DSL,接受过滤表达式作为字符串。例如,带有元数据键如国家、年份和isActive,你可以使用如下表达式:国家 == 'UK' & 年份 >= 2020 && isActive == true。
了解更多关于滤波。表达在元数据过滤器部分。
模式初始化
有些矢量存储要求其后端模式在使用前先初始化。默认情况下不会为你初始化。你必须通过通过布尔对于合适的构造子参数,或者如果使用Spring Boot,则设置相应的参数初始化模式属性到true在application.properties或application.yml. 请查看你使用的向量商店的文档,了解具体属性名称。
批次处理策略
在使用向量存储时,通常需要嵌入大量文档。虽然一次性调用嵌入所有文档看似简单,但这种方法可能带来问题。嵌入模型将文本作为Tokens处理,并设有最大Tokens限制,通常称为上下文窗口大小。该限制限制了单次嵌入请求中可处理的文本量。尝试在一次调用中嵌入过多Tokens可能导致错误或嵌入截断。
为了解决Tokens限制,Spring AI 实施了批处理策略。这种方法将大量文档拆分成更小的批次,以符合嵌入模型的最大上下文窗口。批处理不仅解决了Tokens限制问题,还能提升性能并更高效地利用 API 速率限制。
Spring AI 通过批次处理策略该接口允许根据Tokens数量分批处理文档。
核心批次处理策略接口定义如下:
public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
该接口定义了单一方法,Batch该程序接收一份文档列表并返回文档批次列表。
默认实现
Spring AI 提供了一个默认实现,称为Tokens计数批处理策略.
该策略根据文件的Tokens数量批量处理,确保每批文件不会超过计算出的最大输入Tokens数。
主要特征Tokens计数批处理策略:
-
默认上限是OpenAI的最大输入Tokens数(8191)。
-
设定储备金百分比(默认10%)以缓冲潜在的管理费用。
-
计算实际最大输入Tokens数为:
actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)
该策略估算每个文档的Tokens数,将其分组且不超过最大输入Tokens数,若单文档超过该限制则抛出异常。
你也可以自定义Tokens计数批处理策略以更好地满足你的具体需求。这可以通过在 Spring Boot 中创建带有自定义参数的新实例来实现@Configuration类。
这里有一个创建自定义的示例Tokens计数批处理策略豆:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customTokenCountBatchingStrategy() {
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE, // Specify the encoding type
8000, // Set the maximum input token count
0.1 // Set the reserve percentage
);
}
}
在此配置下:
-
EncodingType.CL100K_BASE: 指定用于分词的编码类型。这种编码类型被以下编码类型使用JTokkitTokenCountEstimator以准确估算Tokens数量。 -
8000:设定最大输入Tokens数。这个值应小于或等于你嵌入模型的最大上下文窗口大小。 -
0.1:设定储备金比例。根据最大输入Tokens数保留的Tokens比例。这为处理过程中Tokens数量的增加创造了缓冲区。
默认情况下,该构造函数使用Document.DEFAULT_CONTENT_FORMATTER用于内容格式化和MetadataMode.NONE用于元数据处理。如果你需要自定义这些参数,可以使用带有额外参数的完整构造函数。
一旦确定,这一习俗Tokens计数批处理策略豆子会自动被嵌入模型在你的应用中实现,取代默认策略。
这Tokens计数批处理策略内部使用TokenCountEstimator(具体来说,JTokkitTokenCountEstimator)用于计算Tokens计数以实现高效的批处理。这确保了基于指定编码类型的准确Tokens估计。
此外Tokens计数批处理策略通过允许你实现自己的TokenCountEstimator接口。此功能允许您使用针对具体需求的定制Tokens计数策略。例如:
TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
this.customEstimator,
8000, // maxInputTokenCount
0.1, // reservePercentage
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);
与自动截断的合作
一些嵌入模型,如 Vertex AI 文本嵌入,支持auto_truncate特征。启用时,模型会静默截断超过最大大小的文本输入并继续处理;禁用时,对于过大的输入会抛出显式错误。
在使用自动截断处理处理策略时,你必须将输入Tokens数远高于模型实际最大值。这防止了批处理策略对大型文档提出异常,使嵌入模型能够内部处理截断。
自动截断配置
启用自动截断时,将批处理策略的最大输入Tokens数设定远高于模型的实际限制。这防止了批处理策略对大型文档提出异常,使嵌入模型能够内部处理截断。
这里有一个使用Vertex AI自动截断和自定义的示例配置批次处理策略然后在 PgVectorStore 中使用它们:
@Configuration
public class AutoTruncationEmbeddingConfig {
@Bean
public VertexAiTextEmbeddingModel vertexAiEmbeddingModel(
VertexAiEmbeddingConnectionDetails connectionDetails) {
VertexAiTextEmbeddingOptions options = VertexAiTextEmbeddingOptions.builder()
.model(VertexAiTextEmbeddingOptions.DEFAULT_MODEL_NAME)
.autoTruncate(true) // Enable auto-truncation
.build();
return new VertexAiTextEmbeddingModel(connectionDetails, options);
}
@Bean
public BatchingStrategy batchingStrategy() {
// Only use a high token limit if auto-truncation is enabled in your embedding model.
// Set a much higher token count than the model actually supports
// (e.g., 132,900 when Vertex AI supports only up to 20,000)
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // Artificially high limit
0.1 // 10% reserve
);
}
@Bean
public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, BatchingStrategy batchingStrategy) {
return PgVectorStore.builder(jdbcTemplate, embeddingModel)
// other properties omitted here
.build();
}
}
在此配置下:
-
嵌入模型启用了自动截断,使其能够优雅地处理超大输入。
-
批处理策略使用了一个人为设定的高Tokens上限(132,900),远大于实际模型限制(20,000)。
-
向量存储使用配置后的嵌入模型和自定义
批次处理策略豆。
为什么这样做有效
这种方法之所以有效,是因为:
-
这
Tokens计数批处理策略检查是否有单一文档超过配置的最大值,并抛出IllegalArgumentException如果真的有的话。 -
通过在批处理策略中设定非常高的限制,我们确保了这次检查永远不会失败。
-
超过模型限制的文档或批次会被嵌入模型的自动截断功能静默截断和处理。
最佳实践
使用自动截断时:
-
将批处理策略的最大输入Tokens数设置为模型实际限制的5-10倍,以避免批处理策略中出现过早的异常。
-
监控日志中嵌入模型的截断警告(注意:并非所有模型都会记录截断事件)。
-
考虑静默截断对嵌入质量的影响。
-
用样本文档测试,确保截断嵌入仍符合你的要求。
-
请为未来的维护者记录此配置,因为它是非标准配置。
| 虽然自动截断防止了错误,但可能导致嵌入不完整。长文档结尾的重要信息可能会丢失。如果你的应用要求所有内容都嵌入,嵌入前先把文档拆分成更小的部分。 |
Spring Boot自动配置
如果你用的是 Spring Boot 自动配置,必须提供自定义配置批次处理策略用来覆盖Spring AI自带的默认版本:
@Bean
public BatchingStrategy customBatchingStrategy() {
// This bean will override the default BatchingStrategy
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // Much higher than model's actual limit
0.1
);
}
这个豆子在你的应用上下文中存在,会自动取代所有向量存储的默认批处理策略。
自定义实现
而Tokens计数批处理策略提供强健的默认实现,你可以根据具体需求定制批处理策略。
这可以通过 Spring Boot 的自动配置实现。
要自定义批处理策略,定义一个批次处理策略Spring Boot应用中的豆子:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
这一习俗批次处理策略随后将被嵌入模型应用中的实现。
Spring AI 支持的向量存储配置为使用默认Tokens计数批处理策略.
SAP Hana 向量存储目前尚未配置为批处理。 |
向量存储实现
以下是可用的实现VectorStore接口:
-
Azure 矢量搜索 - Azure 矢量存储器。
-
Apache Cassandra - Apache Cassandra 向量存储器。
-
Elasticsearch 向量存储——Elasticsearch 向量存储器。
-
GemFire矢量商店——GemFire矢量商店。
-
MariaDB 向量存储——MariaDB 向量存储器。
-
Milvus向量存储——Milvus向量存储。
-
MongoDB Atlas 向量存储 - MongoDB Atlas 向量存储器。
-
Neo4j 向量存储器——Neo4j 向量存储器。
-
OpenSearch 向量存储——OpenSearch 向量存储器。
-
Oracle 向量存储——Oracle 数据库的向量存储器。
-
PgVector Store - PostgreSQL/PGVector 向量存储器。
-
Pinecone矢量存储——Pinecone矢量存储。
-
Qdrant 向量存储器 - Qdrant 向量存储器。
-
SAP Hana 向量存储——SAP HANA 向量存储器。
-
Typesense 向量存储 - Typesense 向量存储器。
-
Weaviate 向量商店——Weaviate 向量商店。
-
SimpleVectorStore - 一个简单的持久向量存储实现,适合教育用途。
未来版本可能会支持更多实现。
如果你有一个矢量数据库需要 Spring AI 支持,可以在 GitHub 上开启问题,或者更好的是提交包含实现的拉取请求。
关于各项的信息VectorStore实现内容可见本章各小节。
示例用法
要计算向量数据库的嵌入,你需要选择与所用更高层次AI模型匹配的嵌入模型。
例如,OpenAI的ChatGPT中,我们使用OpenAiEmbeddingModel以及一个名为文本嵌入-ada-002.
Spring Boot 启动程序的 OpenAI 自动配置实现了嵌入模型在 Spring 应用上下文中提供依赖注入。
向量存储加载数据的一般用途是批处理类的作业,首先将数据加载到 Spring AI 中公文然后调用救方法。
给定字符串引用一个源文件,代表一个包含我们想加载到矢量数据库中的数据的 JSON 文件,我们使用 Spring AI 的JsonReader加载 JSON 中的特定字段,将其拆分成小块,然后将这些小块传递给向量存储实现。
这VectorStore实现计算嵌入数据,并将JSON和嵌入存储在矢量数据库中:
@Autowired
VectorStore vectorStore;
void load(String sourceFile) {
JsonReader jsonReader = new JsonReader(new FileSystemResource(sourceFile),
"price", "name", "shortDescription", "description", "tags");
List<Document> documents = jsonReader.get();
this.vectorStore.add(documents);
}
随后,当用户问题被传递到AI模型时,会进行相似性搜索以检索相似文档,这些文档会被“塞入”提示中,作为用户提问的上下文。
String question = <question from user>
List<Document> similarDocuments = store.similaritySearch(this.question);
还可以向相似性搜索定义检索多少文档及相似性搜索的阈值的方法。
元数据过滤器
本节介绍了你可以用来对查询结果进行筛选的各种筛选方法。
Filter串
你可以通过类似SQL的过滤表达式传递给字符串到其中一个相似性搜索重载。
请考虑以下例子:
-
“乡村音乐== ''背景'” -
“类型 == '戏剧' & >年= 2020” -
“[喜剧”、“纪录片”、“戏剧]中的体裁」
滤波。表达
你可以创建一个滤波。表达其中过滤器表达构建器这会暴露一个流畅的API。
一个简单的例子如下:
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = this.b.eq("country", "BG").build();
你可以通过使用以下作符构建复杂的表达式:
EQUALS: '=='
MINUS : '-'
PLUS: '+'
GT: '>'
GE: '>='
LT: '<'
LE: '<='
NE: '!='
你可以通过以下作符组合表达式:
AND: 'AND' | 'and' | '&&';
OR: 'OR' | 'or' | '||';
考虑以下例子:
Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();
你也可以使用以下作符:
IN: 'IN' | 'in';
NIN: 'NIN' | 'nin';
NOT: 'NOT' | 'not';
请考虑以下例子:
Expression exp = b.and(b.in("genre", "drama", "documentary"), b.not(b.lt("year", 2020))).build();
从向量存储中删除文档
矢量存储界面提供了多种删除文档的方法,允许你通过特定文档ID或使用过滤表达式删除数据。
通过文档ID删除
删除文档最简单的方法是提供一份文档ID列表:
void delete(List<String> idList);
该方法删除所有与所提供列表中ID相符的文件。 如果列表中没有任何ID在商店中,它将被忽略。
// Create and add document
Document document = new Document("The World is Big",
Map.of("country", "Netherlands"));
vectorStore.add(List.of(document));
// Delete document by ID
vectorStore.delete(List.of(document.getId()));
通过过滤器表达式删除
对于更复杂的删除标准,可以使用过滤表达式:
void delete(Filter.Expression filterExpression);
该方法接受滤波。表达定义删除哪些文档的对象。
当你需要根据元数据属性删除文档时,它尤其有用。
// Create test documents with different metadata
Document bgDocument = new Document("The World is Big",
Map.of("country", "Bulgaria"));
Document nlDocument = new Document("The World is Big",
Map.of("country", "Netherlands"));
// Add documents to the store
vectorStore.add(List.of(bgDocument, nlDocument));
// Delete documents from Bulgaria using filter expression
Filter.Expression filterExpression = new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("country"),
new Filter.Value("Bulgaria")
);
vectorStore.delete(filterExpression);
// Verify deletion with search
SearchRequest request = SearchRequest.builder()
.query("World")
.filterExpression("country == 'Bulgaria'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results will be empty as Bulgarian document was deleted
通过字符串过滤表达式删除
为了方便起见,你也可以使用基于字符串的过滤表达式删除文档:
void delete(String filterExpression);
该方法将提供的字符串Filter转换为滤波。表达内部物体。
当你有字符串格式的过滤条件时,它非常有用。
// Create and add documents
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));
// Delete Bulgarian documents using string filter
vectorStore.delete("country == 'Bulgaria'");
// Verify remaining documents
SearchRequest request = SearchRequest.builder()
.query("World")
.topK(5)
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results will only contain the Netherlands document
调用删除 API 时的错误处理
所有删除方法在出现错误时都可以抛出异常:
最佳实践是将删除作包裹在尝试捕获块中:
try {
vectorStore.delete("country == 'Bulgaria'");
}
catch (Exception e) {
logger.error("Invalid filter expression", e);
}
文档版本控制用例
一个常见的情况是管理文档版本,需要上传新版本,同时移除旧版本。以下是使用滤波表达式处理的方法:
// Create initial document (v1) with version metadata
Document documentV1 = new Document(
"AI and Machine Learning Best Practices",
Map.of(
"docId", "AIML-001",
"version", "1.0",
"lastUpdated", "2024-01-01"
)
);
// Add v1 to the vector store
vectorStore.add(List.of(documentV1));
// Create updated version (v2) of the same document
Document documentV2 = new Document(
"AI and Machine Learning Best Practices - Updated",
Map.of(
"docId", "AIML-001",
"version", "2.0",
"lastUpdated", "2024-02-01"
)
);
// First, delete the old version using filter expression
Filter.Expression deleteOldVersion = new Filter.Expression(
Filter.ExpressionType.AND,
Arrays.asList(
new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("docId"),
new Filter.Value("AIML-001")
),
new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("version"),
new Filter.Value("1.0")
)
)
);
vectorStore.delete(deleteOldVersion);
// Add the new version
vectorStore.add(List.of(documentV2));
// Verify only v2 exists
SearchRequest request = SearchRequest.builder()
.query("AI and Machine Learning")
.filterExpression("docId == 'AIML-001'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results will contain only v2 of the document
你也可以用字符串过滤表达式实现同样的作:
// Delete old version using string filter
vectorStore.delete("docId == 'AIML-001' AND version == '1.0'");
// Add new version
vectorStore.add(List.of(documentV2));