検索エンジンOSS勉強会第1回の発表資料
発表での議論を基に改修予定
https://home.apache.org/~mikemccand/lucenebench/VectorSearch.html
768次元のword embedding のベクトルに対して、近似近傍探索の結果のベンチマーキング。
Lucene Nightly Benchmarking での大きな改善
GE: 104QPS → 219QPS
GE: 近似近傍探索で複数セグメントに対して並行に検索
Elasticsearch のエンジニアの人が作成している
Lucene のセグメントの概念
docs→file→segment→index
+- Index 5 ------------------------------------------+
| |
| +- Segment _0 ---------------------------------+ |
| | | |
| | +- file 1 -------------------------------+ | |
| | | | | |
| | | +- L.Doc1-+ +- L.Doc2-+ +- L.Doc3-+ | | |
| | | | | | | | | | | |
| | | | field 1 | | field 1 | | field 1 | | | |
| | | | field 2 | | field 2 | | field 2 | | | |
| | | | field 3 | | field 3 | | field 3 | | | |
| | | | | | | | | | | |
| | | +---------+ +---------+ +---------+ | | |
| | | | | |
| | +----------------------------------------+ | |
| | | |
| | | |
| | +- file 2 -------------------------------+ | |
| | | | | |
| | | +- L.Doc4-+ +- L.Doc5-+ +- L.Doc6-+ | | |
| | | | | | | | | | | |
| | | | field 1 | | field 1 | | field 1 | | | |
| | | | field 2 | | field 2 | | field 2 | | | |
| | | | field 3 | | field 3 | | field 3 | | | |
| | | | | | | | | | | |
| | | +---------+ +---------+ +---------+ | | |
| | | | | |
| | +----------------------------------------+ | |
| | | |
| +----------------------------------------------+ |
| |
| +- Segment _1 (optional) ----------------------+ |
| | | |
| +----------------------------------------------+ |
+----------------------------------------------------+
このStackOverflow で共有されているこの公式ドキュメントは理解しやすかった
Apache Lucene - Index File Formats
- 2.9.4のやつなので最新版のpage を探しだせれば差し替える
Lucene の KNN の解説
Lucene comitter の Mike Sokolov さんの発表がわかりやすい
実際に作成されたPRを読んでみる
実際の複数セグメントを並行検索する処理の例 ref
AbstractKnnVectorQuery.java
内で KNNベクトル検索の並行検索がサポートされた
このクラスは名前の通り、近似近傍探索(kNN)を扱うクラス。
LeafReaderContext の解説
Lucene での検索処理における LeafReaderContext の役割とその重要性を、より具体的な例を交えて解説。 Leaf はセグメントを意味していると解釈
Lucene におけるインデックスは、複数のセグメントに分けられており、各セグメントは独立したインデックスとして扱われる。このアーキテクチャにより、データの更新や検索の効率化が図られている。 たとえば、大規模なドキュメント集合に対する更新や追加が発生したとき、全体のインデックスを一から再構築するのではなく、新しいセグメントが作成され、最終的には既存のセグメントとマージされることでインデックスが更新される。
具体例: 検索処理
ユーザーが “Lucene” というキーワードでドキュメントを検索する場面を想定 Lucene のインデックスは複数のセグメントから構成されているため、検索処理は以下のステップで行われる
- クエリ解析: ユーザーの入力から検索クエリが生成されます。この例では、“Lucene” という単語を検索するクエリです。
- セグメントごとの検索: Lucene は、インデックス内の各セグメントに対してクエリを実行します。ここが
LeafReaderContext
の出番。インデックスの全セグメントに対応するLeafReaderContext
オブジェクトのリストを通じて、それぞれのセグメントに対して独立してクエリが実行されます。
例えば、インデックスに 3 つのセグメントがあった場合、
LeafReaderContext1
を使ってsegment 1 を検索LeafReaderContext2
を使ってsegment 2 を検索LeafReaderContext3
を使ってsegment 3 を検索
各 LeafReaderContext
からは、そのセグメント固有の情報(ドキュメントID、頻度など)にアクセスするためのインターフェースが提供されます。この手順により、各セグメントから “Lucene” が含まれるドキュメントが検索されます。
- 結果の集約: セグメントごとの検索結果を集約して、最終的な検索結果をユーザーに表示します。このプロセスでは、各セグメントから得られたドキュメントIDを、全体のインデックスにおける一意のドキュメントIDに割り当て直す必要があります。この割り当ても、
LeafReaderContext
を通じて処理されます。
この例では、LeafReaderContext
の役割がセグメントレベルでの検索処理の実行とその結果の取り扱いにどのように影響するかを示している。具体的には、LeafReaderContext
は検索クエリのセグメントごとの実行を助け、セグメント内の情報に対するアクセスを提供し、さらに全体の検索結果を集約する際の橋渡しをします。また、新しいデータがインデックスに追加された場合や、セグメントがマージされた場合にも、LeafReaderContext
が更新されることで、検索クエリの正確さと迅速さを保証している。
このPRで導入された KnnCollectorManager について
- KNNCollectorManager は KnnCollectorを管理
-
- KnnCollector は、近傍結果からkNN結果を収集する
-
- TopKNNCollectormanager は KNNCollectorManager のサブクラス(のようなもの)
concurrency
がサポートされている場合、BlockingFloatHeap 内に、すべての葉のグローバルトップスコアが収集される (既存のCollectorManager を基に作成したらしい)
検索対象のセグメントが複数セグメントの場合に、並行検索を実行する MultiLeafKnnCollector
今回の実装のパフォーマンス比較実験
Recall は1-10%下がっているが、QPSが最大3倍になる。トレードオフはあるがOPSの向上に価値があるので、Recall の毀損は無視
https://github.com/apache/lucene/pull/12962#issuecomment-1919701631
まとめ
で複数セグメントに対して、並行して近似近傍探索を実行できるようになり、recall は少し既存しているが、QPSは Nightly Benchmarking で2倍ほど改善された。