44 類似検索の実行
Coherenceに格納されているベクトルに対して類似検索を実行するには、SimilaritySearchアグリゲータを使用できます。これを構築する最も簡単な方法は、Aggregators.similaritySearchファクトリ・メソッドを使用することです。
SimilaritySearchアグリゲータを構築する場合は、3つの引数を指定する必要があります:
- マップ・エントリからベクトル属性を取得するために使用する必要がある
ValueExtractor。 - 抽出された値を比較する検索ベクトル。
- 返される結果の最大数。
Bookオブジェクトを含むマップを検索し、最も類似した最大10の本を返すには、このようにSimilaritySearchアグリゲータ・インスタンスを作成します:var searchVector = createEmbedding(searchQuery); // outside of Coherence var search = Aggregators.similaritySearch(Book::getSummaryEmbedding, searchVector, 10);
algorithmメソッドをコールし、別のDistanceAlgorithm実装のインスタンスを渡すことによって変更できます:var search = Aggregators.similaritySearch(Book::getSummaryEmbedding, searchVector, 10)
.algorithm(new L2SquaredDistance());デフォルトでは、CoherenceではCosineDistance、L2SquaredDistance、およびInnerProductDistance実装を提供しますが、DistanceAlgorithmインタフェースを自身で実装することで、追加のアルゴリズムのサポートを簡単に追加できます。
SimilaritySearchアグリゲータのインスタンスを作成した後は、通常どおりNamedMap.aggregateメソッドをコールして類似検索を実行できます:NamedMap<String, Book> books = session.getMap("books");
List<QueryResult<String, Book>> results = books.aggregate(search);検索の結果は、指定された最大QueryResultオブジェクト(前の例では10)のリストで、エントリ・キー、値、および検索ベクトルと指定されたエントリから抽出されたベクトルの間の計算距離が含まれます。結果は、最も近い距離から最も遠い距離の昇順でソートされます。
総当り検索
デフォルトでは、ベクトル属性に索引が定義されていない場合、Coherenceは、すべてのエントリをデシリアライズし、そこからベクトル属性を抽出し、指定された距離アルゴリズムを使用して抽出されたベクトルと検索ベクトルの間の距離計算を実行することで、総当たり検索を実行します。
これは、小規模または中規模のデータ・セットでは問題ありません。Coherenceはクラスタ・メンバー間で並行して検索を実行し、結果を集計しますが、データ・セットが大きくなるときわめて非効率になる場合があります。この場合、サポートされている索引タイプの1つ(後の項で説明)を使用することをお薦めします。
ただし、索引を使用する場合でも、(近似の)索引ベースの検索と(正確な)総当たり検索によって返された結果を比較してリコールをテストするために、総当たりを使用して同じ問合せを実行することが役立つ可能性があります。
bruteForceメソッドをコールして、構成済の索引を無視し、総当たり検索を実行するようにSimilaritySearchアグリゲータを構成できます:var search = Aggregators.similaritySearch(Book::getSummaryEmbedding, searchVector, 10)
.bruteForce();索引付き総当り検索
DeserializationAcceleratorを使用してベクトル属性に順方向のみの索引を作成することで、総当たり検索のパフォーマンスを向上させることができます。NamedMap<String, Book> books = session.getMap("books");
books.addIndex(new DeserializationAccelerator(Book::getSummaryEmbedding));これにより、総当たり検索の実行時に、索引付きベクトル・インスタンスによって消費される追加メモリーを犠牲にして、Book値のデシリアライズが繰り返されることを回避します。
検索では、正確な距離計算が実行されるため、索引付けされていない総当たり検索の場合と同様に、結果は正確になります。
索引ベースの検索
小規模なデータ・セットでは総当たり検索は正常に機能しますが、データ・セットが大きくなるにつれて、ベクトル・プロパティのベクトル索引を作成することを強くお薦めします。
デフォルトでは、CoherenceはHNSWとバイナリ量子化索引の2つのベクトル索引タイプをサポートしています。
HNSW索引
HNSW索引は、MalkovおよびYashuninによって説明されているように、Hierarchical Navigable Small Worldグラフを使用して近似ベクトル検索を実行します。
coherence-hnswモジュールへの依存関係を追加する必要があります:<dependency>
<groupId>${coherence.groupId}</groupId>
<artifactId>coherence-hnsw</artifactId>
<version>${coherence.version}</version>
</dependency>NamedMap<String, Book> books = session.getMap("books");
books.addIndex(new HnswIndex<>(Book::getSummaryEmbedding, 768));HnswIndexコンストラクタの最初の引数は、索引付けするベクトル属性のエクストラクタで、2番目の引数は、各索引付きベクトルが持つ(同一である必要がある)ディメンションの数です。これにより、ネイティブ索引実装で索引に必要なメモリーを事前割当てできます。
HnswIndexはコサイン距離を使用してベクトル距離を計算しますが、これはコンストラクタでspaceName引数を指定することでオーバーライドできます:NamedMap<String, Book> books = session.getMap("books");
books.addIndex(new HnswIndex<>(Book::getSummaryEmbedding, "L2", 768));スペース名の有効な値は、COSINE、L2およびIP(内積)です。
HnswIndexには、その動作を微調整するために使用できる多数のオプションも用意されており、これはFluent APIを使用して指定できます:var hnsw = new HnswIndex<>(Book::getSummaryEmbedding, 768)
.setEfConstr(200)
.setEfSearch(50)
.setM(16)
.setRandomSeed(100);
books.addIndex(hnsw);前述の例のアルゴリズム・パラメータの詳細は、hnswlibのドキュメントを参照してください。
setMaxElementsメソッドをコールして、最大索引サイズを指定することもできます。デフォルトでは、索引は最大サイズが4,096の要素で作成され、データ・セットの拡張に対応するために必要に応じてサイズ変更されます。ただし、サイズ変更操作には多少コストがかかり、索引を作成するCoherenceマップに格納されるエントリの数が事前にわかっている場合は回避できます。その場合は、それに応じて索引サイズを構成する必要があります。
ノート:
Coherenceパーティションは索引付けするため、HNSW索引のインスタンスはパーティションと同じ数になることに注意してください。
つまり、理想的なmaxElements設定は、大きすぎる実際のマップ・サイズではなく、mapSize / partitionCountより少し大きくなります。
HNSW索引を作成して構成した後は、前述の総当たり検索を使用して実行した場合と同じ方法で単純に検索を実行できます。使用可能な場合、CoherenceはHNSW索引を自動的に検出して使用します。
バイナリ量子化
Coherenceでは、バイナリ量子化ベースの索引もサポートしており、HNSWなどのfloat32ベクトルを使用するベクトル索引と比較して、大幅な領域の節約(32x)を実現します。これは、元のベクトルの各32ビット浮動小数点数を0または1に変換し、BitSetの単一ビットを使用して表すことによって行われます。
デメリットは、特により小さいベクトルではリコールが正確でない場合があることですが、結果のオーバーサンプリングおよび再スコアリング(Coherenceが自動的に実行する)によって対処できます。
BinaryQuantIndexはPure Javaで実装され、Coherenceのメイン・ディストリビューションの一部であるため、追加の依存関係は必要ありません。これを作成するには、単純にNamedMap.addIndexメソッドをコールします:NamedMap<String, Book> books = session.getMap("books");
books.addIndex(new BinaryQuantIndex<>(Book::getSummaryEmbedding));指定できるオプションは、oversamplingFactorのみです。これは、返される結果の最大数に対する乗数であり、デフォルトでは3です。つまり、検索アグリゲータが10の結果を返すように構成されている場合、バイナリ量子化検索は最初に検索ベクトルのバイナリ表現と索引ベクトルの間のハミング距離に基づいて30の結果を返し、正確な距離計算を使用して30の結果をすべて再スコアリングし、計算された正確な距離に基づいて上位10の結果を並べ替えて返します。
oversamplingFactorを変更するには、索引の作成時にFluent APIを使用して指定します:NamedMap<String, Book> books = session.getMap("books");
books.addIndex(new BinaryQuantIndex<>(Book::getSummaryEmbedding).oversamplingFactor(5));前述の例では、これによってSimilaritySearchアグリゲータは30ではなく最初に50の結果を返して再スコアリングします。
HNSW索引と同様に、バイナリ量子化索引を作成して構成した後は、総当たり検索を使用して、以前と同じ方法で単純に検索を実行できます。Coherenceは自動的に検出して使用します。
メタデータのフィルタリング
SimilaritySearchアグリゲータがベクトル類似検索と組み合せて使用するメタデータ・フィルタを指定できます:var search = Aggregators.similaritySearch(Book::getSummaryEmbedding, searchVector, 3)
.filter(Filters.equal(Book::getAuthor, "Jules Verne"));
var results = books.aggregate(search);前述の例は、ベクトル類似度に従ってソートされたJules Verneの上位3の著書のみを返す必要があります。
メタデータのフィルタリングは、総当たり検索と索引ベースの検索のどちらを使用するかに関係なく同じように機能し、フィルタするメタデータ属性にある可能性のある索引、この場合はBook::getAuthorなどを使用して、フィルタの評価を高速化します。
長年のCoherenceユーザーの場合、フィルタを受け入れ、集計するエントリのセットを事前にフィルタできるaggregateメソッドを使用するかわりに、アグリゲータ自体にフィルタを設定し、アグリゲータ内でフィルタ評価を実行する理由を疑問に思うことがあります。
理由は、両方のベクトル索引実装でフィルタを内部的に評価する必要があり、trueと評価された場合にのみ結果が含まれるため、前述の例はすべての状況で機能します。
var search = Aggregators.similaritySearch(Book::getSummaryEmbedding, searchVector, 3); var results = books.aggregate(Filters.equal(Book::getAuthor, "Jules Verne"), search);