ベクトルデータベース

ベクトルデータベースは、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[] に変換されます。Word2Vec [Wikipedia] (英語) GLoVE [Wikipedia] (英語) BERT [Wikipedia] (英語) などの埋め込みモデル、または OpenAI の text-embedding-ada-002 は、単語、文、または段落をこれらのベクトル埋め込みに変換するために使用されます。

ベクトルデータベースのロールは、これらの埋め込みの類似性検索を保存し、容易にすることです。埋め込み自体は生成しません。ベクトル埋め込みを作成するには、EmbeddingModel を利用する必要があります。

インターフェースの similaritySearch メソッドを使用すると、指定されたクエリ文字列に類似したドキュメントを取得できます。これらのメソッドは、次のパラメーターを使用して微調整できます。

  • k: 返される類似ドキュメントの最大数を指定する整数。これは、「上位 K」検索、または「K 最近傍」(KNN) と呼ばれることがよくあります。

  • threshold: 0 ~ 1 の範囲の double 値。1 に近い値ほど類似性が高いことを示します。デフォルトでは、たとえば、しきい値を 0.75 に設定すると、この値を超える類似性を持つドキュメントのみが返されます。

  • Filter.Expression: SQL の 'where' 句と同様に機能する流れるような DSL (ドメイン固有言語) 式を渡すために使用されるクラスですが、Document のメタデータのキーと値のペアにのみ適用されます。

  • filterExpression: フィルター式を文字列として受け入れる ANTLR4 に基づく外部 DSL。例: 国、年、isActive などのメタデータキーでは、次のような式を使用できます。: country == 'UK' && year >= 2020 && isActive == true.

Filter.Expression の詳細については、"メタデータフィルター" セクションを参照してください。

スキーマの初期化

一部のベクトルストアでは、使用前にバックエンドスキーマを初期化する必要があります。デフォルトでは初期化されません。適切なコンストラクター引数に boolean を渡すか、Spring Boot を使用する場合は、適切な initialize-schema プロパティを application.properties または application.yml の true に設定して、オプトインする必要があります。特定のプロパティ名については、使用しているベクトルストアのドキュメントを確認してください。

バッチ戦略

ベクトルストアを使用する場合、大量のドキュメントを埋め込む必要があることがよくあります。1 回の呼び出しですべてのドキュメントを一度に埋め込むのは簡単なように思えるかもしれませんが、このアプローチは課題を引き起こす可能性があります。埋め込みモデルはテキストをトークンとして処理し、最大トークン制限 (コンテキストウィンドウサイズと呼ばれることが多い) があります。この制限により、1 回の埋め込みリクエストで処理できるテキストの量が制限されます。1 回の呼び出しで埋め込もうとするトークンが多すぎると、エラーが発生したり、埋め込みが切り捨てられたりする可能性があります。

このトークン制限に対処するために、Spring AI はバッチ処理戦略を実装しています。このアプローチでは、大量のドキュメントセットを、埋め込みモデルの最大コンテキストウィンドウ内に収まる小さなバッチに分割します。バッチ処理はトークン制限の課題を解決するだけでなく、パフォーマンスの向上や API レート制限のより効率的な使用にもつながります。

Spring AI は、BatchingStrategy インターフェースを通じてこの機能を提供します。これにより、トークン数に基づいてサブバッチでドキュメントを処理できるようになります。

コア BatchingStrategy インターフェースは次のように定義されます。

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

このインターフェースは、ドキュメントのリストを受け取り、ドキュメントバッチのリストを返す単一のメソッド batch を定義します。

デフォルトの実装

Spring AI は、TokenCountBatchingStrategy と呼ばれるデフォルトの実装を提供します。この戦略は、トークン数に基づいてドキュメントをバッチ処理し、各バッチが計算された最大入力トークン数を超えないようにします。

TokenCountBatchingStrategy の主な特徴:

  1. デフォルトの上限として OpenAI の最大入力トークン数 (英語) (8191) を使用します。

  2. 潜在的なオーバーヘッドのためのバッファを提供するために、予約率 (デフォルトは 10%) を組み込みます。

  3. 実際の最大入力トークン数を次のように計算します: 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
        );
    }
}

この構成では、次のようになります。

  1. EncodingType.CL100K_BASE: トークン化に使用するエンコード型を指定します。このエンコード型は、JTokkitTokenCountEstimator によってトークン数を正確に推定するために使用されます。

  2. 8000: 最大入力トークン数を設定します。この値は、埋め込みモデルの最大コンテキストウィンドウサイズ以下にする必要があります。

  3. 0.1: 予約率を設定します。最大入力トークン数から予約するトークンの割合。これにより、処理中にトークン数が増加する可能性に備えてバッファが作成されます。

デフォルトでは、このコンストラクターはコンテンツのフォーマットに Document.DEFAULT_CONTENT_FORMATTER を使用し、メタデータの処理に MetadataMode.NONE を使用します。これらのパラメーターをカスタマイズする必要がある場合は、追加のパラメーターを含む完全なコンストラクターを使用できます。

定義されると、このカスタム TokenCountBatchingStrategy Bean は、デフォルトの戦略を置き換えて、アプリケーション内の EmbeddingModel 実装によって自動的に使用されます。

TokenCountBatchingStrategy は、効率的なバッチ処理のためにトークン数を計算するために、内部的に 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
);

カスタム実装

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 で課題をオープンするか、実装を含めたプルリクエストを送信するとさらに良いでしょう。

VectorStore の各実装に関する情報は、この章のサブセクションに記載されています。

使用例

ベクトルデータベースの埋め込みを計算するには、使用されている高レベルの AI モデルと一致する埋め込みモデルを選択する必要があります。

例: OpenAI の ChatGPT では、OpenAiEmbeddingModel と text-embedding-ada-002 というモデルを使用します。

Spring Boot スターターの OpenAI 用の自動構成により、依存性注入のために Spring アプリケーションコンテキストで EmbeddingModel の実装が利用できるようになります。

ベクトルストアにデータをロードする一般的な使用箇所は、最初にデータを Spring AI の Document クラスにロードし、次に save メソッドを呼び出すことにより、バッチのようなジョブで実行します。

ベクトルデータベースにロードしたいデータを含む JSON ファイルを表すソースファイルへの String 参照が与えられると、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 オーバーロードの 1 つに渡すことができます。

次の例を考えてみましょう。

  • "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.eq("genre", "drama"), b.gte("year", 2020)).build();

ベクトルストアからドキュメントを削除する

ベクトルストアインターフェースには、ドキュメントを削除するための複数のメソッドが用意されており、特定のドキュメント 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 を収集するのではなく、ドキュメントのプロパティに基づいて削除する場合は、フィルター式の使用を検討してください。

ベクトルを理解する