跳到主要内容

向量数据库

DeepSeek V3 中英对照 Vector Databases

向量数据库是一种专门类型的数据库,在 AI 应用中扮演着至关重要的角色。

在向量数据库中,查询与传统的关系型数据库有所不同。它们不进行精确匹配,而是执行相似性搜索。当给定一个向量作为查询时,向量数据库会返回与查询向量“相似”的向量。关于如何在高层次上计算这种相似性的更多详细信息,请参阅 Vector Similarity

向量数据库用于将你的数据与 AI 模型集成。使用的第一步是将你的数据加载到向量数据库中。然后,当用户查询要发送到 AI 模型时,首先会检索一组相似的文档。这些文档作为用户问题的上下文,并与用户的查询一起发送到 AI 模型。这种技术被称为 检索增强生成 (Retrieval Augmented Generation, RAG)

以下部分描述了 Spring AI 接口,用于使用多种向量数据库实现以及一些高级示例用法。

最后一节旨在揭秘向量数据库中相似性搜索的基本方法。

API 概览

本节作为指南,介绍 Spring AI 框架中的 VectorStore 接口及其相关类。

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();
}
}
java

以及相关的 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() {...}
}
java

要将数据插入到向量数据库中,需要将其封装在一个 Document 对象中。Document 类封装了来自数据源(如 PDF 或 Word 文档)的内容,并以字符串形式表示文本。它还包含以键值对形式存储的元数据,其中包括文件名等详细信息。

在插入到向量数据库时,文本内容通过嵌入模型被转换为数值数组,即 float[],这些数值数组被称为向量嵌入。嵌入模型,如 Word2VecGLoVEBERT,或 OpenAI 的 text-embedding-ada-002,用于将单词、句子或段落转换为这些向量嵌入。

向量数据库的作用是存储这些嵌入向量并促进相似性搜索。它本身并不生成嵌入向量。要创建向量嵌入,应使用 EmbeddingModel

接口中的 similaritySearch 方法允许检索与给定查询字符串相似的文档。这些方法可以通过使用以下参数进行微调:

  • k: 一个整数,指定返回的相似文档的最大数量。这通常被称为“前 K 个”搜索或“K 最近邻”(KNN)。

  • threshold: 一个范围在 0 到 1 之间的双精度值,值越接近 1 表示相似度越高。默认情况下,如果你设置阈值为 0.75,例如,只有相似度高于此值的文档才会被返回。

  • Filter.Expression: 一个用于传递流畅 DSL(领域特定语言)表达式的类,其功能类似于 SQL 中的“where”子句,但它仅适用于 Document 的元数据键值对。

  • filterExpression: 一个基于 ANTLR4 的外部 DSL,接受字符串形式的过滤表达式。例如,对于元数据键如 country、year 和 isActive,你可以使用如下表达式:country == 'UK' && year >= 2020 && isActive == true

元数据过滤器 部分查找更多关于 Filter.Expression 的信息。

模式初始化

一些向量存储在使用前需要初始化其后端架构。默认情况下,它不会自动为你初始化。你必须通过为适当的构造函数参数传递一个 boolean 值来选择启用,或者如果使用 Spring Boot,则在 application.propertiesapplication.yml 中将相应的 initialize-schema 属性设置为 true。请查阅你使用的向量存储的文档以获取具体的属性名称。

批处理策略

在使用向量存储时,通常需要嵌入大量文档。虽然一次性调用嵌入所有文档看似简单,但这种方法可能会导致问题。嵌入模型将文本处理为 token,并且有一个最大 token 限制,通常称为上下文窗口大小。这一限制限制了单次嵌入请求中可以处理的文本量。如果尝试在一次调用中嵌入过多的 token,可能会导致错误或截断的嵌入。

为了解决这一令牌限制问题,Spring AI 实现了一种分批处理策略。该方法将大量文档集分解为较小的批次,以适应嵌入模型的最大上下文窗口。分批处理不仅解决了令牌限制问题,还可以提高性能并更有效地利用 API 速率限制。

Spring AI 通过 BatchingStrategy 接口提供了这一功能,该接口允许根据文档的 token 数量对文档进行子批次处理。

核心的 BatchingStrategy 接口定义如下:

public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
java

该接口定义了一个单一方法 batch,该方法接收一个文档列表并返回一个文档批次列表。

默认实现

Spring AI 提供了一个名为 TokenCountBatchingStrategy 的默认实现。该策略根据文档的 token 数量进行批处理,确保每个批次不会超过计算得出的最大输入 token 数量。

TokenCountBatchingStrategy 的主要特性:

  1. 使用 OpenAI 的最大输入 token 数(8191)作为默认上限。

  2. 包含一个保留百分比(默认 10%),为潜在的额外开销提供缓冲。

  3. 计算实际的最大输入 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
);
}
}
java

在此配置中:

  1. EncodingType.CL100K_BASE: 指定用于分词的编码类型。该编码类型由 JTokkitTokenCountEstimator 使用,以准确估计 token 数量。

  2. 8000: 设置最大输入 token 数量。该值应小于或等于您的嵌入模型的最大上下文窗口大小。

  3. 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
);
java

自定义实现

虽然 TokenCountBatchingStrategy 提供了一个强大的默认实现,但你可以根据特定需求自定义批处理策略。这可以通过 Spring Boot 的自动配置来实现。

要自定义批处理策略,请在您的 Spring Boot 应用程序中定义一个 BatchingStrategy bean:

@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
java

这个自定义的 BatchingStrategy 将会自动被应用程序中的 EmbeddingModel 实现使用。

备注

Spring AI 支持的向量存储配置为使用默认的 TokenCountBatchingStrategy。SAP Hana 向量存储目前未配置为批处理。

VectorStore 实现

以下是 VectorStore 接口的可用实现:

在未来的版本中可能会支持更多的实现。

如果你有一个需要 Spring AI 支持的向量数据库,请在 GitHub 上提交一个 issue,或者更好的是,提交一个带有实现的 pull request。

有关每个 VectorStore 实现的信息可以在本章的各个小节中找到。

示例用法

为了计算向量数据库的嵌入,你需要选择一个与使用的高级 AI 模型相匹配的嵌入模型。

例如,使用 OpenAI 的 ChatGPT 时,我们使用 OpenAiEmbeddingModel 和一个名为 text-embedding-ada-002 的模型。

Spring Boot 启动器对 OpenAI 的自动配置使得 EmbeddingModel 的实现可以在 Spring 应用程序上下文中进行依赖注入。

将数据加载到向量存储中的一般用法类似于批量作业,首先将数据加载到 Spring AI 的 Document 类中,然后调用 save 方法。

给定一个指向表示 JSON 文件的源文件的 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);
}
java

之后,当用户的问题被传递到 AI 模型中时,会进行相似性搜索以检索类似的文档,然后这些文档会被“塞入”提示中,作为用户问题的上下文。

String question = <question from user>
List<Document> similarDocuments = store.similaritySearch(this.question);
java

可以在 similaritySearch 方法中传入额外的参数来定义要检索的文档数量以及相似性搜索的阈值。

元数据过滤器

本节描述了可用于查询结果的各种过滤器。

过滤字符串

你可以将一个类似 SQL 的过滤表达式作为 String 传递给 similaritySearch 的重载方法之一。

考虑以下示例:

  • "country == 'BG'"

    • "国家 == 'BG'"
  • "genre == 'drama' && year >= 2020"

    • "类型 == 'drama' && 年份 >= 2020"
  • "genre in ['comedy', 'documentary', 'drama']"

    • "类型 in ['comedy', 'documentary', 'drama']"

Filter.Expression

你可以使用 FilterExpressionBuilder 创建一个 Filter.Expression 实例,它提供了一个流畅的 API。一个简单的示例如下:

FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = this.b.eq("country", "BG").build();
java

你可以使用以下运算符构建复杂的表达式:

EQUALS: '=='
MINUS : '-'
PLUS: '+'
GT: '>'
GE: '>='
LT: '<'
LE: '<='
NE: '!='
text

你可以使用以下运算符来组合表达式:

AND: 'AND' | 'and' | '&&';
OR: 'OR' | 'or' | '||';
text

考虑以下示例:

Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();
java

你也可以使用以下运算符:

IN: 'IN' | 'in';
NIN: 'NIN' | 'nin';
NOT: 'NOT' | 'not';
text

考虑以下示例:

Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();
java

从向量存储中删除文档

Vector Store 接口提供了多种删除文档的方法,允许您通过特定的文档 ID 或使用过滤表达式来移除数据。

按文档 ID 删除

删除文档的最简单方法是提供文档 ID 列表:

void delete(List<String> idList);
java

此方法会移除所有 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()));
java

通过过滤表达式删除

对于更复杂的删除条件,你可以使用过滤表达式:

void delete(Filter.Expression filterExpression);
java

此方法接受一个 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
java

通过字符串过滤表达式删除

为了方便,你也可以使用基于字符串的过滤表达式来删除文档:

void delete(String filterExpression);
java

此方法将提供的字符串过滤器在内部转换为 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
java

调用 Delete API 时的错误处理

所有删除方法在发生错误时都可能抛出异常:

最佳实践是将删除操作包裹在 try-catch 块中:

try {
vectorStore.delete("country == 'Bulgaria'");
}
catch (Exception e) {
logger.error("Invalid filter expression", e);
}
java

文档版本控制用例

一个常见的场景是管理文档版本,其中你需要上传文档的新版本并删除旧版本。以下是使用过滤表达式处理这种情况的方法:

// 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
java

你也可以使用字符串过滤器表达式来完成相同的操作:

// Delete old version using string filter
vectorStore.delete("docId == 'AIML-001' AND version == '1.0'");

// Add new version
vectorStore.add(List.of(documentV2));
java

删除文档时的性能考虑

  • 当您确切知道要删除哪些文档时,按 ID 列表删除通常更快。

  • 基于过滤器的删除可能需要扫描索引以查找匹配的文档;然而,这取决于向量存储的具体实现。

  • 大型删除操作应分批进行,以避免给系统带来过大压力。

  • 在根据文档属性删除时,考虑使用过滤器表达式,而不是先收集 ID。

理解向量

章节摘要