原作者:SessionBest
元のアドレス:Luceneの基礎となる実装原則、そのインデックス構造
1.Luceneとインデックス作成の原則の概要
このパートは、Luceneの概要、インデックスの原則、Luceneインデックスの実装の3つの側面から拡張されています。
1.1Luceneの概要
Luceneはもともと有名なDougCuttingによって開発され、2000年にオープンソース化され、現在はオープンソースのフルテキスト検索ソリューションに最適です。その特徴は次のように要約されます:完全なJava実装、オープンソース、高性能、完全な機能、簡単な拡張と完全な機能は、単語セグメンテーションのサポート、さまざまなクエリメソッド(プレフィックス、ファジー、通常など)、スコアリングハイライト、列ストレージ(DocValues)などに反映されます。
さらに、Luceneは10年以上開発されていますが、増大するデータ分析のニーズに対応するために活発な開発を続けています。最新バージョン6.0ではブロックkdツリーが導入されており、デジタルタイプと地理的位置情報の検索パフォーマンスが包括的に向上しています。 。さらに、LuceneベースのSolrおよびElasticSearch分散検索および分析システムも本格的に開発されており、ElasticSearchもプロジェクトで使用されています。
Luceneの全体的な使用法を図に示します。
コードを組み合わせて、4つのステップを説明します。
IndexWriter iw=new IndexWriter();//创建IndexWriter
Document doc=new Document( new StringField("name", "Donald Trump", Field.Store.YES)); //构建索引文档
iw.addDocument(doc); //做索引库
IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(index)));
IndexSearcher searcher = new IndexSearcher(reader); //打开索引
Query query = parser.parse("name:trump");//解析查询
TopDocs results =searcher.search(query, 100);//检索并取回前100个文档号
for(ScoreDoc hit:results.hits)
{
Document doc=searcher .doc(hit.doc)//真正取文档
}
使い方はとても簡単ですが、その背後にある原則を知ることによってのみ、Luceneをうまく活用できます。Luceneの一般的な検索の原則と実装の詳細については、後で紹介します。
1.2インデックスの原則
全文検索技術には長い歴史があり、それらのほとんどは転置インデックスに基づいています。ファイルのフィンガープリントなど、他のいくつかの解決策があります。転置インデックスは、その名前が示すように、記事に含まれる単語とは逆です。転置インデックスは、単語から始まり、単語が出現したドキュメントを記録します。辞書と反転テーブルの2つの部分で構成されます。
その中でも、辞書の構造は特に重要です。辞書の構造にはさまざまな種類があり、それぞれに長所と短所があります。最も簡単なのは、配列を並べ替えてバイナリ検索でデータを取得することです。高速なハッシュテーブルがあります。ディスク検索にはBツリーとB +ツリーがありますが、テラバイトのデータをサポートできる逆インデックス構造は、時間と空間のバランスを取る必要があります。次の図に、いくつかの一般的な辞書の長所と短所を示します。
使用可能なものは次のとおりです。B+ツリー、スキップテーブル、FST
B +ツリー:
- 理論的根拠:バランスの取れた多方向探索木
- 利点:外部ストレージインデックス、更新可能
- 短所:スペースが広く、速度が不十分
ジャンプテーブル:
- 利点:単純な構造、制御可能なスキップ間隔、および制御可能なレベル。Lucene3.0より前は、スキップテーブル構造も使用されていましたが、後でFSTに置き換えられました。ただし、スキップテーブルには、反転テーブルのマージやドキュメントなど、Luceneの他のアプリケーションがあります。番号の索引付け。
- 短所:あいまいなクエリのサポートが不十分
FST:
Luceneが現在使用しているインデックス構造
理論的根拠:「最小の非巡回後続トランスデューサの直接構築」。順序付けられた文字列を入力することにより、最小の有向非巡回グラフを構築します。
利点:メモリ使用量が少なく、圧縮率が通常3倍から20倍、ファジークエリのサポートが良好、クエリが高速
短所:複雑な構造、整然とした入力要件、更新が容易ではないLuceneにはFST実装があります。外部インターフェースからは、検索と反復を伴うMap構造と非常によく似ています。
String inputs={"abc","abd","acf","acg"}; //keys
long outputs={1,3,5,7}; //values
FST<Long> fst=new FST<>();
for(int i=0;i<inputs.length;i++)
{
fst.add(inputs[i],outputs[i])
}
//get
Long value=fst.get("abd"); //得到3
//迭代
BytesRefFSTEnum<Long> iterator=new BytesRefFSTEnum<>(fst);
while(iterator.next!=null){...}
100万のデータパフォーマンステスト:
データ構造 | HashMap | TreeMap | FST |
---|---|---|---|
ビルド時間(ミリ秒) | 185 | 500 | 1512 |
すべてのキーを照会する(ミリ秒) | 106 | 218 | 890 |
FSTのパフォーマンスは基本的にHaspMapのパフォーマンスと同じですが、FSTは、HashMapの約10分の1の少量のメモリしか占有しないという比類のない利点があり、これは大規模な検索に不可欠です。結局のところ、速度は速く、メモリに入らないのは無意味です。したがって、修飾された辞書構造には次のものが必要です。
- クエリ速度。
- メモリ使用量。
- メモリ+ディスクの組み合わせ。
後で、LuceneのFST実装の機能からこれらの3つのポイントに焦点を当てて、Luceneインデックス構造を分析します。
1.3Luceneインデックスの実装
何年にもわたる進化と最適化の後、Luceneは図に示すようなインデックスファイル構造になります。これは基本的に、ディクショナリ、反転テーブル、フォワードファイル、列型ストレージDocValuesの3つの部分に分けることができます。
以下に、各パーツの構造の詳細を示します。
1.インデックス構造
Luceneが現在使用しているデータ構造はFSTであり、その特徴は次のとおりです。
- 単語検索の複雑さはO(len(str))です
- 共有プレフィックス、スペースの節約
- メモリはプレフィックスインデックスを格納し、ディスクはサフィックスワードブロックを格納します
これは、前述の辞書構造の3つの要素と一致しています。1。クエリ速度。2.メモリ使用量。3.メモリ+ディスクの組み合わせ。4つの単語abd、abe、acf、acgをインデックスライブラリに挿入し、そのインデックスファイルの内容を確認します。
先端部分では、各列にFSTインデックスがあるため、複数のFSTがあり、各FSTには、プレフィックスとサフィックスのブロックポインターが格納されます。ここで、プレフィックスはa、ab、およびacです。逆テーブルポインタ、TFDFなどの接尾辞ブロックと単語のその他の情報はtimに格納されます。docファイルには、各単語の逆テーブルが含まれています。したがって、その取得プロセスは3つのステップに分けられます。
- チップファイルをメモリにロードし、FST一致プレフィックスを介してサフィックスワードブロックの位置を見つけます。
- ワードブロックの位置に応じて、ディスク上のtimファイルのサフィックスブロックを読み取り、サフィックスと対応する反転テーブルの位置情報を見つけます。
- 反転テーブルの位置に従って、反転テーブルをdocファイルにロードします。
ここでは2つの問題があります。1つはプレフィックスの計算方法、2つ目はディスクにサフィックスを書き込んでFSTを介して見つける方法です。以下では、LuceneがFSTを構築するプロセスについて説明します。FSTは既知です。順番に入力が必要なため、Luceneが解析します。ドキュメントの単語が事前に並べ替えられてから、FSTが構築されます。入力がabd、abd、acf、acgであると仮定すると、構築プロセス全体は次のようになります。
- abdを挿入すると、出力はありません。
- 阿部を挿入すると接頭辞abが計算されますが、現時点ではabの接頭辞が他にないかわからないため、現時点では出力されません。
- acfを挿入するときは、順序どおりであるため、abで始まる単語がなくなることを知っているので、tipとtimを書き込むことができます。timでは、接尾辞ブロックd、eとそれらの反転テーブル位置ip_dを書き込みます。 bおよびip_e、tipのabで始まる接尾辞の単語ブロックの位置(実際の状況では、単語の頻度などの詳細情報が書き込まれます)。
- acgが挿入されると、プレフィックスacがacfと共有されていると計算されます。この時点で、入力は終了し、すべてのデータがディスクに書き込まれます。接尾辞ブロックf、gおよび対応する反転テーブル位置はtimで書き込まれ、cおよびacで始まる接尾辞ブロック位置はtipで書き込まれます。
上記は単純化されたプロセスです。LuceneのFSTによって実装される主な最適化戦略は次のとおりです。
- 接尾辞の最小数。Luceneには、メモリ使用量をさらに削減するために、チップに書き込まれるプレフィックスの最小数のサフィックスがあります。デフォルトは25です。サフィックス番号が25の場合、abおよびacプレフィックスはなく、フォロワーノードは1つだけであり、abd、abe、acf、およびacgはすべてサフィックスとしてtimファイルに保存されます。10gのインデックスライブラリであるインデックスメモリの消費量は約2,000万です。
- プレフィックスの計算はcharではなくbyteに基づいているため、サフィックスの数を減らし、サフィックスの数が多すぎてパフォーマンスに影響を与えるのを防ぐことができます。たとえば、Yu(e9 b8 a2)、Shou(e9 b8 a3)、An(e9 b8 a4)の3つの漢字は、FSTによって作成されます。ルートノードと接尾辞としての3つの漢字だけでなく、次の図に示すように、e9とb8はプレフィックス、a2、a3、a4はサフィックスです。
2.逆テーブル構造
反転テーブルはドキュメント番号のコレクションですが、それを格納および取得する方法については多くの詳細があります。現在Luceneで使用されている反転テーブル構造は、参照フレームと呼ばれ、2つの主な機能があります
。1)データ圧縮: 6を変換する方法を見ることができます。数値は元の24バイトから7バイトに圧縮されています。
2)ブールクエリでは、および/または操作の両方で反転テーブルをマージする必要があるため、ジャンプテーブルはマージを高速化します。次に、同じドキュメント番号をすばやく見つける必要があるため、ジャンプテーブルを使用して同じものを検索します。書類番号。
この部分は、いくつかのパフォーマンステストを含むElasticSearchのブログを参照できます:
ElasticSearch反転テーブル
3.ドキュメントを転送する
転送ファイルは元のドキュメントを参照します。Luceneは元のドキュメントのストレージ機能も提供します。そのストレージ機能はブロック+圧縮です。fdtファイルは元のドキュメントを保存するファイルです。ディスクスペースの90%を占めます。インデックスライブラリ。fdxファイルファイルにインデックスを付けるには、ドキュメント番号(自動インクリメント番号)からドキュメントの場所をすばやく取得します。ファイル構造は次のとおりです
。fnmは、メタのさまざまな列タイプ、列名、保存方法、その他の情報を格納します。情報。
fdtはドキュメントの値です。その中のチャンクはブロックです。Luceneがドキュメントにインデックスを付けると、ドキュメントが最初にキャッシュされます。キャッシュが16KBより大きい場合、ドキュメントは圧縮されて保存されます。チャンクには、チャンクの開始ドキュメント、ドキュメントの数、および圧縮されたドキュメントのコンテンツが含まれます。
fdxはドキュメント番号のインデックスです。反転テーブルを保存すると、ドキュメント番号をfdxですばやく見つけて、ドキュメントの位置、つまりチャンクの位置を見つけることができます。インデックスの構造は比較的単純で、スキップテーブルの構造です。まず、1024チャンクを1つのブロックにグループ化します。各ブロックは開始ドキュメント値を記録し、ブロックはレベルジャンプテーブルに相当します。
したがって、ドキュメントを見つけるには、3つのステップがあります。
最初のステップは、2つの方法でブロックを見つけ、それが属するブロックを見つけることです。
2番目のステップは、ブロック内の各チャンクの開始ドキュメント番号に従って、それが属するチャンクとチャンクの位置を見つけることです。
3番目のステップは、fdtのチャンクをロードし、ドキュメントを見つけることです。ここでもう1つ詳細は、開始ドキュメント値とチャンクのチャンク位置の格納は単純な配列ではなく、平均的な圧縮方法であるということです。したがって、N番目のチャンクの開始ドキュメント値はDocBase + AvgChunkDocs * n + DocBaseDeltas [n]から復元され、fdt内のN番目のチャンクの位置はStartPointerBase + AvgChunkSize * n + StartPointerDeltas [n]から復元されます。
上記の分析から、Luceneは元のファイルをストレージとして保存し、スペース使用率を向上させるために、複数のドキュメントが一緒に圧縮されるため、ドキュメントをフェッチするときに追加のドキュメントを読み取って解凍する必要があることがわかります。ドキュメントのフェッチは非常にランダムです。IOとLuceneはフェッチ用の特定の列を提供しますが、ストレージ構造から、ドキュメントのフェッチにかかる時間を短縮しないことがわかります。
4.列指向ストレージDocValues
転置インデックスは、単語からドキュメントへの迅速なマッピングを解決できることはわかっていますが、検索結果に対して分類、並べ替え、数学計算などの集計操作を実行する必要がある場合は、ドキュメント番号から値への高速マッピングが必要です。転置インデックスであるか、行に格納されているドキュメントであるかは、要件を満たすことができません。
元のバージョン4.0より前は、LuceneはFieldCacheを介してこの要件を実現していました。その原則は、反転したテーブルを列ごとに逆にすることで、(フィールド値->ドキュメント)マッピングを(ドキュメント->フィールド値)マッピングに変更することですが、この実装方法には2つの重要な問題:
1。ビルド時間が長い。
2.メモリフットプリントが大きく、OutOfMemoryが発生しやすく、ガベージコレクションに影響します。
したがって、バージョン4.0以降、Luceneはこの問題を解決するためにDocValuesを導入しました。これはFieldCacheのような列型ストレージですが、次の利点があります
。1。ファイルを事前にビルドして書き込む。
2.マッピングファイルに基づいて、JVMヒープメモリから、システムスケジューリングページフォールト。
DocValuesの実装方法は、メモリFieldCacheよりも約10〜25%遅いだけですが、安定性が大幅に向上しています。
Luceneには現在、NUMERIC、BINARY、SORTED、SORTED_SET、SORTED_NUMERICの5種類のDocValueがあり、Luceneの種類ごとに特定の圧縮方法があります。
たとえば、NUMERICタイプ、つまり数値タイプの場合、インクリメント、テーブル圧縮、最大公約数など、数値タイプには多くの圧縮方法があります。データの特性に応じて、さまざまな圧縮方法が選択されます。 。
SORTED型は文字列型で、圧縮方法はテーブル圧縮です。文字列辞書は事前に並べ替えられてデジタルIDが割り当てられ、格納時に文字列マッピングテーブルと数値配列のみが格納されます。 NUMERICによって再度圧縮されます。以下に示すように圧縮されます。
このようにして、元の文字列配列が数値配列に変換されます。まず、スペースが削減され、ファイルマッピングがより効率的になります。次に、元のアクセス方法が固定長アクセスになります。
DocValuesを適用するために、ElasticSearch関数は、より体系的かつ完全に実装されます。つまり、ElasticSearchの集計-集計関数です。その集計関数は、次の3つのカテゴリに分類され
ます。1。メトリック->
一般的な統計 関数:合計、最小、最大、平均、カーディナリティ、パーセントなど
。2。バケット->
一般的な関数のグループ化 :日付ヒストグラム、グループ化、地理的位置のパーティション
3.パイプライン->集計および再集計に基づく
一般的な関数:各グループの平均に基づいて最大値を見つける。
これらの集計関数に基づいて、ElasticSearchは検索に限定されなくなり、次のSQLの質問に答えることができます
select gender,count(*),avg(age) from employee where dept='sales' group by gender
销售部门男女人数、平均年龄是多少
ElasticSearchが転置インデックスとDocValuesに基づいて上記のSQLをどのように実装するかを見てみましょう。
1.転置インデックスから営業部門の転置テーブルを見つけます。
2.逆表に従って、性別のDocValuesから各人の対応する性別を取り出し、女性と男性にグループ化します。
3.グループ化の状況と年齢DocValuesに従って、各グループの人数と平均年齢を計算します
。4。ElasticSearchはパーティション化されている ため、各パーティションの返された結果を組み合わせることで最終結果が得られます。
上記はElasticSearchの集約の全体的なプロセスです。ElasticSearchの集約のボトルネックは、集約の最後のステップが単一のマシンでしか集約できないことであるため、一部の統計にはcount(*)などのエラーが発生することもわかります。生産制限5でグループ化し、最終的な合計は正確ではありません。シングルポイントメモリ集約のため、各パーティションがすべてのグループ化統計を返すことは不可能であり、その一部のみを返すことは不可能であり、次のように要約すると最終結果は不正確になり
ます。
シャード1 | シャード2 | シャード3 |
---|---|---|
製品A(25) | 製品A(30) | 製品A(45) |
製品B(18) | 製品B(25) | 製品C(44) |
製品C(6) | 製品F(17) | 製品Z(36) |
製品D(3) | 製品Z(16) | 製品G(30) |
製品E(2) | 製品G(15) | 製品E(29) |
製品F(2) | 製品H(14) | 製品H(28) |
製品G(2) | 製品I(10) | 製品Q(2) |
製品H(2) | 製品Q(6) | 製品D(1) |
製品I(1) | 製品J(8) | |
製品J(1) | 製品C(4) |
count(*)group byproducet制限5。各ノードから返されるデータは次のとおりです。
シャード1 | シャード2 | シャード3 |
---|---|---|
製品A(25) | 製品A(30) | 製品A(45) |
製品B(18) | 製品B(25) | 製品C(44) |
製品C(6) | 製品F(17) | 製品Z(36) |
製品D(3) | 製品Z(16) | 製品G(30) |
製品E(2) | 製品G(15) | 製品E(29) |
合併後:
合併 |
---|
製品A(100) |
製品Z(52) |
製品C(50) |
製品G(45) |
製品B(43) |
製品Aの総数はすべてのノードが戻ってきたので正しいですが、製品Cは上位5にないため、ノード2では返されないため、総数は間違っています。
総括する
上記は、Luceneの紹介と、Luceneの実装戦略と特性に焦点を当てた基本原則の分析です。次の記事では、これらの基本原則からフルテキスト検索システムを最適化する方法を紹介します。
著作権表示:この記事はブロガーのオリジナル記事であり、ブロガーの許可なしに複製することはできません。https://blog.csdn.net/njpjsoftdev/article/details/54015485