矢量数据库

矢量数据库是一种特殊类型的数据库,在 AI 应用程序中起着至关重要的作用。spring-doc.cadn.net.cn

在矢量数据库中,查询不同于传统的关系数据库。 它们执行相似性搜索,而不是完全匹配。 当给定一个向量作为查询时,向量数据库会返回与查询向量“相似”的向量。 有关如何在高级别计算此相似性的更多详细信息,请参阅 向量相似性.spring-doc.cadn.net.cn

矢量数据库用于将数据与 AI 模型集成。 使用它们的第一步是将您的数据加载到矢量数据库中。 然后,当要将用户查询发送到 AI 模型时,首先检索一组类似的文档。 然后,这些文档用作用户问题的上下文,并与用户的查询一起发送到 AI 模型。 这种技术被称为检索增强生成 (RAG)。spring-doc.cadn.net.cn

以下部分描述了使用多个矢量数据库实现的 Spring AI 接口和一些高级示例用法。spring-doc.cadn.net.cn

最后一部分旨在揭开向量数据库中相似性搜索的基本方法的神秘面纱。spring-doc.cadn.net.cn

API 概述

本节将作为VectorStore接口及其关联的类。spring-doc.cadn.net.cn

Spring AI 提供了一个抽象的 API,用于通过VectorStore接口。spring-doc.cadn.net.cn

这是VectorStore接口定义:spring-doc.cadn.net.cn

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架构工人:spring-doc.cadn.net.cn

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 文档)的内容,并包含表示为字符串的文本。 它还包含键值对形式的元数据,包括文件名等详细信息。spring-doc.cadn.net.cn

插入向量数据库后,文本内容将转换为数字数组或float[],称为向量嵌入,使用嵌入模型。嵌入模型,例如 Word2VecGLoVEBERT,或 OpenAI 的text-embedding-ada-002用于将单词、句子或段落转换为这些向量嵌入。spring-doc.cadn.net.cn

向量数据库的作用是存储和促进这些嵌入的相似性搜索。它本身不会生成嵌入向量。为了创建向量嵌入,EmbeddingModel应该利用。spring-doc.cadn.net.cn

similaritySearch方法允许检索类似于给定查询字符串的文档。可以使用以下参数对这些方法进行微调:spring-doc.cadn.net.cn

  • k:一个整数,指定要返回的相似文档的最大数量。这通常被称为 “top K” 搜索或 “K 最近邻” (KNN)。spring-doc.cadn.net.cn

  • threshold:范围从 0 到 1 的双精度值,其中值越接近 1 表示相似度越高。默认情况下,例如,如果将阈值设置为 0.75,则仅返回相似度高于此值的文档。spring-doc.cadn.net.cn

  • Filter.Expression:用于传递流畅的 DSL(域特定语言)表达式的类,其功能类似于 SQL 中的“where”子句,但它仅适用于Document.spring-doc.cadn.net.cn

  • filterExpression:基于 ANTLR4 的外部 DSL,接受筛选表达式作为字符串。例如,使用 country、year 和isActive,您可以使用如下表达式:country == 'UK' && year >= 2020 && isActive == true.spring-doc.cadn.net.cn

查找更多信息Filter.ExpressionMetadata Filters 部分。spring-doc.cadn.net.cn

Schema 初始化

某些 vector store 要求在使用前初始化其后端 schema。 默认情况下,它不会为您初始化。 您必须通过传递boolean对于适当的构造函数参数,或者,如果使用 Spring Boot,则设置适当的initialize-schemaproperty 设置为trueapplication.propertiesapplication.yml. 有关特定属性名称,请查看您正在使用的矢量存储的文档。spring-doc.cadn.net.cn

分批处理策略

使用矢量存储时,通常需要嵌入大量文档。 虽然一次调用嵌入所有文档似乎很简单,但这种方法可能会导致问题。 嵌入模型将文本作为标记处理,并且具有最大标记限制,通常称为上下文窗口大小。 此限制限制了可在单个嵌入请求中处理的文本量。 尝试在一次调用中嵌入过多的标记可能会导致错误或嵌入被截断。spring-doc.cadn.net.cn

为了解决此令牌限制,Spring AI 实现了批处理策略。 这种方法将大型文档集分解为适合嵌入模型的最大上下文窗口的较小批次。 批处理不仅可以解决令牌限制问题,还可以提高性能并更有效地使用 API 速率限制。spring-doc.cadn.net.cn

Spring AI 通过BatchingStrategy接口,该接口允许根据文档的令牌计数在子批处理中处理文档。spring-doc.cadn.net.cn

核心BatchingStrategy接口定义如下:spring-doc.cadn.net.cn

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

此接口定义了一个方法batch,它采用文档列表并返回文档批次列表。spring-doc.cadn.net.cn

默认实现

Spring AI 提供了一个名为TokenCountBatchingStrategy. 此策略根据文档的令牌计数对文档进行批处理,确保每个批处理不超过计算的最大输入令牌计数。spring-doc.cadn.net.cn

主要特点TokenCountBatchingStrategy:spring-doc.cadn.net.cn

  1. 使用 OpenAI 的最大输入令牌计数 (8191) 作为默认上限。spring-doc.cadn.net.cn

  2. 合并预留百分比(默认为 10%),以便为潜在开销提供缓冲。spring-doc.cadn.net.cn

  3. 计算实际的最大输入令牌计数,如下所示:actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)spring-doc.cadn.net.cn

该策略估计每个文档的令牌计数,在不超过最大输入令牌计数的情况下将它们分组为批次,如果单个文档超过此限制,则引发异常。spring-doc.cadn.net.cn

您还可以自定义TokenCountBatchingStrategy以更好地满足您的特定要求。这可以通过在 Spring Boot 中创建具有自定义参数的新实例来完成@Configuration类。spring-doc.cadn.net.cn

以下是如何创建自定义的示例TokenCountBatchingStrategy豆:spring-doc.cadn.net.cn

@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
        );
    }
}

在此配置中:spring-doc.cadn.net.cn

  1. EncodingType.CL100K_BASE:指定用于分词的编码类型。此编码类型由JTokkitTokenCountEstimator准确估计令牌计数。spring-doc.cadn.net.cn

  2. 8000:设置最大输入令牌计数。此值应小于或等于嵌入模型的最大上下文窗口大小。spring-doc.cadn.net.cn

  3. 0.1:设置预留百分比。从最大输入令牌计数中要预留的令牌的百分比。这会为处理过程中可能增加的令牌计数创建缓冲区。spring-doc.cadn.net.cn

默认情况下,此构造函数使用Document.DEFAULT_CONTENT_FORMATTER用于内容格式设置和MetadataMode.NONE用于元数据处理。如果需要自定义这些参数,可以将 full 构造函数与其他参数一起使用。spring-doc.cadn.net.cn

定义后,此自定义TokenCountBatchingStrategybean 将自动被EmbeddingModelimplementations 替换默认策略。spring-doc.cadn.net.cn

TokenCountBatchingStrategy内部使用TokenCountEstimator(具体来说,JTokkitTokenCountEstimator) 计算令牌计数以实现高效批处理。这可确保根据指定的编码类型进行准确的令牌估计。spring-doc.cadn.net.cn

此外TokenCountBatchingStrategy通过允许您传入自己的TokenCountEstimator接口。此功能使您能够使用根据您的特定需求量身定制的自定义代币计数策略。例如:spring-doc.cadn.net.cn

TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
		this.customEstimator,
    8000,  // maxInputTokenCount
    0.1,   // reservePercentage
    Document.DEFAULT_CONTENT_FORMATTER,
    MetadataMode.NONE
);

自定义实现

TokenCountBatchingStrategy提供了强大的默认实现,您可以自定义 Batching 策略以满足您的特定需求。 这可以通过 Spring Boot 的自动配置来完成。spring-doc.cadn.net.cn

要自定义批处理策略,请定义BatchingStrategybean 在 Spring Boot 应用程序中:spring-doc.cadn.net.cn

@Configuration
public class EmbeddingConfig {
    @Bean
    public BatchingStrategy customBatchingStrategy() {
        return new CustomBatchingStrategy();
    }
}

此自定义BatchingStrategy将由EmbeddingModel实现。spring-doc.cadn.net.cn

Spring AI 支持的向量存储配置为使用默认的TokenCountBatchingStrategy. SAP Hana 矢量存储当前未配置为批处理。

VectorStore 实现

以下是VectorStore接口:spring-doc.cadn.net.cn

未来版本可能支持更多实施。spring-doc.cadn.net.cn

如果你有一个需要 Spring AI 支持的向量数据库,请在 GitHub 上打开一个问题,或者更好的是,提交一个带有实现的拉取请求。spring-doc.cadn.net.cn

每个VectorStore实现可以在本章的小节中找到。spring-doc.cadn.net.cn

示例用法

要计算向量数据库的嵌入向量,您需要选择与正在使用的更高级别 AI 模型匹配的嵌入模型。spring-doc.cadn.net.cn

例如,对于 OpenAI 的 ChatGPT,我们使用OpenAiEmbeddingModel和一个名为text-embedding-ada-002.spring-doc.cadn.net.cn

Spring Boot Starters对 OpenAI 的自动配置使EmbeddingModel在 Spring 应用程序上下文中用于依赖项注入。spring-doc.cadn.net.cn

将数据加载到向量存储中的一般用法是您在类似批处理的工作中执行的作,首先将数据加载到 Spring AI 的Document类,然后调用save方法。spring-doc.cadn.net.cn

给定一个String引用一个源文件,该文件表示一个 JSON 文件,其中包含我们要加载到向量数据库中的数据,我们使用 Spring AI 的JsonReader加载 JSON 中的特定字段,JSON 将它们拆分为小块,然后将这些小块传递给 Vector Store 实现。 这VectorStoreimplementation 计算嵌入向量并将 JSON 和嵌入向量存储在向量数据库中:spring-doc.cadn.net.cn

  @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 模型时,将执行相似性搜索以检索相似文档,然后将这些文档作为用户问题的上下文“塞入”提示中。spring-doc.cadn.net.cn

   String question = <question from user>
   List<Document> similarDocuments = store.similaritySearch(this.question);

其他选项可以传递到similaritySearch定义要检索的文档数和相似性搜索的阈值的方法。spring-doc.cadn.net.cn

元数据过滤器

本节介绍可用于查询结果的各种筛选条件。spring-doc.cadn.net.cn

Filter String

您可以将类似 SQL 的筛选条件表达式作为String复制到similaritySearch重载。spring-doc.cadn.net.cn

请考虑以下示例:spring-doc.cadn.net.cn

Filter.Expression (筛选.表达式)

您可以创建Filter.Expression替换为FilterExpressionBuilder公开 Fluent API。 一个简单的示例如下:spring-doc.cadn.net.cn

FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = this.b.eq("country", "BG").build();

您可以使用以下运算符构建复杂的表达式:spring-doc.cadn.net.cn

EQUALS: '=='
MINUS : '-'
PLUS: '+'
GT: '>'
GE: '>='
LT: '<'
LE: '<='
NE: '!='

您可以使用以下运算符组合表达式:spring-doc.cadn.net.cn

AND: 'AND' | 'and' | '&&';
OR: 'OR' | 'or' | '||';

考虑以下示例:spring-doc.cadn.net.cn

Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();

您还可以使用以下运算符:spring-doc.cadn.net.cn

IN: 'IN' | 'in';
NIN: 'NIN' | 'nin';
NOT: 'NOT' | 'not';

请考虑以下示例:spring-doc.cadn.net.cn

Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();

从 Vector Store 中删除文档

Vector Store 界面提供了多种删除文档的方法,允许您按特定文档 ID 或使用筛选表达式删除数据。spring-doc.cadn.net.cn

按文档 ID 删除

删除文档的最简单方法是提供文档 ID 列表:spring-doc.cadn.net.cn

void delete(List<String> idList);

此方法将删除其 ID 与所提供列表中的 ID 匹配的所有文档。 如果列表中的任何 ID 在 store 中不存在,则将被忽略。spring-doc.cadn.net.cn

用法示例
// 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()));

按筛选表达式删除

对于更复杂的删除条件,您可以使用筛选条件表达式:spring-doc.cadn.net.cn

void delete(Filter.Expression filterExpression);

此方法接受Filter.Expression定义应删除哪些文档的条件的对象。 当您需要根据文档的元数据属性删除文档时,它特别有用。spring-doc.cadn.net.cn

用法示例
// 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 筛选表达式

为方便起见,您还可以使用基于字符串的筛选表达式删除文档:spring-doc.cadn.net.cn

void delete(String filterExpression);

此方法将提供的字符串过滤器转换为Filter.Expressionobject 的 当您具有字符串格式的筛选条件时,它非常有用。spring-doc.cadn.net.cn

用法示例
// 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

调用 Delete API 时的错误处理

如果出现错误,所有删除方法都可能引发异常:spring-doc.cadn.net.cn

最佳做法是将删除作包装在 try-catch 块中:spring-doc.cadn.net.cn

用法示例
try {
    vectorStore.delete("country == 'Bulgaria'");
}
catch (Exception  e) {
    logger.error("Invalid filter expression", e);
}

文档版本控制用例

一种常见情况是管理文档版本,您需要在删除旧版本的同时上传文档的新版本。以下是使用 filter 表达式处理此问题的方法:spring-doc.cadn.net.cn

用法示例
// 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

您还可以使用字符串筛选条件表达式完成相同的作:spring-doc.cadn.net.cn

用法示例
// Delete old version using string filter
vectorStore.delete("docId == 'AIML-001' AND version == '1.0'");

// Add new version
vectorStore.add(List.of(documentV2));

删除文档时的性能注意事项

  • 当您确切知道要删除哪些文档时,按 ID 列表删除通常会更快。spring-doc.cadn.net.cn

  • 基于过滤器的删除可能需要扫描索引以查找匹配的文档;但是,这是特定于 Vector Store 实现的。spring-doc.cadn.net.cn

  • 应批量执行大型删除作,以避免系统不堪重负。spring-doc.cadn.net.cn

  • 根据文档属性删除时,请考虑使用筛选表达式,而不是先收集 ID。spring-doc.cadn.net.cn