近似最近傍を検索してベクトル インデックスを作成し、ベクトル エンベディングをクエリする

このページでは、Spanner で次の ANN 距離関数を使用して近似最近傍(ANN)を検索し、ベクトル インデックスを作成して、ベクトル エンベディングをクエリする方法について説明します。

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

データセットが小さい場合は、k 近傍法(KNN)を使用して、正確な k 近傍ベクトルを検索できます。ただし、データセットのサイズが大きくなるにつれて、KNN 検索のレイテンシとコストも増加します。ANN を使用すると、レイテンシと費用を大幅に削減しながら、近似 k 最近傍を検索できます。

近似 K 最近傍

ANN 検索では、近似距離が計算され、データセット内のすべてのベクトルが検査されない可能性があるため、返される k 個のベクトルは真の上位 k 近傍ではありません。上位 k 近傍にないベクトルが返されることがあります。これは「再現損失」と呼ばれます。許容できる再現損失の量はユースケースによって異なりますが、ほとんどの場合、データベースのパフォーマンスを改善するために再現率を多少犠牲にすることは許容されるトレードオフです。

Spanner の近似距離関数の詳細については、以下をご覧ください。

ベクトル インデックス

Spanner は、専用のベクトル インデックスを使用して ANN ベクトル検索を高速化します。このインデックスは、Google Research の高効率な最近傍アルゴリズムである Scalable Nearest Neighbor(ScaNN)を活用しています。

ベクトル インデックスは、ツリーベースの構造を使用してデータをパーティショニングし、高速な検索を可能にします。Spanner では、2 レベルと 3 レベルの両方のツリー構成が可能です。

  • 2 レベルのツリー構成: リーフノード(num_leaves)には、密接に関連するベクトルのグループと、対応するセントロイドが含まれます。ルートレベルは、すべてのリーフノードのセントロイドで構成されます。
  • 3 レベルのツリー構成: 2 レベルのツリーとコンセプトは似ていますが、ブランチレイヤ(num_branches)が追加されています。このレイヤからリーフノード セントロイドがさらにパーティショニングされ、ルートレベル(num_leaves)が形成されます。

Spanner がインデックスを選択します。ただし、特定のインデックスが最適であることをご存じの場合は、FORCE_INDEX ヒントを使用して、ユースケースに最も適したベクトル インデックスを選択できます。

詳細については、VECTOR INDEX ステートメントをご覧ください。

制限事項

  • ベクトル インデックスを事前に分割することはできません。詳細については、事前分割の概要をご覧ください。

ベクトル インデックスを作成する

ベクトル インデックスの再現率とパフォーマンスを最適化するには、次のことをおすすめします。

  • エンベディングを含む行のほとんどがデータベースに書き込まれた後に、ベクトル インデックスを作成します。新しいデータを挿入した後で、ベクトル インデックスを定期的に再構築することが必要になる場合があります。詳細については、ベクトル インデックスを再構築するをご覧ください。

  • STORING 句を使用して、ベクトル インデックスに列のコピーを保存します。列値がベクトル インデックスに保存されている場合、Spanner はインデックスのリーフレベルでフィルタしてクエリのパフォーマンスを向上させます。フィルタ条件で使用される列は保存することをおすすめします。インデックスで STORING を使用する方法の詳細については、インデックスのみをスキャンするインデックスを作成するをご覧ください。

テーブルを作成するときに、エンベディング列は FLOAT32(推奨)または FLOAT64 データ型の配列で、ベクトルのディメンションを示す vector_length アノテーションが必要です。

次の DDL ステートメントは、ベクトル長のエンベディング列 DocEmbedding を持つ Documents テーブルを作成します。

CREATE TABLE Documents (
  DocId INT64 NOT NULL,
  ...
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128),
) PRIMARY KEY (DocId);

Documents テーブルにデータを入力したら、エンベディング列 DocEmbedding を持つ Documents テーブルに、2 レベルのツリーと 1,000 個のリーフノードを持つベクトル インデックスを、コサイン距離を使用して作成できます。

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING (WordCount)
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

3 レベルのツリーと 1,000,000 個のリーフノードを含むベクトル インデックスを作成するには:

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 3, num_branches=1000, num_leaves = 1000000);

エンベディング列がテーブル定義で NOT NULL としてマークされていない場合は、WHERE COLUMN_NAME IS NOT NULL 句で宣言する必要があります。

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

ベクトル インデックスをフィルタする

フィルタされたベクトル インデックスを作成して、データベース内でフィルタ条件に一致する最も類似したアイテムを見つけることができます。フィルタ付きベクトル インデックスは、指定されたフィルタ条件を満たす行のみをインデックスに登録するため、検索パフォーマンスが向上します。

次の例では、テーブル DocumentsCategory という列があります。ベクトル検索で「Tech」カテゴリにインデックスを付けるため、カテゴリ条件が満たされない場合に NULL と評価される生成列を作成します。

CREATE TABLE Documents (
  DocId INT64 NOT NULL,
  Category STRING(MAX),
  NullIfFiltered BOOL AS (IF(Category = 'Tech', TRUE, NULL)) HIDDEN,
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128),
) PRIMARY KEY (DocId);

次に、フィルタを使用してベクトル インデックスを作成します。TechDocEmbeddingIndex ベクトル インデックスには、「Tech」カテゴリのドキュメントのみがインデックスに登録されます。

CREATE VECTOR INDEX TechDocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING(NullIfFiltered)
  WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
  OPTIONS (...);

Spanner は、TechDocEmbeddingIndex に一致するフィルタを含む次のクエリを実行すると、TechDocEmbeddingIndex によって自動的に選択され、高速化されます。このクエリは、「Tech」カテゴリ内のドキュメントのみを検索します。{@FORCE_INDEX=TechDocEmbeddingIndex} を使用して、Spanner に TechDocEmbeddingIndex を明示的に使用させることもできます。

SELECT *
FROM Documents
WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
ORDER BY APPROX_(....)
LIMIT 10;

ベクトル エンベディングのクエリ

ベクトル インデックスをクエリするには、次の 3 つの近似距離関数のいずれかを使用します。

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

近似距離関数を使用する際の制限事項は次のとおりです。

  • 近似距離関数は、エンベディング列と定数式(パラメータやリテラルなど)間の距離を計算する必要があります。
  • 近似距離関数の出力を ORDER BY 句で唯一の並べ替えキーとして使用し、ORDER BY の後に LIMIT を指定する必要があります。
  • クエリで、インデックスに登録されていない行を明示的に除外する必要があります。ほとんどの場合、テーブル定義で列がすでに NOT NULL としてマークされている場合を除き、クエリにベクトル インデックス定義に一致する WHERE <column_name> IS NOT NULL 句を含める必要があります。

制限事項の詳細なリストについては、近似距離関数のリファレンス ページをご覧ください。

[1.0, 2.0, 3.0] に最も近い 100 個のベクトルを検索するには:

SELECT DocId
FROM Documents
WHERE WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], DocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

エンベディング列が null 値を許容する場合:

SELECT DocId
FROM Documents
WHERE NullableDocEmbedding IS NOT NULL AND WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], NullableDocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

ベスト プラクティス

ベクトル インデックスを最適化してクエリ結果を改善するには、次のベスト プラクティスを実施してください。

ベクトル検索オプションを調整する

最適なベクトル検索値は、ユースケース、ベクトル データセット、クエリベクトルによって異なります。特定のワークロードに最適な値を見つけるには、反復的な調整作業が必要になる場合があります。

適切な値を選択する際のガイドラインは次のとおりです。

  • tree_depth(ツリーレベル): インデックスに登録するテーブルの行数が 1,000 万行未満の場合は、tree_depth2 を使用します。それ以外の場合、3tree_depth は最大 100 億行のテーブルをサポートします。

  • num_leaves: データセット内の行数の平方根を使用します。値が大きいと、ベクトル インデックスのビルド時間が長くなる可能性があります。num_leavestable_row_count/1000 より大きく設定しないでください。リーフが小さくなりすぎ、パフォーマンスが低下します。

  • num_leaves_to_search: このオプションは、インデックスのリーフノードを検索する数を指定します。num_leaves_to_search を増やすと再現率は向上しますが、レイテンシとコストも増加します。num_leaves_to_search の値には、CREATE VECTOR INDEX ステートメントで定義されたリーフの合計数の 1% の値を使用することをおすすめします。フィルタ句を使用している場合は、この値を増やすと検索範囲が広がります。

許容できる再現率が達成されたものの、クエリのコストが高すぎて最大 QPS が低い場合は、次の手順で num_leaves を増やしてみてください。

  1. num_leaves を元の値の k 倍に設定します(例: 2 * sqrt(table_row_count))。
  2. num_leaves_to_search を、元の値の k 倍と同じ値に設定します。
  3. num_leaves_to_search を減らして、再現率を維持しながらコストと QPS を改善します。

再現率を改善する

再現率が悪化する原因として、次のようなことが考えられます。

  • num_leaves_to_search が小さすぎる: 一部のクエリベクトルの最近傍を見つけるのが難しくなる場合があります。num_leaves_to_search を増やしてより多くのリーフを検索すると、再現率を改善できます。最近のクエリでは、このような難しいベクトルがより多く含まれるように変化している可能性があります。

  • ベクトル インデックスの再構築が必要: ベクトル インデックスのツリー構造は、作成時にデータセット用に最適化され、その後は静的になります。したがって、最初のベクトル インデックスの作成後に大幅に異なるベクトルが追加された場合、ツリー構造が最適でなくなり、再現率が低下する可能性があります。

ベクトル インデックスを再構築する

ダウンタイムなしでベクトル インデックスを再構築するには:

  1. 現在のベクトル インデックスと同じエンベディング列に新しいベクトル インデックスを作成し、必要に応じてパラメータ(OPTIONS など)を更新します。
  2. インデックスの作成が完了したら、FORCE_INDEX ヒントを使用して新しいインデックスを参照し、ベクトル検索クエリを更新します。これにより、クエリで新しいベクトル インデックスが使用されます。新しいクエリで num_leaves_to_search の再調整が必要になる場合もあります。
  3. 古いベクトル インデックスを削除します。

次のステップ