Spring AI之向量数据库

这里写自定义目录标题

    • API 概述
      • 数据插入与向量嵌入
      • 相似性搜索参数
    • 模式初始化
    • 批处理策略
      • 核心接口定义
        • 默认实现:TokenCountBatchingStrategy
        • 内部机制与扩展性
      • 自定义批处理策略
    • VectorStore 实现
    • 示例用法
      • 数据加载到向量存储
      • 相似性搜索与上下文注入
      • 关键技术点
    • 元数据过滤器
      • 过滤字符串(Filter String)
      • Filter.Expression 构建器
    • 从向量存储中删除文档
      • 按文档ID删除
      • 按过滤表达式删除
      • 按字符串过滤表达式删除
      • 删除操作的错误处理
      • 文档版本控制场景
      • 性能优化建议

向量数据库是一种在人工智能应用中发挥关键作用的专用数据库类型。
在向量数据库中,查询方式与传统关系型数据库存在显著差异。它们并非进行精确匹配,而是执行相似性搜索。当以向量作为查询输入时,向量数据库会返回与查询向量"相似"的向量集合。关于这种相似性在高层级的计算方式,详见《向量相似性》章节的说明。

向量数据库用于将数据与人工智能模型进行整合。其使用流程的第一步是将数据加载到向量数据库中。当需要向AI模型发送用户查询时,系统会首先检索一组相似文档。这些文档将作为用户问题的上下文信息,与用户查询一起被发送至AI模型。这种技术被称为检索增强生成(Retrieval Augmented Generation, 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, "查询内容不能为空。");
            this.searchRequest.query = query;
            return this;
        }

        public Builder topK(int topK) {
            Assert.isTrue(topK >= 0, "TopK 必须为非负数。");
            this.searchRequest.topK = topK;
            return this;
        }

        public Builder similarityThreshold(double threshold) {
            Assert.isTrue(threshold >= 0 && threshold <= 1, "相似度阈值必须在 [0,1] 范围内。");
            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 文档)的内容,包含以字符串形式表示的文本,以及以键值对形式存储的元数据(如文件名等)。

插入向量数据库时,文本内容会通过 嵌入模型 (如 Word2Vec、GLoVE、BERT 或 OpenAI 的 text-embedding-ada-002)转换为数值数组(float[]),即 向量嵌入 (vector embeddings)。
向量数据库的职责是存储这些嵌入并向其提供相似性搜索功能,但其本身不生成嵌入。生成向量嵌入需使用 EmbeddingModel 。

相似性搜索参数

接口中的 similaritySearch 方法支持通过以下参数优化检索结果:

  • k (topK)
    指定返回的相似文档最大数量(整数)。此参数通常称为 Top K 搜索 或 K 近邻(KNN) 。
  • threshold (相似度阈值)
    范围为 0 到 1 的浮点值,数值越接近 1 表示相似度越高。例如,默认阈值 0.75 仅返回相似度高于此值的文档。
  • Filter.Expression
    用于传递类似 SQL WHERE 子句的领域特定语言(DSL)表达式,但仅作用于文档的元数据键值对。
    示例:若元数据包含 country、year 和 isActive,可使用表达式 country == ‘UK’ && year >= 2020 && isActive == true。
  • filterExpression (字符串形式的过滤表达式)
    基于 ANTLR4 的外部 DSL,接受字符串形式的过滤表达式。

更多关于 Filter.Expression 的信息,请参考 元数据过滤器 章节。

模式初始化

某些向量存储需要先初始化后端模式后才能使用。默认情况下,系统不会自动完成此初始化操作。您需要通过以下方式主动启用:

在构造函数中传递一个布尔值参数,或
如果使用 Spring Boot,在 application.properties 或 application.yml 中将对应的 initialize-schema 属性设置为 true。

批处理策略

在处理向量存储时,通常需要嵌入大量文档。尽管一次性嵌入所有文档看似直接,但这种方法可能导致问题。嵌入模型以令牌(token)为单位处理文本,并存在最大令牌限制(即上下文窗口大小)。单次嵌入请求若超出此限制,可能引发错误或截断嵌入结果。

为解决令牌限制问题,Spring AI 实现了批处理策略 。该策略将大量文档拆分为较小批次,确保每个批次的令牌数不超过嵌入模型的最大上下文窗口限制。批处理不仅规避了令牌限制,还能提升性能并更高效地利用 API 速率限制。

Spring AI 通过 BatchingStrategy 接口提供此功能,支持基于令牌数对文档进行分批处理。

核心接口定义

BatchingStrategy 接口定义如下:

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

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

默认实现:TokenCountBatchingStrategy

Spring AI 提供了默认实现 TokenCountBatchingStrategy,该策略根据文档的令牌数进行分批,确保每批不超过最大输入令牌限制。

核心特性 :

  • 默认使用 OpenAI 的最大输入令牌数(8191)作为上限。
  • 包含保留百分比(默认 10%),为潜在开销提供缓冲。
  • 实际最大输入令牌数计算公式:
    actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)
  • 若单个文档超出限制,将抛出异常。
    自定义配置示例 :
    可通过 Spring Boot 配置类定义自定义参数:
@Configuration
public class EmbeddingConfig {
    @Bean
    public BatchingStrategy customTokenCountBatchingStrategy() {
        return new TokenCountBatchingStrategy(
            EncodingType.CL100K_BASE,  // 指定编码类型
            8000,                      // 最大输入令牌数
            0.1                        // 保留百分比
        );
    }
}

参数说明 :

  • EncodingType.CL100K_BASE:用于令牌计数的编码类型,确保与 JTokkitTokenCountEstimator 兼容。
  • 8000:需小于等于嵌入模型的最大上下文窗口大小。
  • 0.1:保留 10% 的令牌数作为缓冲。
内部机制与扩展性
  • 令牌计数估计器 :
    TokenCountBatchingStrategy 内部使用 JTokkitTokenCountEstimator 估算令牌数,支持通过 TokenCountEstimator 接口自定义实现。
    示例:
TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
    customEstimator,
    8000,    // 最大输入令牌数
    0.1,     // 保留百分比
    Document.DEFAULT_CONTENT_FORMATTER,
    MetadataMode.NONE
);
  • 内容与元数据处理
    默认使用 Document.DEFAULT_CONTENT_FORMATTER 格式化内容,MetadataMode.NONE 忽略元数据。可通过完整构造函数自定义。

自定义批处理策略

若需完全自定义批处理逻辑,可通过实现 BatchingStrategy 接口:

@Configuration
public class EmbeddingConfig {
    @Bean
    public BatchingStrategy customBatchingStrategy() {
        return new CustomBatchingStrategy(); // 自定义实现
    }
}

此自定义策略将自动被 Spring AI 的 EmbeddingModel 实现使用。
注意事项

  • 支持的向量存储 :目前 SAP Hana 向量存储未配置批处理支持。
  • 性能优化 :合理设置 maxInputTokenCount 和 reservePercentage 可平衡吞吐量与稳定性。

VectorStore 实现

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

  • Azure 向量搜索
  • Apache Cassandra
  • Chroma 向量存储
  • Elasticsearch 向量存储
  • GemFire 向量存储
  • MariaDB 向量存储
  • Milvus 向量存储
  • MongoDB Atlas
  • Neo4j 向量存储
  • OpenSearch 向量存储
  • Oracle 向量存储
  • PPostgreSQL/PGVector 向量存储。
  • Pinecone 向量存储
  • Qdrant 向量存储
  • Redis 向量存储
  • SAP HANA 向量存储
  • Typesense 向量存储
  • Weaviate 向量存储
  • SimpleVectorStore - 简单的持久化向量存储实现,适合教育用途。

未来版本可能会支持更多实现。
如果需要 Spring AI 支持您的向量数据库,可在 GitHub 提交问题(issue)或直接通过拉取请求(pull request)贡献实现代码。

示例用法

要计算向量数据库的嵌入(embeddings),需选择与所使用的高层级 AI 模型相匹配的嵌入模型。
例如,使用 OpenAI 的 ChatGPT 时,需选用 OpenAiEmbeddingModel 及名为 text-embedding-ada-002 的模型。
Spring Boot 的 OpenAI starter 会通过自动配置,在 Spring 应用上下文中提供一个 EmbeddingModel 实现,用于依赖注入。

数据加载到向量存储

将数据加载到向量存储的操作通常以批处理模式完成:

  • 将数据加载到 Spring AI 的 Document 类中;
  • 调用 save 方法存储。

示例代码 :

@Autowired
VectorStore vectorStore;

void load(String sourceFile) {
    // 使用 JsonReader 加载 JSON 文件的指定字段(如 price、name 等)
    JsonReader jsonReader = new JsonReader(
        new FileSystemResource(sourceFile),
        "price", "name", "shortDescription", "description", "tags"
    );
    List<Document> documents = jsonReader.get();
    // 调用 VectorStore.add() 存储文档
    this.vectorStore.add(documents); // [[4]]
}

过程说明 :

  • JsonReader 将 JSON 文件拆分为小块(如按字段分割),并封装为 Document 对象;
  • VectorStore 实现会自动计算嵌入(通过 EmbeddingModel),并将 JSON 内容与嵌入向量一并存储到数据库中。

相似性搜索与上下文注入

当用户提问传入 AI 模型时,流程如下:

  • 执行相似性搜索 :
String question = <用户问题>;
List<Document> similarDocuments = store.similaritySearch(question); 
  • 参数调优 :
    • topK:指定返回的相似文档数量(如默认 4 个);
    • similarityThreshold:设置相似度阈值(范围 0-1,值越大匹配越严格)。
  • 上下文注入 :
    检索到的相似文档会被“填充”到 AI 模型的输入提示(prompt)中,作为用户问题的上下文。

关键技术点

  • 批处理策略 :若文档量较大,需通过 TokenCountBatchingStrategy 分批处理,避免超出嵌入模型的上下文窗口限制(如 OpenAI 的 8191 令牌上限)。
  • 元数据过滤 :可通过 Filter.Expression 对元数据进行筛选(如 country == ‘UK’)。

元数据过滤器

本节描述可用于对查询结果进行过滤的多种方式。

过滤字符串(Filter String)

您可以通过 similaritySearch 方法的重载形式,传入类似 SQL 语法的字符串表达式作为过滤条件。
示例 :

"country == 'BG'"                // 等值过滤
"genre == 'drama' && year >= 2020" // 组合条件
"genre in ['comedy', 'documentary', 'drama']" // 枚举过滤

Filter.Expression 构建器

您可以通过 FilterExpressionBuilder 创建 Filter.Expression 实例,以构建链式 API 表达式。
基础示例 :

FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = b.eq("country", "BG").build(); // 等于条件

支持的操作符 :

  • 比较操作符 :
    EQUALS(==)、GT(>)、GE(>=)、LT(<)、LE(<=)、NE(!=)
  • 组合操作符 :
    AND(&& 或 and)、OR(|| 或 or)
  • 集合操作符 :
    IN(包含)、NIN(不包含)、NOT(取反)

复杂表达式示例 :

// 组合条件:genre 为 'drama' 且年份 ≥ 2020
Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build(); 

技术细节

  • 预过滤优化 :部分向量数据库(如 LanceDB)会在向量搜索前应用元数据过滤,以缩小检索范围并降低延迟。
  • 嵌入模型兼容性 :过滤条件仅作用于文档元数据,不影响嵌入向量的计算。

从向量存储中删除文档

向量存储接口(VectorStore)提供了多种删除文档的方法,支持通过文档ID列表 或过滤表达式 删除数据。

按文档ID删除

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

void delete(List<String> idList);

此方法会删除所有ID与列表匹配的文档。若列表中存在不存在的ID,将被忽略。
示例代码 :

// 创建并添加文档
Document document = new Document("The World is Big",
    Map.of("country", "Netherlands")); // 元数据包含国家信息
vectorStore.add(List.of(document));

// 按ID删除文档
vectorStore.delete(List.of(document.getId())); 

按过滤表达式删除

对于复杂删除条件,可使用 Filter.Expression 对象定义删除规则:

void delete(Filter.Expression filterExpression);

此方法适用于基于元数据属性删除文档,例如按国家、年份等条件过滤。
示例代码 :

// 创建不同元数据的测试文档
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));

// 使用过滤表达式删除保加利亚文档
Filter.Expression filterExpression = new Filter.Expression(
    Filter.ExpressionType.EQ, // 等值操作符
    new Filter.Key("country"), // 元数据键
    new Filter.Value("Bulgaria") // 目标值
);
vectorStore.delete(filterExpression); // [[5]][[8]]

// 验证删除结果
SearchRequest request = SearchRequest.builder()
    .query("World")
    .filterExpression("country == 'Bulgaria'") // 过滤条件
    .build();
List<Document> results = vectorStore.similaritySearch(request);
// 结果为空,因Bulgaria文档已被删除

按字符串过滤表达式删除

为简化操作,支持直接传递字符串形式的过滤表达式:

void delete(String filterExpression);

此方法内部将字符串解析为 Filter.Expression 对象,适用于动态生成的过滤条件。
示例代码 :

// 添加文档
vectorStore.add(List.of(bgDocument, nlDocument));

// 使用字符串过滤表达式删除保加利亚文档
vectorStore.delete("country == 'Bulgaria'"); // [[8]]

// 验证剩余文档
SearchRequest request = SearchRequest.builder()
    .query("World")
    .topK(5)
    .build();
List<Document> results = vectorStore.similaritySearch(request);
// 仅返回荷兰文档

删除操作的错误处理

所有删除方法可能因错误抛出异常,建议使用 try-catch 块包裹操作:

try {
    vectorStore.delete("country == 'Bulgaria'");
} catch (Exception e) {
    logger.error("无效的过滤表达式", e); // [[7]]
}

文档版本控制场景

典型用例是管理文档版本 ,例如替换旧版本文档:
实现步骤 :

  • 添加初始版本 :
Document documentV1 = new Document(
    "AI与机器学习最佳实践",
    Map.of(
        "docId", "AIML-001",
        "version", "1.0",
        "lastUpdated", "2024-01-01"
    )
);
vectorStore.add(List.of(documentV1));
  • 删除旧版本并添加新版本 :
// 使用过滤表达式删除旧版本
Filter.Expression deleteOldVersion = new Filter.Expression(
    Filter.ExpressionType.AND,
    Arrays.asList(
        new Filter.Expression(Filter.ExpressionType.EQ, "docId", "AIML-001"),
        new Filter.Expression(Filter.ExpressionType.EQ, "version", "1.0")
    )
);
vectorStore.delete(deleteOldVersion); // [[5]][[8]]

// 添加新版本文档
Document documentV2 = new Document(
    "AI与机器学习最佳实践 - 更新版",
    Map.of(
        "docId", "AIML-001",
        "version", "2.0",
        "lastUpdated", "2024-02-01"
    )
);
vectorStore.add(List.of(documentV2));

// 验证仅保留新版本
SearchRequest request = SearchRequest.builder()
    .query("AI与机器学习")
    .filterExpression("docId == 'AIML-001'")
    .build();
List<Document> results = vectorStore.similaritySearch(request);
// results 仅包含版本2.0的文档

性能优化建议

  • 按ID删除 :若已知具体ID列表,此方式速度更快。
  • 过滤表达式删除 :需扫描索引匹配文档,性能依赖具体向量存储实现。
  • 批量删除 :避免单次删除大量文档,建议分批处理。

你可能感兴趣的:(Spring,AI,spring,人工智能,数据库)