向量数据库
向量数据库是一种特殊类型的数据库,在 AI 应用中发挥着至关重要的作用。
在向量数据库中,查询与传统关系型数据库不同。它们执行的是相似性搜索,而非精确匹配。当给定一个向量作为查询时,向量数据库会返回与该查询向量“相似”的向量。关于如何从高层级计算相似性的更多细节,请参阅向量相似性一节。
向量数据库用于将您的数据与 AI 模型集成。使用它们的第一步是将您的数据加载到向量数据库中。然后,当要将用户查询发送到 AI 模型时,首先会检索一组相似的文档。这些文档随后作为用户问题的上下文,与用户查询一起发送到 AI 模型。这种技术被称为检索增强生成 (RAG)。
以下章节描述了 Spring AI 用于使用多种向量数据库实现的接口以及一些高层级的示例用法。
最后一节旨在揭示向量数据库中相似性搜索的底层方法。
API 概述
本节作为 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, "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() {...}
}
要将数据插入向量数据库,请将其封装在 Document
对象中。Document
类封装了数据源(例如 PDF 或 Word 文档)的内容,并包含表示为字符串的文本。它还包含键值对形式的元数据,包括文件名等详细信息。
插入向量数据库后,文本内容会使用嵌入模型转换为数值数组或 float[]
,这称为向量嵌入。嵌入模型,例如 Word2Vec、GLoVE 和 BERT,或 OpenAI 的 text-embedding-ada-002
,用于将单词、句子或段落转换为这些向量嵌入。
向量数据库的作用是存储这些嵌入向量并促进对其进行相似性搜索。它本身不生成嵌入向量。要创建向量嵌入向量,应使用 EmbeddingModel
。
接口中的 similaritySearch
方法允许检索与给定查询字符串相似的文档。这些方法可以通过使用以下参数进行微调
-
k
:一个整数,指定要返回的相似文档的最大数量。这通常被称为“top K”搜索或“K 近邻”(KNN)。 -
threshold
:一个介于 0 到 1 之间的双精度值,值越接近 1 表示相似度越高。默认情况下,如果您将阈值设置为 0.75,则仅返回相似度高于此值的文档。 -
Filter.Expression
:一个用于传递流畅 DSL(领域特定语言)表达式的类,其功能类似于 SQL 中的“where”子句,但它专门应用于Document
的元数据键值对。 -
filterExpression
:一个基于 ANTLR4 的外部 DSL,接受字符串形式的过滤表达式。例如,对于国家/地区、年份和isActive
等元数据键,您可以使用诸如country == 'UK' && year >= 2020 && isActive == true.
的表达式。
有关 Filter.Expression
的更多信息,请参阅元数据过滤器一节。
模式初始化
某些向量存储在使用前需要初始化其后端模式。默认情况下,不会为您初始化。您必须选择启用此功能,通过为相应的构造函数参数传递 boolean
值,或者,如果使用 Spring Boot,则在 application.properties
或 application.yml
中将相应的 initialize-schema
属性设置为 true
。请查阅您正在使用的向量存储的文档以了解具体的属性名称。
分批策略
使用向量存储时,通常需要嵌入大量文档。虽然一次性调用嵌入所有文档可能看起来很简单,但这种方法可能会导致问题。嵌入模型将文本作为 token 处理,并且具有最大 token 限制,通常称为上下文窗口大小。此限制限制了在单个嵌入请求中可以处理的文本量。尝试在一次调用中嵌入过多的 token 可能会导致错误或嵌入向量被截断。
为了解决此 token 限制,Spring AI 实现了分批策略。此方法将大量文档分解为适合嵌入模型最大上下文窗口的较小批次。分批处理不仅解决了 token 限制问题,还可以提高性能并更有效地利用 API 速率限制。
Spring AI 通过 BatchingStrategy
接口提供此功能,该接口允许根据文档的 token 计数对文档进行子批次处理。
核心 BatchingStrategy
接口定义如下
public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
此接口定义了一个方法 batch
,该方法接受一个文档列表并返回一个文档批次列表。
默认实现
Spring AI 提供了一个名为 TokenCountBatchingStrategy
的默认实现。此策略根据文档的 token 计数对文档进行分批处理,确保每个批次不超过计算出的最大输入 token 计数。
TokenCountBatchingStrategy
的主要特性
-
使用OpenAI 的最大输入 token 计数 (8191) 作为默认上限。
-
包含一个保留百分比(默认为 10%),为潜在的开销提供缓冲。
-
计算实际最大输入 token 计数:
actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)
该策略估算每个文档的 token 计数,将它们分组到不超过最大输入 token 计数的批次中,如果单个文档超过此限制,则抛出异常。
您还可以自定义 TokenCountBatchingStrategy
以更好地满足您的特定要求。这可以通过在 Spring Boot @Configuration
类中创建具有自定义参数的新实例来完成。
以下是创建自定义 TokenCountBatchingStrategy
Bean 的示例:
@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
:指定用于 tokenization 的编码类型。此编码类型由JTokkitTokenCountEstimator
使用,以准确估算 token 计数。 -
8000
:设置最大输入 token 计数。此值应小于或等于您的嵌入模型的最大上下文窗口大小。 -
0.1
:设置保留百分比。从最大输入 token 计数中保留的 token 百分比。这为处理期间潜在的 token 计数增加创建了一个缓冲。
默认情况下,此构造函数使用 Document.DEFAULT_CONTENT_FORMATTER
进行内容格式化,并使用 MetadataMode.NONE
进行元数据处理。如果您需要自定义这些参数,可以使用带附加参数的完整构造函数。
一旦定义,此自定义 TokenCountBatchingStrategy
Bean 将被应用程序中的 EmbeddingModel
实现自动使用,替换默认策略。
TokenCountBatchingStrategy
内部使用 TokenCountEstimator
(特别是 JTokkitTokenCountEstimator
)来计算 token 计数,以实现高效分批。这确保了基于指定编码类型的准确 token 估算。
此外,TokenCountBatchingStrategy
通过允许您传入自己的 TokenCountEstimator
接口实现来提供灵活性。此功能使您能够使用根据您的特定需求定制的自定义 token 计数策略。例如:
TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
this.customEstimator,
8000, // maxInputTokenCount
0.1, // reservePercentage
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);
自定义实现
虽然 TokenCountBatchingStrategy
提供了一个健壮的默认实现,但您可以自定义分批策略以满足您的特定需求。这可以通过 Spring Boot 的自动配置来完成。
要自定义分批策略,请在您的 Spring Boot 应用程序中定义一个 BatchingStrategy
Bean:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
此自定义 BatchingStrategy
随后将被应用程序中的 EmbeddingModel
实现自动使用。
Spring AI 支持的向量存储配置为使用默认的 TokenCountBatchingStrategy 。SAP Hana 向量存储当前未配置分批处理。 |
VectorStore 实现
以下是 VectorStore
接口的可用实现:
-
Azure Vector Search - Azure 向量存储。
-
Apache Cassandra - Apache Cassandra 向量存储。
-
Chroma Vector Store - Chroma 向量存储。
-
GemFire Vector Store - GemFire 向量存储。
-
MariaDB Vector Store - MariaDB 向量存储。
-
Milvus Vector Store - Milvus 向量存储。
-
Neo4j Vector Store - Neo4j 向量存储。
-
OpenSearch Vector Store - OpenSearch 向量存储。
-
Oracle Vector Store - Oracle Database 向量存储。
-
PgVector Store - PostgreSQL/PGVector 向量存储。
-
Pinecone Vector Store - PineCone 向量存储。
-
Qdrant Vector Store - Qdrant 向量存储。
-
Redis Vector Store - Redis 向量存储。
-
SAP Hana Vector Store - SAP HANA 向量存储。
-
Typesense Vector Store - Typesense 向量存储。
-
Weaviate Vector Store - Weaviate 向量存储。
-
SimpleVectorStore - 一个简单的持久化向量存储实现,适合教学目的。
未来的版本可能会支持更多实现。
如果您有需要 Spring AI 支持的向量数据库,请在 GitHub 上提交一个 issue,或者更好的是,提交一个包含实现的 pull request。
关于每个 VectorStore
实现的信息可以在本章的子章节中找到。
示例用法
为了计算向量数据库的嵌入向量,您需要选择一个与所使用的高级 AI 模型匹配的嵌入模型。
例如,对于 OpenAI 的 ChatGPT,我们使用 OpenAiEmbeddingModel
和名为 text-embedding-ada-002
的模型。
Spring Boot Starter 对 OpenAI 的自动配置使得 EmbeddingModel
的实现可以在 Spring 应用程序上下文中用于依赖注入。
将数据加载到向量存储中的一般用法就像批处理作业一样,首先将数据加载到 Spring AI 的 Document
类中,然后调用 save
方法。
给定一个指向源文件的 String
引用,该源文件表示包含我们要加载到向量数据库中的数据的 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);
可以将附加选项传递给 similaritySearch
方法,以定义要检索的文档数量和相似性搜索的阈值。
元数据过滤器
本节描述了可用于对查询结果进行筛选的各种过滤器。
过滤字符串
您可以将类似 SQL 的过滤表达式作为 String
传递给 similaritySearch
的重载方法之一。
请考虑以下示例:
-
"country == 'BG'"
-
"genre == 'drama' && year >= 2020"
-
"genre in ['comedy', 'documentary', 'drama']"
Filter.Expression
您可以使用公开流畅 API 的 FilterExpressionBuilder
创建 Filter.Expression
的实例。一个简单的示例如下:
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();
从向量存储中删除文档
Vector Store 接口提供了多种删除文档的方法,允许您通过指定的文档 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);
此方法接受一个 Filter.Expression
对象,该对象定义要删除文档的条件。当您需要根据文档的元数据属性删除文档时,此方法特别有用。
// 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.Expression
对象。当您以字符串格式包含过滤条件时,此方法非常有用。
// 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-catch 块中:
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));