向量数据库(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 [],称为向量嵌入。诸如 Word2VecGLoVEBERT 等嵌入模型,或 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.propertiesapplication.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();
    }
}

在此配置中:

  1. 嵌入模型启用了自动截断功能,使其能够优雅地处理过大的输入。
  2. 批处理策略采用了一个人为设置的高令牌限制(132,900),这远大于模型的实际限制(20,000)。
  3. 向量存储采用配置的嵌入模型和自定义的批处理策略 Bean。
为何此方法有效(Why This Works)

此方法之所以有效,是因为:

  1. TokenCountBatchingStrategy 会检查是否有任何单个文档超出配置的最大值,若超出则抛出 IllegalArgumentException 异常。
  2. 通过在批处理策略中设置一个极高的限制,我们确保了这项检查永远不会失败。
  3. 超过模型限制的文件或批次将被静默截断,并由嵌入模型的自动截断功能进行处理。
最佳实践(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 AIJsonReader 加载 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-08-03 12:02
最后编辑:Jeebiz  更新时间:2025-09-28 09:15