Lzh on GitHub

向量数据库

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

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

在向量数据库中,查询方式不同于传统的关系型数据库。它们不是进行精确匹配,而是执行相似性搜索。当给定一个向量作为查询时,向量数据库会返回与该查询向量 “相似” 的向量。关于这种相似性是如何高层次计算的,可参见向量相似性(Vector Similarity)部分。

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

以下各节将介绍 Spring AI 提供的用于使用多种向量数据库实现的接口,以及一些高层次的示例用法。

最后一节旨在揭示向量数据库中相似性搜索的底层原理。

API 概览

本节旨在介绍 Spring AI 框架中 VectorStore 接口及其相关类的使用指南。

Spring AI 提供了一个抽象化的 API,用于通过 VectorStore 接口与向量数据库进行交互,同时还提供了只读接口 VectorStoreRetriever 接口。

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 <= 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 文档)的内容,并将文本表示为字符串。同时,它还包含以键值对形式存储的元数据,例如文件名等信息。

在插入向量数据库时,文本内容会使用嵌入模型(Embedding Model)转换为数值数组,即 float[],称为向量嵌入(vector embeddings)。嵌入模型如 Word2VecGLoVEBERT,或 OpenAI 的 text-embedding-ada-002 可用于将单词、句子或段落转换为向量嵌入。

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

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

  • k:指定返回的最相似文档的最大数量。通常称为 “Top K” 搜索或 “K 最近邻”(KNN)。
  • threshold:一个 0 到 1 的 double 值,数值越接近 1 表示相似度越高。例如,设置 threshold 为 0.75,则只返回相似度大于 0.75 的文档。
  • Filter.Expression:用于传递流式 DSL(领域特定语言)表达式的类,其作用类似于 SQL 的 where 子句,但仅应用于 Document 的元数据键值对。
  • filterExpression:基于 ANTLR4 的外部 DSL,接受字符串形式的过滤表达式。例如,假设元数据键为 country、year 和 isActive,可以使用表达式:country == 'UK' && year >= 2020 && isActive == true

有关 Filter.Expression 的更多信息,请参见 Metadata Filters 部分。

模式初始化

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

批处理策略

在使用向量存储时,通常需要对大量文档进行嵌入(embedding)。虽然一次性调用将所有文档嵌入看起来很简单,但这种方法可能会导致问题。嵌入模型以 token 为单位处理文本,并有最大 token 限制,通常称为上下文窗口大小(context window size)。这一限制会限制单次嵌入请求中可处理的文本量。尝试一次性嵌入过多 token 可能会导致错误或嵌入被截断。

为了解决这个 token 限制问题,Spring AI 实现了批处理策略(batching strategy)。该方法将大量文档拆分为较小的批次,使其符合嵌入模型的最大上下文窗口要求。批处理不仅解决了 token 限制问题,还可以提高性能并更有效地利用 API 速率限制。

Spring AI 通过 BatchingStrategy 接口提供了此功能,允许根据文档的 token 数量将文档分批处理。

核心的 BatchingStrategy 接口定义如下:

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

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

默认实现

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

TokenCountBatchingStrategy 的主要特点:

  • 使用 OpenAI 的最大输入 token 数(8191)作为默认上限。
  • 包含保留百分比(默认 10%),为潜在开销提供缓冲。
  • 实际最大输入 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,  // 指定编码类型
            8000,                      // 设置最大输入 token 数
            0.1                        // 设置保留百分比
        );
    }
}

在该配置中:

  • EncodingType.CL100K_BASE:指定用于分词的编码类型,该编码类型由 JTokkitTokenCountEstimator 使用,以准确估算 token 数量。
  • 8000:设置最大输入 token 数,该值应小于或等于嵌入模型的最大上下文窗口大小。
  • 0.1:设置保留百分比,从最大输入 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(
    customEstimator,
    8000,  // maxInputTokenCount
    0.1,   // reservePercentage
    Document.DEFAULT_CONTENT_FORMATTER,
    MetadataMode.NONE
);

使用自动截断

某些嵌入模型(例如 Vertex AI 文本嵌入模型)支持 auto_truncate(自动截断)功能。启用时,模型会自动截断超过最大长度的文本输入并继续处理;禁用时,如果输入过大,则会抛出明确的错误。

在使用自动截断与批处理策略(batching strategy)时,必须将批处理策略的最大输入 token 数设置得远高于模型的实际最大值。这可以防止批处理策略因大文档而抛出异常,让嵌入模型自行处理截断。

自动截断的配置

启用自动截断时,应将批处理策略的最大输入 token 数设置得远高于模型实际限制,从而防止批处理策略在遇到大文档时抛出异常,并允许嵌入模型内部处理截断。

示例:在 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() {
        // 仅在嵌入模型启用自动截断时使用高 token 限制
        // 设置远高于模型实际支持的 token 数(例如 Vertex AI 支持 20,000,但这里设为 132,900)
        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();
    }
}

在该配置中:

  • 嵌入模型启用了自动截断,可优雅处理超大输入。
  • 批处理策略使用远高于实际模型限制(132,900 vs 20,000)的 token 上限。
  • 向量存储使用配置好的嵌入模型和自定义批处理策略(BatchingStrategy) Bean。

原理

  • TokenCountBatchingStrategy 会检查单个文档是否超过配置的最大值,如果超过则抛出 IllegalArgumentException
  • 通过将批处理策略上限设置得非常高,可以确保检查不会失败。
  • 超过模型限制的文档或批次将由嵌入模型的自动截断功能静默处理。

最佳实践

  • 将批处理策略的最大输入 token 数设置为模型实际限制的至少 5~10 倍,避免批处理策略过早抛异常。
  • 监控嵌入模型的截断警告日志(注意:并非所有模型都会记录截断事件)。
  • 考虑静默截断对嵌入质量的影响。
  • 使用示例文档测试,确保截断后的嵌入仍符合需求。
  • 对该配置进行文档化,以便后续维护。
自动截断虽然防止错误,但可能导致嵌入不完整,长文档结尾的重要信息可能丢失。如果应用需要完整嵌入,请在嵌入前将文档拆分为较小的片段。

Spring Boot 自动配置

如果使用 Spring Boot 自动配置,必须提供自定义的 BatchingStrategy Bean,以覆盖 Spring AI 默认策略:

@Bean
public BatchingStrategy customBatchingStrategy() {
    // 该 Bean 会覆盖默认 BatchingStrategy
    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();
    }
}

此自定义 BatchingStrategy 将会被应用中所有 EmbeddingModel 实现自动使用。

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

VectorStore 实现

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

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

如果你使用的向量数据库尚未被 Spring AI 支持,可以在 GitHub 上提交 issue,或者更好的是提交包含实现的 pull request。

关于每个 VectorStore 实现的详细信息,可以在本章节的各个子章节中找到。

示例用法

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

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

Spring Boot Starter 对 OpenAI 的自动配置会在 Spring 应用上下文中提供一个 EmbeddingModel 的实现,便于依赖注入使用。

写入向量存储

将数据加载到向量存储的一般用法通常是在批处理任务中完成的,首先将数据加载到 Spring AI 的 Document 类中,然后调用 VectorStore 接口的 add 方法。

假设有一个指向源文件的 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);
}

从向量存储读取

随后,当用户问题被传入 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 的优势

  1. 关注点分离:清晰地区分了读操作和写操作。
  2. 接口隔离:仅需要检索功能的客户端不会暴露给变更方法。
  3. 函数式接口:对于简单用例,可以通过 lambda 表达式或方法引用来实现。
  4. 依赖减少:仅需执行搜索的组件无需依赖完整的 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(Retrieval-Augmented Generation)应用中非常有用,它用于检索相关文档,为 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 = "Context information:\n" + context + "\n\nUser query: " + userQuery;
        return chatModel.generate(prompt);
    }
}

这种模式在 RAG 应用中实现了检索组件与生成组件的清晰分离。

元数据过滤器

本节介绍可用于查询结果的各种过滤器。

Filter 字符串

你可以将类似 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 = 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();

另外还可以使用:

IS: 'IS' | 'is';
NULL: 'NULL' | 'null';
NOT NULL: 'NOT NULL' | 'not null';

示例:

Expression exp = b.and(b.isNull("year")).build();
Expression exp = b.and(b.isNotNull("year")).build();
需要注意的是,IS NULLIS NOT NULL 并未在所有向量数据库中实现。

从向量存储中删除文档

Vector Store 接口提供了多种删除文档的方法,允许你通过指定文档 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 表达式删除

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

void delete(Filter.Expression filterExpression);

此方法接受一个 Filter.Expression 对象,用于定义应删除的文档的条件。当需要根据文档的元数据属性删除文档时,这种方法特别有用。

示例用法:

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

// 使用搜索验证删除结果
SearchRequest request = SearchRequest.builder()
    .query("World")
    .filterExpression("country == 'Bulgaria'")
    .build();
List<Document> results = vectorStore.similaritySearch(request);
// 由于保加利亚文档已被删除,results 将为空

按字符串 Filter 表达式删除

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

void delete(String filterExpression);

此方法会将提供的字符串过滤器内部转换为 Filter.Expression 对象。当你的过滤条件以字符串形式存在时,这种方法非常实用。

示例用法:

// 创建并添加文档
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));

// 使用字符串过滤器删除保加利亚文档
vectorStore.delete("country == 'Bulgaria'");

// 验证剩余文档
SearchRequest request = SearchRequest.builder()
    .query("World")
    .topK(5)
    .build();
List<Document> results = vectorStore.similaritySearch(request);
// results 将只包含荷兰的文档

调用删除 API 时的错误处理

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

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

示例用法:

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

文档版本控制使用案例

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

示例用法:

// 创建初始文档(v1)并添加版本元数据
Document documentV1 = new Document(
    "AI and Machine Learning Best Practices",
    Map.of(
        "docId", "AIML-001",
        "version", "1.0",
        "lastUpdated", "2024-01-01"
    )
);

// 将 v1 添加到向量存储
vectorStore.add(List.of(documentV1));

// 创建同一文档的更新版本(v2)
Document documentV2 = new Document(
    "AI and Machine Learning Best Practices - Updated",
    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("AI and Machine Learning")
    .filterExpression("docId == 'AIML-001'")
    .build();
List<Document> results = vectorStore.similaritySearch(request);
// results 将只包含文档的 v2 版本

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

// 使用字符串过滤删除旧版本
vectorStore.delete("docId == 'AIML-001' AND version == '1.0'");

// 添加新版本
vectorStore.add(List.of(documentV2));

删除文档时的性能考虑

  • 当你确切知道要删除哪些文档时,通过 ID 列表删除通常更快。
  • 基于过滤器的删除可能需要扫描索引以找到匹配的文档,但这取决于具体的向量存储实现。
  • 对于大规模删除操作,应进行分批处理,以避免对系统造成过大负载。
  • 当需要根据文档属性删除而不是先收集 ID 时,建议使用过滤表达式。

理解向量

理解向量