I. はじめにこの記事では、私の実務経験に基づいて、ElasticSearch の使い方に関する提案をいくつかご紹介します。他の記事は結論ばかりに重点を置きがちですが、この記事では提案の背後にある原理を説明し、使用例も紹介することで、「何」だけ知っていて「なぜ」を理解できないような表面的な理解を避けます。ご意見・ご指摘があれば、ぜひお聞かせください。 II. クエリ関連情報キャッシュを最大限に活用する
Elasticsearch (ES) レイヤーのキャッシュ実装は、`IndicesRequestCache` クラスにカプセル化されています。キャッシュキーはクライアントリクエスト全体であり、キャッシュされたコンテンツは単一のシャードのクエリ結果です。主な機能は集計をキャッシュすることです。クエリ結果のキャッシュされたコンテンツには、主に集計、Hits.total、Suggestions などが含まれます。 シャードレベルのクエリはすべてキャッシュされるわけではありません。クライアントリクエストのサイズが0のクエリのみがキャッシュされます。キャッシュが妨げられるその他の条件としては、スクロール、Profile属性の設定、QUERY_THEN_FETCH以外のクエリタイプ、requestCache=falseの設定などがあります。さらに、「Now」を含む範囲クエリ(ミリ秒単位のためキャッシュは無意味)など、不確定要素を含むクエリや、スクリプトクエリでMath.random()などの関数を使用するクエリもキャッシュされません。 新しいセグメントがシャードに書き込まれると、以前にキャッシュされた結果ではシャード全体のクエリ結果を表現できなくなるため、キャッシュは無効になります。そのため、シャードの更新ごとにキャッシュはクリアされます。
Luceneレベルのキャッシュ実装はLRUQueryCacheクラスにカプセル化されており、デフォルトで有効になっています。このクラスは、セグメントのフィルターのサブクエリステートメントのクエリ結果をキャッシュします。 すべてのフィルタークエリがキャッシュされるわけではありません。小さなセグメントはすぐにマージされるため、クエリキャッシュは作成されません。セグメントがキャッシュされるには、10,000件以上のドキュメントを含み、かつシャード全体の3%以上を占めている必要があります(「キャッシュ」を参照)。 セグメントが結合されると、削除されたセグメントに関連付けられたキャッシュは無効になります。 01. クエリ コンテキストの代わりにフィルター コンテキストを使用します。
肯定的な例: 反例: 02. ドキュメントの詳細ではなく集計結果のみに焦点を当てる場合は、サイズを 0 に設定すると、シャードされたクエリ キャッシュが使用されます。参考例: 03. 日付範囲クエリでは絶対時刻値が使用されます。日付フィールドで「Now」を使用すると、一致する時刻は常に変化するため、通常はキャッシュされません。したがって、ビジネスの観点から「Now」が本当に必要かどうかを検討してください。可能な限り絶対時刻値を使用することで、相対時刻表現の解析を回避し、クエリキャッシュを活用してクエリ効率を向上させることができます。例えば、時間範囲クエリで「Now/h」を使用し、時間単位を指定すると、キャッシュされたデータに1時間以内にアクセスできます。 肯定的な例: 集計クエリ04. 複数レベルのネストされた集計クエリを避けます。集計クエリの中間結果と最終結果はメモリに保存されます。ネストが多すぎるとメモリ不足につながる可能性があります。 のように: 05. ネストされたクエリの場合は、複合集計クエリ方式を使用することをお勧めします。A、B、Cのような一般的な多次元Groupbyクエリでは、ネストされた集計はパフォーマンスが低下します。ネストされた集計は各バケット内のメトリクスを計算するように設計されているため、フラットなGroupbyでは多くの冗長な計算が発生します。さらに、Metaフィールドのシリアライズとデシリアライズのコストも非常に高くなります。このタイプのGroupbyをCompositeに置き換えることで、クエリ速度を約2倍向上させることができます。 肯定的な例: 反例: 06. 大規模な集計クエリを避ける。集計クエリの中間結果と最終結果は両方ともメモリ内で処理されるため、大量のデータがあるとメモリ不足につながる可能性があります。 07. 高カーディナリティのシナリオでのネストされた集計クエリでは、BFS 検索を使用することをお勧めします。集計はElasticsearchメモリ内で実行されます。集計操作にネストされた集計操作が含まれる場合、各ネストされた集計操作は、前のレベルの集計操作で構築されたバケットを入力として使用し、それぞれの集計条件に従ってバケットをさらにグループ化します。これにより、ネストのレベルごとに新しい集計バケットセットが動的に構築されます。高カーディナリティのシナリオでは、ネストされた集計操作により、ネストレベルに応じて集計バケットの数が指数関数的に増加し、最終的にはElasticsearchメモリを大量に消費し、OutOfMemoryError(OOM)が発生します。 Elasticsearchはデフォルトで深さ優先探索(DFS)を使用します。DFSはまず完全なツリーを構築し、不要なノードを削除します。幅優先探索(BFS)はまず最初のレベルの集約を実行し、その後、次のレベルの集約に進む前に不要なノードを削除します。 集計クエリにおいて、幅優先探索アルゴリズムを使用するには、各バケットレベルでドキュメントデータをキャッシュし、プルーニングフェーズ後にこれらのドキュメントをサブ集計に再読み込みする必要があります。そのため、幅優先探索アルゴリズムのメモリ消費量は、各バケット内のドキュメント数に依存します。多くの集計クエリでは、各バケット内のドキュメント数が非常に多く、集計には数千から数十万のドキュメントが含まれる場合があります。 しかし、バケット数は多いものの、バケットあたりのドキュメント数が比較的少ない場合、幅優先探索はメモリリソースをより効率的に利用し、より複雑な集計クエリを構築できます。バケット数は多くても、バケットあたりのドキュメント数が比較的少ないため、幅優先探索はメモリ効率に優れています。 参考例: 08. テキスト フィールド タイプでは集計クエリを使用しないでください。
09. 集約されたディープ ページネーション クエリに bucket_sort を使用することはお勧めしません。Elasticsearch の高カーディナリティ集約クエリはメモリを大量に消費します。100 万カーディナリティを超える集約は、ノード メモリ不足や OutOfMemoryError (OOM) につながる可能性が高くなります。 bucket_sortはバケットソートアルゴリズムを使用します。このアルゴリズムのパフォーマンス上の問題は、ソートとページ区切りを実行する前に、すべてのドキュメントをキャッシュし、バケットをメモリに集約する必要があることに主に起因します。ドキュメント数とページ区切りの深さが増加すると、深いページ区切りの問題によりパフォーマンスが徐々に低下します。バケットソートはすべてのドキュメントを完全にソートする必要があるため、その時間計算量はO(NlogN)です(Nはドキュメントの総数)。 現在、Elasticsearchはページ区切りの集計において、複合集計(スクロール集計)のみをサポートしています。スクロールの仕組みはSearchAfterに似ています。集計時に複合キーが指定され、各シャードはこのキーに従ってソートされ、集計されます。すべてのドキュメントとバケットをメモリにキャッシュする必要はなく、一度に1ページのデータを返すことができます。 反例: ディープ ページネーションに bucket_sort を使用すると、応答時間 (RT) が 5000 ミリ秒以上になりました。 良い例: 複合集約を使用して最適化されたディープページネーションクエリ: 423 ミリ秒 ページネーション10. from+size メソッドの使用は避けてください。Elasticsearchでは、ディープページネーションのコストはページネーションの深度に応じて指数関数的に増加し、ページネーションされた検索は個別にキャッシュされません。各ページネーションリクエストは、最初の検索から結果を取得するのではなく、新たな検索プロセスとして実行されます。データが非常に大きい場合、CPUとメモリの消費量が膨大になり、OutOfMemoryError(OOM)が発生する可能性があります。 11. リアルタイム要件が高く、結果セットが大きいシナリオでは、Scroll メソッドを使用しないでください。スナップショットベースのコンテキスト。リアルタイムアプリケーションには推奨されません。結果セットが大きい場合、多数のスクロールコンテキストが生成され、メモリ消費が過剰になる可能性があります。そのため、SnapshotAfterメソッドの使用をお勧めします。 考えてみましょう:ScrollとSearchAfterのどちらを選ぶべきでしょうか?それぞれどのようなシナリオに適していますか?SearchAfterはScrollを完全に置き換えることができますか? Scroll は現在のインデックスセグメントのスナップショットを保持します。これは、フルデータクエリの非リアルタイムスクロールに適していますが、多数のコンテキストによるヒープメモリ消費量の増大が顕著です。バージョン7.10では、新機能の Search After + PIT が導入されました。この機能では、クエリは基本的に前のページのソート済みリストを使用して次のページを照合するため、データの一貫性が確保されます。バージョン8.10の公式ドキュメントでは、Scroll API を使用したディープページネーションは推奨されなくなったことが明記されています。ページネーションが上位10,000件を超える場合は、PIT + Search After が推奨されます。 12. インデックス内の SearchAfter ページネーション/スクロール ID/データ トラバーサルでは、ソート フィールドが一意である必要があることを指定します。そうでない場合、ページネーション/トラバーサル データが不完全または重複することになります。13. デフォルトのスコアリング ソートを使用する代わりに、ビジネス フィールドのソートを指定することをお勧めします。Elasticsearch (ES) は、デフォルトで「_score」フィールドを使用してスコアでソートします。Scroll API を使用してデータを取得する際、特別なソート要件がない場合は、「sort":"_doc"」を使用してヒットしたドキュメントをインデックス順に返すことをお勧めします。これにより、ソートのオーバーヘッドを削減できます。その理由は次のとおりです。
14. スクロール ルックアップでは、clearScroll() メソッドを明示的に呼び出すことによって、スクロール ID がクリアされることが保証されます。そうしないと、Elasticsearch は有効期限前にスクロール結果セットが占有しているメモリリソースを解放できず、デフォルトの 3000 個のスクロール クエリの容量も占有してしまい、スクロール ID が多すぎるためにクエリ拒否エラーが発生し、業務に影響を及ぼします。 他の15. Must と Should が同じステートメント内に表示される場合、Should は無効になることに注意してください。また、Must と Should が同じレベルの同じブールクエリ内に表示される場合、Should クエリは無効になることに注意してください。肯定的な例: 反例: Elasticsearch のインデックス名はグローバルに表示されるため、すべてのインデックスをクエリすることでクラスター内のすべてのインデックス名を列挙できます。Elasticsearch 設定ファイルで `action.destructive_requires_name` パラメータを設定することで、indexName-* のクエリを無効にすることができます。 17. インライン スクリプトの代わりに保存されたスクリプトを使用します。固定構造のスクリプトの場合、Stored メソッドを使用して Kibana 経由で Elasticsearch クラスターにスクリプトを保存し、スクリプトを繰り返しコンパイルすることによって発生するパフォーマンスの低下を軽減します。 肯定的な例: 反例: 18. _all フィールドの使用は避けてください。`_all` フィールドには、インデックス付けされたすべてのフィールドが含まれます。元のドキュメントデータを取得する必要がない場合は、`Includes` 属性と `Excludes` 属性を設定することで、`_source` に含めるフィールドを定義できます。デフォルトでは、`_all` は書き込まれたフィールドを大きな文字列に連結し、そのフィールドに対してトークン化を実行して、ドキュメント全体の全文検索をサポートします。`_all` フィールドは、クエリ中に CPU とディスクストレージの消費量を増加させます。デフォルトでは「false」に設定されており、このフィールドを有効にしたり使用したりすることは推奨されません。 19. 検索クエリを GET クエリに置き換えることをお勧めします。GET/MGETは、ドキュメントIDに基づいて前方インデックスから直接コンテンツを取得します。_idを指定せずに検索を実行すると、キーワードに基づいて転置インデックスからコンテンツが取得されます。 20. 複数のインデックス クエリを実行しないでください。反例: 21. 一度に大量のデータを呼び出さないようにするには、_source_includes および _source_excludes パラメータを使用してフィールドを含めたり除外したりすることをお勧めします。これは、部分的なフィールド取得によってネットワークのオーバーヘッドを節約できるため、大規模なドキュメントの場合に特に便利です。 参考例: 22. 中置あいまい検索ではワイルドカードを使用しないでください。Elasticsearchの公式ドキュメントでは、中置曖昧クエリにワイルドカードを使用することは推奨されていません。これは、Elasticsearchが入力文字列パターンから決定性有限オートマトン(DFA)を内部的に構築し、ワイルドカードを使ったクエリを高速化するためです。ワイルドカードパターンから構築されるDFAは非常に複雑で、コストが高くなる可能性があります。 ES 7.9で正式に導入されたワイルドカードフィールドタイプの使用をお勧めします。これは、あいまい検索の速度低下に対処するためです。テキストフィールドと比較すると、ワイルドカードフィールドはテキストを句読点で区切られた単語の集合として扱いません。また、キーワードフィールドと比較すると、インフィックス検索シナリオにおいて比類のないクエリ速度を実現し、入力サイズ制限もありません。これはキーワードタイプでは実現できないものです。 23. スクリプトの使用は避けてください。Painlessスクリプト言語は、比較的シンプルな構文、高い柔軟性、高いセキュリティ、そして高いパフォーマンス(他のスクリプト言語と比較して、DSLよりは劣る)を備えています。複雑でないビジネスロジックには適していません。DSLは一般的にほとんどの問題を解決でき、Painlessのようなスクリプト言語はそれらを処理できます。主なパフォーマンスへの影響は次のとおりです。単一のクエリまたは更新にかかる時間の増加。スクリプト実行前に字句解析、構文解析、コードコンパイルなどの前処理作業が必要となるため、スクリプトの実行時間は他のクエリおよび更新操作よりも長くなる可能性があります。 24. 動的フィールドを計算するためにスクリプトクエリを使用することは避けてください。インデックス作成時にフィールドを計算し、ドキュメントに追加することをお勧めします。例えば、大量のユーザー情報を含むインデックスがあり、名前が「1234」で始まるすべてのユーザーをクエリする必要がある場合、「source":"doc['num'].value.startsWith('1234')"」のようなクエリを実行するスクリプトを実行すると、リソースを大量に消費します。インデックスを作成する際は、キーワードフィールド「num_prefix」を追加し、「name_prefix":"1234"」のようにクエリすることを検討してください。 III. 関連情報の記述25. コード内で直接、または手動で更新操作を実行しないでください。システムが更新アクションを実行できるように、インデックス設定/Refresh_Interval を適切に設定します。 26. 個々のドキュメントが大きくなりすぎないようにします。デフォルトの http.max_content_length が 100 MB に設定されている場合、Elasticsearch はその値より大きいドキュメントのインデックス作成を拒否します。 27. データを書き込むときに Doc_ID を指定しないでください。Elasticsearch によって自動的に生成されます。インデックスに明示的なIDを持つドキュメントが含まれている場合、Elasticsearchは書き込みプロセス中に、同じシャード内に同じIDを持つドキュメントが既に存在するかどうかをチェックする追加ステップを実行します。この処理は、インデックスが大きくなるにつれてコストが増加します。 28. バッチ書き込みには Bulk API を活用します。バルクは大量のデータ書き込みに使用できますが、応答時間が長くなり、接続が切断されてもElasticsearchクラスターは実行を継続します。高速で大量のデータ書き込みを行うと、クラスターの動作が遅くなったり、一時的にフリーズしたように見える場合があります。
29. スクリプトが大量のデータを書き込む場合、書き込み前にレプリカシャードを0に設定することは推奨されません。書き込みが完了したら、レプリカシャードを0に戻してください。レプリカシャードをノードに再追加すると、セカンダリシャードのリカバリプロセスがトリガーされます。シャードのサイズが大きい場合、クラスターのパフォーマンスに影響します。 IV. インデックスの作成断片30. レプリカ シャードの数は 1 以上です。高可用性が保証されます。レプリカ数を増やすと検索パフォーマンスはある程度向上しますが、書き込みパフォーマンスは低下します。プライマリシャード1つにつき、1~2個のレプリカシャードを割り当てることをお勧めします。 31. 公式の推奨事項では、シャードあたりのデータエントリの最大数を 2^32 - 1 以下に制限します。32. インデックスのプライマリ シャードの数をあまり大きく設定しないでください。Elasticsearch でインデックスが作成されると、プライマリ シャードの数は通常は動的に調整されません。 各シャードは本質的に Lucene インデックスであるため、対応するファイル ハンドル、メモリ、および CPU リソースを消費します。 Elasticsearchは関連性を計算するために単語の頻度統計を使用します。もちろん、これらの統計は複数のシャードに分散されます。少量のデータが多数のシャードに分散されている場合、最終的なドキュメントの関連性は低くなります。 一般的に言えば、私たちはいくつかの原則に従います:
33. 単一のデータフラグメントのサイズは 50 GB を超えてはなりません。単一のインデックスのサイズは1TB未満、単一のシャードのサイズは30~50GB、ドキュメントの数は10億未満に抑える必要があります。これらの制限を超える場合は、ロールオーバーをお勧めします。 マッピングデザイン 34. フィールドの動的マッピング機能は使用しないでください。特定のフィールドタイプ、サブタイプ(必要な場合)、トークナイザー(特に特定のシナリオで必要な場合)を指定してください。35. トークン化を必要としない文字列フィールドの場合は、テキスト タイプではなくキーワード タイプを使用します。36. Elasticsearch のフィールドのデフォルトの最大数は 1000 です。100 を超えないようにすることをお勧めします。単一ドキュメントのインデックス作成における計算の複雑さは、ドキュメントのバイトサイズや特定のフィールド値の長さではなく、主にフィールド数によって決まります。例えば、同じマッピングを使用したフルロード書き込みストレステストでは、フィールド数が10でサイズが200バイトのドキュメントの場合、一部のフィールド値の長さを500バイトに増やしても、Elasticsearchへの書き込み速度はわずかに低下するだけです。しかし、フィールド数が20に増えると、ドキュメント全体のバイトサイズが大幅に増加しなくても、書き込み速度は半分になります。 37. インデックスが作成されていないフィールドの場合、Index プロパティは False に設定されます。以下の例では、「Title」フィールドの「Index」プロパティがFalseに設定されており、このフィールドはインデックスに含まれないことを示しています。一方、「Content」フィールドの「Index」プロパティはデフォルトでTrueに設定されており、このフィールドはインデックスに含まれることを意味します。なお、IndexプロパティがFalseに設定されている場合でも、フィールドはドキュメントに保存され、クエリや集計が可能です。 参考例: 38. ネストや親/子の使用は避けてください。ネストされたクエリは遅く、親子クエリはさらに遅くなります。単一のドキュメントに対して、ネストされた各フィールドは個別のドキュメントを生成するため、ドキュメント数が大幅に増加し、クエリ効率、特にJOIN効率に影響を与えます。したがって、マッピング設計段階では、可能な限り親子マッピングの使用を避けてください(幅の広いテーブル設計やよりスマートなデータ構造を使用)。ネストされたフィールドを使用する必要がある場合は、過度にならないようにしてください。Elasticsearchのデフォルトの制限は `Index.mapping.nested_fields.limit=50` です。ネストされたクエリは一般的に推奨されません。では、ElasticsearchがJOINを実行できない問題にどのように対処すればよいでしょうか?主な実装方法はいくつかあります。
39. 規範の使用を避けます。Normはインデックスのスコアリング係数です。ドキュメントをスコアで並べ替えたくない場合は、「False」に設定してください。 参考例: テキスト タイプのフィールドでは、デフォルトで規範が有効になっていますが、キーワード タイプのフィールドでは、デフォルトで規範が無効になっています。 ノルムを有効にすると、各ドキュメントの各フィールドにノルムを格納するために1バイトが必要になります。テキスト型フィールドではノルムがデフォルトで有効になっているため、スコアリングを必要としないテキスト型フィールドでは無効にすることができます。 40. 集計/並べ替えを必要としないフィールドの列ストアDoc_Valuesを無効にします。この列指向のストレージ方式は、主に並べ替え、集計、スクリプトからのフィールド値へのアクセスといったデータアクセスシナリオで使用されます。分析が必要な文字列フィールドを除き、ほぼすべてのフィールドタイプがDoc_Valuesをサポートしています。この機能は、Doc_Valuesをサポートするすべてのフィールドでデフォルトで有効になっています。フィールドの並べ替えや集計、あるいはスクリプトからのフィールド値へのアクセスが不要と判断した場合は、この機能を無効にして冗長なストレージコストを削減できます。 キーワードと数値の選択キーワード型の主な欠点は、集計時にグローバル序数を構築する必要があることです。一方、数値型ではそうする必要はありません。しかし、カーディナリティの低い数値フィールドは、性別など、多数の結果セットにヒットすることが多く、Numeric型を使用すると、ビットセットの構築に高いコストがかかります。 要約すると、タイプを選択する際には次の原則を参照できます。
41. 範囲クエリであまり使用されない数値の場合は、キーワード型を使用します。すべての数値データを数値フィールドのデータ型にマッピングする必要はありません。Elasticsearchは、Integerやlongなどの数値フィールドをクエリに最適化します。範囲検索が不要な場合、TermクエリではIntegerよりもKeywordの方がパフォーマンスが向上します。 42. 頻繁に使用され、比較的固定されている範囲クエリ フィールドの場合は、キーワード タイプの事前インデックス フィールドを追加します。フィールドに対するほとんどのクエリが固定範囲内で範囲集計を実行する場合は、キーワード フィールドを追加して、範囲をインデックスに「事前インデックス付け」し、用語集計を使用することで、集計を高速化できます。 43. 集計クエリを必要とする高カーディナリティ キーワード フィールドに対して Eager_Global_Ordinals を有効にします。序数(ordinals)は、キーワードフィールドで用語の集計を行うために使用されます。序数は自動増分する数値で表されます。Elasticsearchは、この自動増分する数値と実際の値とのマッピングを維持し、各値にバケットを割り当てます。このマッピングはセグメントレベルで行われます。 しかし、集計操作を実行する際には、複数のセグメントの結果を結合する必要があることがよくあります。各セグメントの序数マッピング関係は一貫していないため、ESは各シャードにグローバル序数構造(グローバルに統一されたマッピング)を作成し、グローバル序数と各セグメントの序数間のマッピング関係を維持します。 デフォルトでは、グローバル序数はクエリが最初に使用する場合(例えばTerm Aggregationなど)のみ、遅延して構築されます。これは、ElasticsearchがTerm Aggregationに使用されるフィールドと使用されないフィールドを認識できないためです。カーディナリティの高いフィールドの場合、構築コストは大きくなります。 `eagerly_global_ordinals` を有効にすると、Elasticsearch はシャード構築時にグローバル序数テーブルを事前計算できるため、クエリ時の読み込みと使用が高速化されます。ただし、`eagerly_global_ordinals` を有効にすると、更新操作ごとにグローバル序数テーブルも構築されるため、検索時に発生する構築コストが書き込み操作にシフトされます。そのため、書き込み効率に影響を与える可能性があります。この機能は、インデックスの更新間隔を長くすることと併用することをお勧めします。 参考例: V. 要約最近十年,Elasticsearch 已经成为了最受欢迎的开源检索引擎,并沉淀了大量的实践案例及优化总结。在本文中,我们尽可能全面地总结了Elasticsearch 日常开发中的一些重要实践&避坑指南,希望能为大家提供Elasticsearch 使用上的一些借鉴点,欢迎讨论! 参考文献: 1.《Elasticsearch 源码解析与优化实战》 2.《Elasticsearch权威指南》 3.https://www.easyice.cn/archives/367 4.https://www.elastic.co/guide/en/elasticsearch/guide/current/filter-caching.html#_independent_query_caching 5.https://www.elastic.co/guide/cn/elasticsearch/guide/current/_preventing_combinatorial_explosions.html 6.https://www.elastic.co/guide/en/elasticsearch/reference/current/eager-global-ordinals.html |