向量数据库是一种专用数据库类型,在人工智能应用中发挥着至关重要的作用。
在向量数据库中,查询方式与传统关系型数据库不同。向量数据库不进行精确匹配,而是执行相似度搜索。当传入一个向量作为查询条件时,向量数据库会返回与该查询向量“相似”的向量。关于相似度的高层级计算方式,将在向量相似度部分进一步说明。
向量数据库用于将数据与人工智能模型进行集成。使用向量数据库的第一步是将数据加载到其中。之后,当用户查询需要发送至AI模型时,系统会先检索出一批相似文档。这些文档将作为用户问题的上下文,与用户查询一同发送给AI模型。该技术被称为检索增强生成(RAG)。
后续章节将介绍Spring AI中可适配多种向量数据库实现的接口,以及部分高层级使用示例。
最后一部分旨在揭秘向量数据库中相似度搜索的底层实现方式。
接口概述
本章节对Spring AI框架内的VectorStore接口及其相关类进行说明。
Spring AI通过VectorStore接口及其只读子接口VectorStoreRetriever,提供了一套可与向量数据库交互的抽象API。
VectorStoreRetriever接口
Spring AI提供了名为VectorStoreRetriever的只读接口,该接口仅暴露文档检索功能:
@FunctionalInterface
public interface VectorStoreRetriever {
List<Document> similaritySearch(SearchRequest request);
default List<Document> similaritySearch(String query) {
return this.similaritySearch(SearchRequest.builder().query(query).build());
}
}该函数式接口适用于仅需从向量存储库检索文档、无需执行任何修改操作的场景。它遵循最小权限原则,仅开放文档检索所需的必要功能。
VectorStore接口
VectorStore接口继承自VectorStoreRetriever,并新增了数据修改能力:
public interface VectorStore extends DocumentWriter, VectorStoreRetriever {
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) { ... }
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
}VectorStore接口整合了读写操作,支持在向量数据库中添加、删除和检索文档。
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 向向量数据库插入数据时,需将数据封装为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,支持传入字符串格式的过滤表达式。例如元数据包含国家、年份、是否启用等字段时,可使用表达式:country == 'UK' && year >= 2020 && isActive == true。关于Filter.Expression的更多信息可参考元数据过滤章节。模式初始化部分向量存储库使用前需要初始化后端模式,默认不会自动初始化。需要手动开启:传入对应构造函数的布尔参数;若使用Spring Boot,则在application.properties或application.yml中将initialize‑schema属性设为true。具体属性名需查阅对应向量存储库的文档。批处理策略使用向量存储库时,通常需要对大量文档进行嵌入处理。直接一次性调用接口处理全部文档看似简单,但会引发问题。嵌入模型以令牌为单位处理文本,存在最大令牌限制,即上下文窗口大小。该限制约束单次嵌入请求可处理的文本量,单次处理过多令牌会引发报错或嵌入向量截断。为解决令牌限制问题,Spring AI实现了批处理策略。该策略将大量文档拆分为多个小批次,确保每批文本不超出嵌入模型的最大上下文窗口。批处理不仅解决令牌限制问题,还可提升性能、高效利用API调用频率限制。Spring AI通过BatchingStrategy接口提供该功能,可根据文档令牌数量进行子批次处理。核心BatchingStrategy接口定义如下:public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}该接口仅定义一个batch方法,接收文档列表,返回分批次后的文档集合。默认实现Spring AI提供默认实现类TokenCountBatchingStrategy。该策略根据文档令牌数进行批处理,确保每批文档不超过计算后的最大输入令牌数。TokenCountBatchingStrategy核心特性:默认以OpenAI最大输入令牌数(8191)为上限。预留百分比(默认10%),用于应对潜在开销。实际最大输入令牌数计算公式:实际最大输入令牌数 = 原始最大输入令牌数 × (1 − 预留百分比)。该策略会估算每个文档的令牌数,在不超过最大输入令牌数的前提下分组;若单个文档超出限制则抛出异常。可自定义TokenCountBatchingStrategy适配业务需求,在Spring Boot的@Configuration类中创建自定义参数实例即可。自定义TokenCountBatchingStrategy Bean示例:@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customTokenCountBatchingStrategy() {
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE, // 指定编码类型
8000, // 设置最大输入令牌数
0.1 // 设置预留百分比
);
}
}配置说明:EncodingType.CL100K_BASE:指定分词编码类型,JTokkitTokenCountEstimator使用该编码精准估算令牌数量。8000:设置最大输入令牌数,需小于等于嵌入模型的最大上下文窗口。0.1:设置预留百分比,从最大输入令牌数中预留部分额度,应对处理过程中令牌数增加的情况。默认构造函数使用Document.DEFAULT_CONTENT_FORMATTER格式化内容,MetadataMode.NONE处理元数据。如需自定义,可使用包含更多参数的完整构造函数。定义完成后,该自定义TokenCountBatchingStrategy Bean会自动被应用内的EmbeddingModel实现类使用,替代默认策略。TokenCountBatchingStrategy内部使用TokenCountEstimator(具体为JTokkitTokenCountEstimator)计算令牌数以实现高效批处理,基于指定编码类型精准估算令牌数。此外,TokenCountBatchingStrategy支持传入自定义TokenCountEstimator接口实现类,适配个性化令牌统计策略。示例如下:TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
this.customEstimator,
8000, // 最大输入令牌数
0.1, // 预留百分比
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);自动截断处理部分嵌入模型(如Vertex AI文本嵌入)支持auto_truncate特性。开启后,模型会静默截断超出最大长度的文本并继续处理;关闭后,超长输入会直接抛出异常。批处理策略搭配自动截断时,需将批处理策略的输入令牌数上限设置为远高于模型实际最大值,避免批处理策略对超长文档抛出异常,由嵌入模型内部完成截断。自动截断配置开启自动截断时,批处理策略的最大输入令牌数需远大于模型实际限制,避免批处理策略拦截超长文档,由嵌入模型内部处理截断。Vertex AI开启自动截断、自定义批处理策略并适配PgVectorStore的配置示例:@Configuration
public class AutoTruncationEmbeddingConfig {
@Bean
public VertexAiTextEmbeddingModel vertexAiEmbeddingModel(
VertexAiEmbeddingConnectionDetails connectionDetails) {
VertexAiTextEmbeddingOptions options = VertexAiTextEmbeddingOptions.builder()
.model(VertexAiTextEmbeddingOptions.DEFAULT_MODEL_NAME)
.autoTruncate(true) // 开启自动截断
.build();
return new VertexAiTextEmbeddingModel(connectionDetails, options);
}
@Bean
public BatchingStrategy batchingStrategy() {
// 仅在嵌入模型开启自动截断时使用超高令牌限制
// 设置远高于模型实际支持的令牌数
// 例如Vertex AI实际上限20000,此处设为132900
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // 人为设置的超高上限
0.1 // 10%预留额度
);
}
@Bean
public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, BatchingStrategy batchingStrategy) {
return PgVectorStore.builder(jdbcTemplate, embeddingModel)
// 此处省略其他属性
.build();
}
}配置说明:嵌入模型开启自动截断,可平稳处理超长输入。批处理策略使用人为设置的超高令牌上限(132900),远大于模型实际限制(20000)。向量存储库使用配置好的嵌入模型和自定义批处理策略Bean。实现原理TokenCountBatchingStrategy会校验单个文档是否超出配置上限,超出则抛出IllegalArgumentException。批处理策略设置超高上限,可避免该校验触发异常。超出模型限制的文档或批次,由嵌入模型的自动截断功能静默截断并处理。最佳实践使用自动截断时:批处理策略最大输入令牌数至少设为模型实际上限的5‑10倍,避免提前触发批处理策略异常。监控嵌入模型的截断警告日志(部分模型无截断日志)。评估静默截断对嵌入质量的影响。使用样本文档测试,确保截断后的嵌入向量满足业务需求。记录该非常规配置,便于后续维护。自动截断虽可避免报错,但会生成不完整的嵌入向量,长文档末尾的关键信息可能丢失。若应用需要完整嵌入全部内容,需在嵌入前将文档拆分为更小片段。Spring Boot自动配置使用Spring Boot自动配置时,需自定义BatchingStrategy Bean以覆盖Spring AI默认实现:@Bean
public BatchingStrategy customBatchingStrategy() {
// 该Bean将覆盖默认批处理策略
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // 远高于模型实际限制
0.1
);
}应用上下文内存在该Bean时,所有向量存储库会自动替换为该批处理策略。自定义实现TokenCountBatchingStrategy提供了稳健的默认实现,也可基于Spring Boot自动配置自定义批处理策略。自定义方式为在Spring Boot应用中定义BatchingStrategy Bean:@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}应用内的EmbeddingModel实现类会自动使用该自定义BatchingStrategy。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数据库向量存储库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 - 简易向量存储实现,仅用于测试SimpleVectorStore不可用于生产环境,仅适用于测试或演示场景。后续版本可能支持更多实现类。若需Spring AI支持其他向量数据库,可在GitHub提交Issue,或直接提交实现代码的Pull Request。各VectorStore实现类的详细信息见本章子章节。使用示例为向量数据库计算嵌入向量时,需选择与上层AI模型匹配的嵌入模型。例如使用OpenAI的ChatGPT时,需搭配OpenAiEmbeddingModel及text‑embedding‑ada‑002模型。Spring Boot Starter的OpenAI自动配置会在Spring应用上下文注入EmbeddingModel实现类,支持依赖注入。向向量存储库写入数据批量向向量存储库加载数据时,先将数据封装为Spring AI的Document类,再调用VectorStore接口的add方法。以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模型时,系统会执行相似度检索获取相似文档,作为上下文填充至提示词中。只读操作可使用VectorStore接口或更精简的VectorStoreRetriever接口:@Autowired
VectorStoreRetriever retriever; // 此处也可使用VectorStore
String question = "<用户的问题>";
List<Document> similarDocuments = retriever.similaritySearch(question);
// 或使用更精细的检索参数
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(5) // 返回前5条结果
.similarityThreshold(0.7) // 仅返回相似度≥0.7的结果
.build();
List<Document> filteredDocuments = retriever.similaritySearch(request);可通过similaritySearch方法传入额外参数,指定检索文档数量及相似度阈值。读写操作分离分离的接口可清晰区分需要写入权限和仅需读取权限的组件:// 拥有完整权限、执行写入操作的服务
@Service
class DocumentIndexer {
private final VectorStore vectorStore;
DocumentIndexer(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void indexDocuments(List<Document> documents) {
vectorStore.add(documents);
}
}
// 仅需检索、执行只读操作的服务
@Service
class DocumentRetriever {
private final VectorStoreRetriever retriever;
DocumentRetriever(VectorStoreRetriever retriever) {
this.retriever = retriever;
}
public List<Document> findSimilar(String query) {
return retriever.similaritySearch(query);
}
}该关注点分离方式仅为真正需要的组件开放修改权限,便于构建更易维护、安全性更高的应用。基于VectorStoreRetriever的检索操作VectorStoreRetriever接口提供向量存储库的只读视图,仅开放相似度检索功能。该接口遵循最小权限原则,尤其适用于RAG(检索增强生成)应用——仅需检索文档、无需修改底层数据。使用VectorStoreRetriever的优势:关注点分离:清晰区分读写操作。接口隔离:仅需检索的客户端不会暴露修改方法。函数式接口:简单场景可通过Lambda表达式或方法引用实现。降低依赖:仅检索的组件无需依赖完整的VectorStore接口。使用示例仅需执行相似度检索时,可直接使用VectorStoreRetriever:@Service
public class DocumentRetrievalService {
private final VectorStoreRetriever retriever;
public DocumentRetrievalService(VectorStoreRetriever retriever) {
this.retriever = retriever;
}
public List<Document> findSimilarDocuments(String query) {
return retriever.similaritySearch(query);
}
public List<Document> findSimilarDocumentsWithFilters(String query, String country) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(5)
.filterExpression("country == '" + country + "'")
.build();
return retriever.similaritySearch(request);
}
}该示例中,服务仅依赖VectorStoreRetriever接口,明确仅执行检索操作、不修改向量存储库。与RAG应用集成VectorStoreRetriever接口在RAG应用中尤为实用,可检索相关文档为AI模型提供上下文:@Service
public class RagService {
private final VectorStoreRetriever retriever;
private final ChatModel chatModel;
public RagService(VectorStoreRetriever retriever, ChatModel chatModel) {
this.retriever = retriever;
this.chatModel = chatModel;
}
public String generateResponse(String userQuery) {
// 检索相关文档
List<Document> relevantDocs = retriever.similaritySearch(userQuery);
// 提取文档内容作为上下文
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// 基于检索上下文生成响应
String prompt = "上下文信息:\n" + context + "\n\n用户查询:" + userQuery;
return chatModel.generate(prompt);
}
}该模式可实现RAG应用中检索组件与生成组件的清晰分离。元数据过滤本章节介绍可用于过滤查询结果的各类筛选方式。字符串过滤表达式可向similaritySearch重载方法传入类SQL的字符串过滤表达式。示例如下:"country == 'BG'""genre == 'drama' && year >= 2020""genre in ['comedy', 'documentary', 'drama']"Filter.Expression可通过FilterExpressionBuilder的流式API创建Filter.Expression实例。简单示例如下:FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = this.b.eq("country", "BG").build();可使用以下运算符构建复杂表达式:等于:==、减:‑、加:+、大于:>、大于等于:>=、小于:<、小于等于:<=、不等于:!=可通过以下运算符组合表达式:与:AND/and/&&;或:OR/or/||示例:Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();还支持以下运算符:包含:IN/in;不包含:NIN/nin;非:NOT/not示例:Expression exp = b.and(b.in("genre", "drama", "documentary"), b.not(b.lt("year", 2020))).build();还支持以下运算符:为空:IS/is;空值:NULL/null;非空:NOT NULL/not null示例:Expression exp = b.and(b.isNull("year")).build();
Expression exp = b.and(b.isNotNull("year")).build();并非所有向量存储库均已实现IS NULL和NOT NULL功能。从向量存储库删除文档Vector Store接口提供多种文档删除方法,可根据指定文档ID或过滤表达式删除数据。根据文档ID删除最简单的删除方式为传入文档ID列表:void delete(List<String> idList);该方法删除所有ID匹配列表的文档;列表中不存在的ID会被忽略。使用示例:// 创建并添加文档
Document document = new Document("世界很大",
Map.of("country", "Netherlands"));
vectorStore.add(List.of(document));
// 根据ID删除文档
vectorStore.delete(List.of(document.getId()));根据过滤表达式删除复杂删除条件可使用过滤表达式:void delete(Filter.Expression filterExpression);该方法接收Filter.Expression对象,定义待删除文档的筛选条件,适用于基于元数据属性删除文档的场景。使用示例:// 创建带不同元数据的测试文档
Document bgDocument = new Document("世界很大",
Map.of("country", "Bulgaria"));
Document nlDocument = new Document("世界很大",
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);
// 检索验证删除结果
SearchRequest request = SearchRequest.builder()
.query("世界")
.filterExpression("country == 'Bulgaria'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// 结果为空,保加利亚相关文档已被删除根据字符串过滤表达式删除为方便使用,也可传入字符串格式的过滤表达式删除文档:void delete(String filterExpression);该方法内部将字符串过滤条件转换为Filter.Expression对象,适用于已有字符串格式筛选条件的场景。使用示例:// 创建并添加文档
Document bgDocument = new Document("世界很大",
Map.of("country", "Bulgaria"));
Document nlDocument = new Document("世界很大",
Map.of("country", "Netherlands"));
vectorStore.add(List.of(bgDocument, nlDocument));
// 通过字符串过滤删除保加利亚相关文档
vectorStore.delete("country == 'Bulgaria'");
// 检索验证剩余文档
SearchRequest request = SearchRequest.builder()
.query("世界")
.topK(5)
.build();
List<Document> results = vectorStore.similaritySearch(request);
// 结果仅包含荷兰相关文档删除API异常处理所有删除方法执行出错时均可能抛出异常。最佳实践为将删除操作包裹在try‑catch代码块中:try {
vectorStore.delete("country == 'Bulgaria'");
}
catch (Exception e) {
logger.error("无效的过滤表达式", e);
}文档版本管理场景常见场景为文档版本管理:上传新版本文档的同时删除旧版本。可通过过滤表达式实现,示例如下:// 创建带版本元数据的初始文档(v1)
Document documentV1 = new Document(
"人工智能与机器学习最佳实践",
Map.of(
"docId", "AIML-001",
"version", "1.0",
"lastUpdated", "2024-01-01"
)
);
// 将v1加入向量存储库
vectorStore.add(List.of(documentV1));
// 创建同一文档的更新版本(v2)
Document documentV2 = new Document(
"人工智能与机器学习最佳实践——更新版",
Map.of(
"docId", "AIML-001",
"version", "2.0",
"lastUpdated", "2024-02-01"
)
);
// 先通过过滤表达式删除旧版本
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);
// 添加新版本
vectorStore.add(List.of(documentV2));
// 验证仅存在v2版本
SearchRequest request = SearchRequest.builder()
.query("人工智能与机器学习")
.filterExpression("docId == 'AIML-001'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// 结果仅包含文档v2版本也可通过字符串过滤表达式实现相同效果:// 通过字符串过滤删除旧版本
vectorStore.delete("docId == 'AIML-001' AND version == '1.0'");
// 添加新版本
vectorStore.add(List.of(documentV2));删除文档的性能考量明确待删除文档ID时,按ID列表删除通常速度更快。基于过滤条件删除可能需要扫描索引匹配文档,具体取决于向量存储库实现。大批量删除操作建议分批执行,避免系统负载过高。基于文档属性删除时,优先使用过滤表达式,而非先收集ID再删除。