向量資料庫

向量資料庫是一種特殊型別的資料庫,在 AI 應用中發揮著至關重要的作用。

在向量資料庫中,查詢與傳統關係型資料庫不同。它們執行的是相似性搜尋,而非精確匹配。當給定一個向量作為查詢時,向量資料庫會返回與該查詢向量“相似”的向量。關於如何從高層級計算相似性的更多細節,請參閱向量相似性一節。

向量資料庫用於將您的資料與 AI 模型整合。使用它們的第一步是將您的資料載入到向量資料庫中。然後,當要將使用者查詢傳送到 AI 模型時,首先會檢索一組相似的文件。這些文件隨後作為使用者問題的上下文,與使用者查詢一起傳送到 AI 模型。這種技術被稱為檢索增強生成 (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, "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”子句,但它專門應用於 Document 的元資料鍵值對。

  • filterExpression:一個基於 ANTLR4 的外部 DSL,接受字串形式的過濾表示式。例如,對於國家/地區、年份和 isActive 等元資料鍵,您可以使用諸如 country == 'UK' && year >= 2020 && isActive == true. 的表示式。

有關 Filter.Expression 的更多資訊,請參閱元資料過濾器一節。

模式初始化

某些向量儲存在使用前需要初始化其後端模式。預設情況下,不會為您初始化。您必須選擇啟用此功能,透過為相應的建構函式引數傳遞 boolean 值,或者,如果使用 Spring Boot,則在 application.propertiesapplication.yml 中將相應的 initialize-schema 屬性設定為 true。請查閱您正在使用的向量儲存的文件以瞭解具體的屬性名稱。

分批策略

使用向量儲存時,通常需要嵌入大量文件。雖然一次性呼叫嵌入所有文件可能看起來很簡單,但這種方法可能會導致問題。嵌入模型將文字作為 token 處理,並且具有最大 token 限制,通常稱為上下文視窗大小。此限制限制了在單個嵌入請求中可以處理的文字量。嘗試在一次呼叫中嵌入過多的 token 可能會導致錯誤或嵌入向量被截斷。

為了解決此 token 限制,Spring AI 實現了分批策略。此方法將大量文件分解為適合嵌入模型最大上下文視窗的較小批次。分批處理不僅解決了 token 限制問題,還可以提高效能並更有效地利用 API 速率限制。

Spring AI 透過 BatchingStrategy 介面提供此功能,該介面允許根據文件的 token 計數對文件進行子批次處理。

核心 BatchingStrategy 介面定義如下

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

此介面定義了一個方法 batch,該方法接受一個文件列表並返回一個文件批次列表。

預設實現

Spring AI 提供了一個名為 TokenCountBatchingStrategy 的預設實現。此策略根據文件的 token 計數對文件進行分批處理,確保每個批次不超過計算出的最大輸入 token 計數。

TokenCountBatchingStrategy 的主要特性

  1. 使用OpenAI 的最大輸入 token 計數 (8191) 作為預設上限。

  2. 包含一個保留百分比(預設為 10%),為潛在的開銷提供緩衝。

  3. 計算實際最大輸入 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,  // Specify the encoding type
            8000,                      // Set the maximum input token count
            0.1                        // Set the reserve percentage
        );
    }
}

在此配置中

  1. EncodingType.CL100K_BASE:指定用於 tokenization 的編碼型別。此編碼型別由 JTokkitTokenCountEstimator 使用,以準確估算 token 計數。

  2. 8000:設定最大輸入 token 計數。此值應小於或等於您的嵌入模型的最大上下文視窗大小。

  3. 0.1:設定保留百分比。從最大輸入 token 計數中保留的 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(
		this.customEstimator,
    8000,  // maxInputTokenCount
    0.1,   // reservePercentage
    Document.DEFAULT_CONTENT_FORMATTER,
    MetadataMode.NONE
);

自定義實現

雖然 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 實現的資訊可以在本章的子章節中找到。

示例用法

為了計算向量資料庫的嵌入向量,您需要選擇一個與所使用的高階 AI 模型匹配的嵌入模型。

例如,對於 OpenAI 的 ChatGPT,我們使用 OpenAiEmbeddingModel 和名為 text-embedding-ada-002 的模型。

Spring Boot Starter 對 OpenAI 的自動配置使得 EmbeddingModel 的實現可以在 Spring 應用程式上下文中用於依賴注入。

將資料載入到向量儲存中的一般用法就像批處理作業一樣,首先將資料載入到 Spring AI 的 Document 類中,然後呼叫 save 方法。

給定一個指向原始檔的 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 模型時,會執行相似性搜尋以檢索相似文件,然後將這些文件“填充”到提示詞中,作為使用者問題的上下文。

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

可以將附加選項傳遞給 similaritySearch 方法,以定義要檢索的文件數量和相似性搜尋的閾值。

元資料過濾器

本節描述了可用於對查詢結果進行篩選的各種過濾器。

過濾字串

您可以將類似 SQL 的過濾表示式作為 String 傳遞給 similaritySearch 的過載方法之一。

請考慮以下示例:

  • "country == 'BG'"

  • "genre == 'drama' && year >= 2020"

  • "genre in ['comedy', 'documentary', 'drama']"

Filter.Expression

您可以使用公開流暢 API 的 FilterExpressionBuilder 建立 Filter.Expression 的例項。一個簡單的示例如下:

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();

從向量儲存中刪除文件

Vector Store 介面提供了多種刪除文件的方法,允許您透過指定的文件 ID 或使用過濾器表示式刪除資料。

按文件 ID 刪除

刪除文件最簡單的方法是提供一個文件 ID 列表:

void delete(List<String> idList);

此方法刪除列表中所有 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()));

按過濾器表示式刪除

對於更復雜的刪除條件,您可以使用過濾器表示式:

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

按字串過濾器表示式刪除

為了方便起見,您還可以使用基於字串的過濾器表示式刪除文件:

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。

理解向量