ベクトル検索

生成 AI の台頭に伴い、ベクトルデータベースはデータベースの世界で大きな注目を集めています。これらのデータベースは、高次元ベクトルの効率的な保存とクエリを可能にするため、セマンティック検索、レコメンデーションシステム、自然言語理解といったタスクに最適です。

ベクトル検索は、従来の完全一致クエリに頼るのではなく、ベクトル表現(埋め込みとも呼ばれます)を比較することで、意味的に類似したデータを検索する手法です。このアプローチにより、キーワードベースの検索を超えた、インテリジェントでコンテキストを考慮したアプリケーションが可能になります。

Spring Data の文脈において、ベクトル検索は、特に自然言語処理、レコメンデーションシステム、生成 AI といった分野において、インテリジェントでコンテキストアウェアなアプリケーション構築の新たな可能性を切り開きます。Spring Data は、使い慣れたリポジトリ抽象化を用いてベクトルベースのクエリをモデル化することで、開発者が類似性ベースのベクトル対応データベースを、Spring Data プログラミングモデルのシンプルさと一貫性とシームレスに統合することを可能にします。

Hibernate Vector Search を使用するには、プロジェクトに次の依存関係を追加する必要があります。

次の例は、MavenGradle で依存関係を設定する方法を示しています。

  • Maven

  • Gradle

<dependencies>
    <dependency>
      <groupId>org.hibernate.orm</groupId>
      <artifactId>hibernate-vector</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
</dependencies>
dependencies {
    implementation 'org.hibernate.orm:hibernate-vector:${hibernateVersion}'
}
Vector をクエリの型として使用できますが、Hibernate ではベクトル型として float または double 配列が必要であるため、ドメインモデルでは使用できません。

ベクトルモデル

型安全かつ慣用的な方法でベクトル検索をサポートするために、Spring Data は次のコア抽象化を導入しています。

Vector

Vector 型は、典型的には埋め込みモデルによって生成される n 次元の数値埋め込みを表します。Spring Data では、浮動小数点数の配列を包む軽量ラッパーとして定義され、不変性と一貫性を保証します。この型は、検索クエリの入力として、または関連するベクトル表現を格納するためのドメインエンティティのプロパティとして使用できます。

Vector vector = Vector.of(0.23f, 0.11f, 0.77f);

ドメインモデルで Vector を使用すると、生の配列や数値リストを扱う必要がなくなり、より型安全で表現力豊かなメソッドでベクトルデータを処理できるようになります。この抽象化により、様々なベクトルデータベースやライブラリとの統合も容易になります。また、標準の浮動小数点表現(IEEE 754 [Wikipedia] (英語) では float と double)にマッピングされないバイナリベクトルや量子化ベクトルなど、ベンダー固有の最適化も実装できます。ドメインオブジェクトにはベクトルプロパティを持たせることができ、類似性検索に使用できます。次の例を考えてみましょう。

class Comment {

  @Id String id;
  String country;
  String comment;

  @Column(name = "the_embedding")
  @JdbcTypeCode(SqlTypes.VECTOR)
  @Array(length = 5)
  Vector embedding;

  // getters, setters, …
}
ベクトルをドメインオブジェクトに関連付けると、ベクトルはエンティティライフサイクルの一部として読み込まれ、保存されるため、取得および永続化操作に追加のオーバーヘッドが発生する可能性があります。

検索結果

SearchResult<T> 型は、ベクトル類似度クエリの結果をカプセル化します。一致したドメインオブジェクトと、クエリベクトルとの一致度を示す関連度スコアの両方が含まれます。この抽象化により、結果のランキングを構造的に処理できるようになり、開発者はデータとそのコンテキスト関連性の両方を容易に操作できるようになります。

例 1: リポジトリ検索メソッドにおける SearchResult<T> の使用
interface CommentRepository extends Repository<Comment, String> {

  SearchResults<Comment> searchByCountryAndEmbeddingNear(String country, Vector vector, Score distance,
    Limit limit);

  @Query("""
      SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c
      WHERE c.country = ?1
        AND cosine_distance(c.embedding, :embedding) <= :distance
      ORDER BY distance asc""")
  SearchResults<Comment> searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding,
      Score distance);
}

SearchResults<Comment> results = repository.searchByCountryAndEmbeddingNear("en", Vector.of(…), Score.of(0.9), Limit.of(10));

この例では、searchByCountryAndEmbeddingNear メソッドは SearchResult<Comment> インスタンスのリストを含む SearchResults<Comment> オブジェクトを返します。各結果には、一致した Comment エンティティとその関連性スコアが含まれます。

関連性スコアは、一致したベクトルがクエリベクトルとどの程度一致しているかを示す数値です。スコアが距離を表すか類似性を表すかによって、スコアが高いほど一致度が高くなるか低くなるかが変わります。

このスコアを計算するために使用されるスコアリング関数は、基礎となるデータベース、インデックス、入力パラメーターによって異なる場合があります。

スコア、類似度、スコアリング関数

Score 型は、検索結果の関連性を示す数値を保持します。クエリベクトルとの類似度に基づいて結果をランク付けするために使用できます。Score 型は通常、浮動小数点数であり、その解釈(高いほど良い、低いほど良い)は、使用される類似度関数によって異なります。スコアはベクトル検索の副産物であり、検索操作の成功に必須ではありません。スコア値はドメインモデルの一部ではないため、帯域外データとして表現するのが最適です。

通常、スコアは ScoringFunction によって計算されます。このスコアを計算するために使用される実際のスコアリング関数は、基盤となるデータベースによって異なり、検索インデックスまたは入力パラメーターから取得できます。

Spring Data サポートは、次のようなよく使用される関数の定数を宣言します。

ユークリッド距離

二乗差の合計の平方根を含む n 次元空間での直線距離を計算します。

コサイン類似度

まずドット積を計算し、その結果を長さの積で割って正規化することで、2 つのベクトル間の 山括弧 を測定します。

内積

要素ごとの乗算の合計を計算します。

類似度関数の選択は、検索のパフォーマンスとセマンティクスの両方に影響を与える可能性があり、多くの場合、基盤となるデータベースまたはインデックスによって決まります。Spring Data は、データベースのネイティブスコアリング関数の機能と、スコアを使用して結果を絞り込むことができるかどうかを考慮します。

Hibernate は、距離関数呼び出しを PGvector および Oracle のネイティブデータベース関数に変換します。その結果は通常、距離となります。Score の代わりに Similarity を使用する場合、Spring Data は距離スコアを 0 から 1 の間の類似度スコアに正規化します。スコアが高いほど、2 つのベクトルの類似度が高くなります。

例 2: リポジトリ検索メソッドにおける Score と Similarity の使用
interface CommentRepository extends Repository<Comment, String> {

  SearchResults<Comment> searchByEmbeddingNear(Vector vector, ScoringFunction function);

  SearchResults<Comment> searchByEmbeddingNear(Vector vector, Score score);

  SearchResults<Comment> searchByEmbeddingNear(Vector vector, Similarity similarity);

  SearchResults<Comment> searchByEmbeddingNear(Vector vector, Range<Similarity> range);
}

repository.searchByEmbeddingNear(Vector.of(…), ScoringFunction.cosine());                               (1)

repository.searchByEmbeddingNear(Vector.of(…), Score.of(0.9, ScoringFunction.cosine()));                (2)

repository.searchByEmbeddingNear(Vector.of(…), Similarity.of(0.9, ScoringFunction.cosine()));           (3)

repository.searchByEmbeddingNear(Vector.of(…), Similarity.between(0.5, 1, ScoringFunction.euclidean()));(4)
1 検索を実行し、コサインスコアリングを適用して、指定された Vector に類似した結果を返します。
2 検索を実行し、コサイン距離を使用してスコアが 0.9 以下の結果を返します。
3 検索を実行し、スコアを類似度値に正規化します。コサインスコアリングを使用して、類似度が 0.9 以上の結果を返します。
4 検索を実行し、スコアを類似度値に正規化します。ユークリッドスコアリングを使用して、0.5 と 1.0 間の類似度以上の結果を返します。
JPA では、スコアリング関数を選択するために Score または Similarity インスタンスを作成するときに、ScoringFunction を提供する必要があります。

ベクトル探索法

ベクトル検索メソッドは、標準の Spring Data クエリメソッドと同じ規則を使用してリポジトリに定義されます。これらのメソッドは SearchResults<T> を返し、クエリベクトルを定義するために Vector パラメーターを必要とします。実際の実装は、基盤となるデータストアの内部構造と、ベクトル検索に関するその機能に依存します。

Spring Data リポジトリを初めて使用する場合は、リポジトリ定義とクエリ方法の基本をよく理解しておいてください。

一般的に、検索メソッドを宣言するには、次の 2 つの方法があります。

  • クエリ導出

  • 文字列ベースのクエリの宣言

ベクトル検索メソッドでは、クエリベクトルを定義するために Vector パラメーターを宣言する必要があります。

派生検索方法

派生検索メソッドは、メソッド名を使用してクエリを派生します。Vector Search は、検索メソッドを宣言する際に Vector Search を実行するために、以下のキーワードをサポートしています。

表 1: 述語キーワードのクエリ
論理キーワード キーワード表現

NEAR

Near, IsNear

WITHIN

Within, IsWithin

例 3: リポジトリ検索方法における Near および Within キーワードの使用
interface CommentRepository extends Repository<Comment, String> {

  SearchResults<Comment> searchByEmbeddingNear(Vector vector, Score score);

  SearchResults<Comment> searchByEmbeddingWithin(Vector vector, Range<Similarity> range);

  SearchResults<Comment> searchByCountryAndEmbeddingWithin(String country, Vector vector, Range<Similarity> range);
}

派生検索メソッドは、ドメインモデル属性とベクトルパラメーターに関する述語を宣言できます。

派生検索メソッドは、メソッド名を用いてクエリの意図を表現するため、一般的に読みやすく保守性も高くなります。ただし、派生検索メソッドでは、Near/Within キーワードの 2 番目の引数として ScoreRange<Score>、または ScoreFunction を宣言し、そのスコアに基づいて検索結果を絞り込む必要があります。

アノテーション付き検索方法

アノテーション付きメソッドは、クエリのセマンティクスとパラメーターを完全に制御できます。派生メソッドとは異なり、メソッド名の命名規則に依存しません。

アノテーション付き検索メソッドでは、ベクトル検索を実行するために JPQL クエリ全体を定義する必要があります。

例 4: @Query 検索方法の使用
interface CommentRepository extends Repository<Comment, String> {

  @Query("""
      SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c
      WHERE c.country = ?1
        AND cosine_distance(c.embedding, :embedding) <= :distance
      ORDER BY distance asc""")
  SearchResults<Comment> searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding,
      Score distance);

  @Query("""
      SELECT c FROM Comment c
      WHERE c.country = ?1
        AND cosine_distance(c.embedding, :embedding) <= :distance
      ORDER BY cosine_distance(c.embedding, :embedding) asc""")
  List<Comment> findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance);
}

ベクトル検索メソッドでは、射影にスコアや距離を含める必要はありません。SearchResults を返すアノテーション付き検索メソッドを使用する場合、実行メカニズムは、2 番目の射影列が存在する場合、その列にスコア値が保持されていると想定します。

実際のクエリをより細かく制御できるため、Spring Data はクエリとそのパラメーターに関する仮定を減らすことができます。例: Similarity 正規化では、クエリ内のネイティブスコア関数を使用して、指定された類似度をスコア述語値に正規化し、その逆も同様に行います。アノテーション付きクエリでたとえばスコアが定義されていない場合、返される SearchResult<T> のスコア値は 0 になります。

ソート

デフォルトでは、検索結果はスコア順に並べられます。Sort パラメーターを使用すると、並べ替えをオーバーライドできます。

例 5: リポジトリ検索方法での Sort の使用
interface CommentRepository extends Repository<Comment, String> {

  SearchResults<Comment> searchByEmbeddingNearOrderByCountry(Vector vector, Score score);

  SearchResults<Comment> searchByEmbeddingWithin(Vector vector, Score score, Sort sort);
}

カスタムソートでは、スコアをソート条件として指定することはできません。ドメインプロパティのみを参照できます。