向量数据库(Vector Databases)
向量数据库是一种特殊类型的数据库,在人工智能应用中扮演着至关重要的角色。
在向量数据库中,查询方式与传统关系型数据库有所不同。 它们不进行精确匹配,而是执行相似性搜索。 当提供一个向量作为查询时,向量数据库会返回与该查询向量 “相似” 的向量。 关于这种相似性如何在高层次上计算的更多细节,请参见 向量相似性 部分。
向量数据库用于将您的数据与 AI 模型集成。 使用它们的第一步是将数据加载到向量数据库中。 随后,当需要将用户查询发送给 AI 模型时,首先会检索出一组相似文档。 这些文档随后作为用户问题的上下文,与用户的查询一同发送至 AI 模型。 此技术被称为 检索增强生成(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();
}
}
以及相关的 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
子句,但仅适用于文档的元数据键值对。 - filterExpression:一个基于 ANTLR4 的外部 DSL,能够接受字符串形式的过滤表达式。例如,对于如国家、年份和是否活跃这样的元数据键,您可以使用如下表达式:country == ‘UK’ && year >= 2020 && isActive == true.
可以在 Filter.Expression
的 <<metadata-filters>>
部分查找更多信息。
模式初始化(Schema Initialization)
某些向量存储要求在使用前初始化其后端架构。默认情况下,系统不会为您自动完成初始化。您需要通过为相应的构造函数参数传递布尔值来手动选择加入,或者,如果使用 Spring Boot,则需在 application.properties
或 application.yml
中将相应的 initialize-schema
属性设置为 true
。请查阅您所使用的向量存储的文档,以获取具体的属性名称。
分批处理策略(Batching Strategy)
在处理向量存储时,通常需要嵌入大量文档。虽然一次性调用嵌入所有文档看似简单,但这种方法可能导致问题。嵌入模型将文本作为令牌处理,并具有最大令牌限制,通常称为上下文窗口大小。这一限制约束了单次嵌入请求中可处理的文本量。试图在一次调用中嵌入过多令牌可能导致错误或截断的嵌入结果。
为解决这一令牌限制问题,Spring AI 采用了分批处理策略。此方法将大量文档分解成小批量,使其适应嵌入模型的最大上下文窗口。分批处理不仅解决了令牌限制问题,还能提升性能,并更高效地利用 API 速率限制。
Spring AI 通过 BatchingStrategy
接口提供了这一功能,该接口允许根据文档的 token 数量将其划分为子批次进行处理。
核心的 BatchingStrategy
接口定义如下:
public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
此接口定义了一个单一方法,batch
,它接收一份文档列表并返回一组文档批次列表。
默认实现(Default Implementation)
Spring AI 提供了一个名为 TokenCountBatchingStrategy
的默认实现。该策略根据文档的令牌数量进行批处理,确保每个批次的输入令牌数量不超过计算出的最大值。
TokenCountBatchingStrategy 的关键特性:
- 采用 OpenAI 的最大输入令牌数 (8191)作为默认上限。
- 包含预留百分比(默认 10%),为潜在开销提供缓冲空间。
- 计算实际最大输入令牌数为:
actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)
该策略会估算每份文档的令牌数量,将它们分批组合,确保不超过最大输入令牌数,并在单份文档超出此限制时抛出异常。
您还可以自定义 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
: 指定用于分词的编码类型。此编码类型由 JTokkitTokenCountEstimator 使用,以准确估算分词数量。8000
:设置最大输入令牌数。此值应小于或等于您的嵌入模型的最大上下文窗口大小。0.1
:设置预留百分比。从最大输入令牌数中预留的百分比。这为处理过程中可能的令牌数增加提供了缓冲。
默认情况下,此构造函数使用 Document.DEFAULT_CONTENT_FORMATTER
进行内容格式化,并使用 MetadataMode.NONE
进行元数据处理。若需自定义这些参数,可选用带有额外参数的完整构造函数。
一旦定义,这个自定义的 TokenCountBatchingStrategy
bean 将被应用程序中的 EmbeddingModel
实现自动使用,取代默认策略。
TokenCountBatching
策略内部采用 TokenCountEstimator
(具体而言,JTokkitTokenCountEstimator
)来计算令牌数量,以实现高效的批次处理。这确保了基于指定编码类型的准确令牌估算。
此外,TokenCountBatchingStrategy
提供了灵活性,允许您传入自己实现的 TokenCountEstimator
接口。这一特性使您能够使用根据特定需求定制的自定义令牌计数策略。例如:
TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
this.customEstimator,
8000, // maxInputTokenCount
0.1, // reservePercentage
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);
与自动截断功能协作(Working with Auto-Truncation)
某些嵌入模型,例如 Vertex AI 文本嵌入,支持 auto_truncate
功能。当启用此功能时,模型会静默截断超过最大尺寸的文本输入并继续处理;当禁用时,对于过大的输入则会抛出明确的错误。
在使用批处理策略进行自动截断时,必须将批处理策略配置为远高于模型实际最大值的输入令牌计数。这可以防止批处理策略因处理大文档而抛出异常,使得嵌入模型能够在内部处理截断操作。
自动截断配置(Configuration for Auto-Truncation)
启用自动截断功能时,请将批处理策略的最大输入标记数设置得远高于模型的实际限制。这可以防止批处理策略因处理大文档而抛出异常,使得嵌入模型能够在内部自行处理截断。
以下是使用 Vertex AI 结合自动截断功能及自定义 BatchingStrategy
的示例配置,随后在 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();
}
}
在此配置中:
- 嵌入模型启用了自动截断功能,使其能够优雅地处理过大的输入。
- 批处理策略采用了一个人为设置的高令牌限制(132,900),这远大于模型的实际限制(20,000)。
- 向量存储采用配置的嵌入模型和自定义的批处理策略 Bean。
为何此方法有效(Why This Works)
此方法之所以有效,是因为:
TokenCountBatchingStrategy
会检查是否有任何单个文档超出配置的最大值,若超出则抛出IllegalArgumentException
异常。- 通过在批处理策略中设置一个极高的限制,我们确保了这项检查永远不会失败。
- 超过模型限制的文件或批次将被静默截断,并由嵌入模型的自动截断功能进行处理。
最佳实践(Best Practices)
使用自动截断功能时:
- 将批处理策略的最大输入令牌计数设置为模型实际限制的至少 5 到 10 倍,以避免批处理策略过早抛出异常。
- 监控日志以查找来自嵌入模型的截断警告(注意:并非所有模型都会记录截断事件)。
- 考虑静默截断对嵌入质量的影响。
- 通过样本文档进行测试,确保截断后的嵌入仍能满足您的需求。
- 请将此配置记录在案,供未来维护者参考,因为它属于非标准设置。
Spring Boot 自动配置
如果您使用的是 Spring Boot 自动配置,必须提供一个自定义的 BatchingStrategy
bean 来覆盖 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
);
}
应用程序上下文中存在此 bean 将自动替换所有向量存储使用的默认批处理策略。
自定义实现(Custom Implementation)
虽然 TokenCountBatchingStrategy
提供了强大的默认实现,但您可以根据特定需求自定义批处理策略。这可以通过 Spring Boot 的自动配置功能来实现。
要自定义批处理策略,请在您的 Spring Boot 应用程序中定义一个 BatchingStrategy bean:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
这一自定义的批处理策略随后将自动被应用程序中的 EmbeddingModel
实现所采用。
Spring AI 支持的向量存储默认配置为使用 TokenCountBatchingStrategy。SAP Hana 向量存储目前尚未配置批处理功能。
VectorStore 实现
以下是 VectorStore
接口的可用实现:
- Azure Vector Search - Azure 向量存储。
- Apache Cassandra - Apache Cassandra 向量存储。
- Chroma Vector Store - Chroma 向量存储。
- Elasticsearch Vector Store - Elasticsearch 向量存储。
- GemFire Vector Store - GemFire 向量存储。
- MariaDB Vector Store - MariaDB 向量存储。
- Milvus Vector Store - Milvus 向量存储.
- MongoDB Atlas Vector Store - MongoDB Atlas 向量存储.
- 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 上提交问题,或更佳地,直接提交包含实现代码的拉取请求。
关于各 VectorStore
实现的详细信息,可在本章各小节中找到。
示例用法(Example Usage)
为了计算向量数据库的嵌入,你需要选择一个与所使用的高层 AI 模型相匹配的嵌入模型。
例如,使用 OpenAI 的 ChatGPT 时,我们会采用 OpenAiEmbeddingModel
以及一个名为 text-embedding-ada-002
的模型。
Spring Boot 启动器针对 OpenAI 的自动配置使得 EmbeddingModel
的实现可在 Spring 应用上下文中用于依赖注入。
将数据加载到向量存储中的常规操作,类似于批处理作业,首先将数据加载到 Spring AI 的 Document
类中,然后调用 save
方法。
给定一个指向 JSON 数据源文件的字符串引用,我们利用 Spring AI
的 JsonReader
加载 JSON 中的特定字段,将其分割成小块后传递给向量存储实现。向量存储实现计算嵌入向量,并将 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
方法,以定义检索文档的数量
及最近邻搜索的阈值
。
元数据过滤器(Metadata Filters)
本节介绍了可用于查询结果的各种过滤器。
过滤字符串(Filter String)
您可以将类似 SQL 的过滤表达式作为字符串传递给 similaritySearch
的重载方法之一。
参考以下示例:
"country == 'BG'"
"genre == 'drama' && year >= 2020"
"genre in ['comedy', 'documentary', 'drama']"
Filter.Expression
你可以使用 FilterExpressionBuilder
创建一个 Filter.Expression
实例,该构建器提供了一个流畅的 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 删除(Delete by Document IDs)
删除文档的最简单方法是提供文档 ID 列表:
void delete(List<String> idList);
此方法将移除所有 ID 与提供列表中匹配的文档。如果列表中任何 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()));
按过滤表达式删除(Delete by Filter Expression)
对于更复杂的删除条件,您可以使用过滤表达式:
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
通过字符串过滤表达式删除(Delete by String Filter Expression)
为了方便起见,您也可以使用基于字符串的过滤表达式来删除文档:
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));
删除文档时的性能考量
- 按 ID 列表删除通常在你确切知道要移除哪些文档时速度更快。
- 基于过滤器的删除可能需要扫描索引以查找匹配的文档;然而,这一过程的具体实现因向量存储而异。
- 大规模删除操作应分批进行,以免对系统造成过载。
- 在根据文档属性进行删除时,建议直接使用过滤表达式,而非先收集 ID。
最后编辑:Jeebiz 更新时间:2025-09-28 09:15