Elasticsearchの遅いクエリを最適化する:パフォーマンスチューニングのベストプラクティス

クエリ形状、ページネーション、キャッシュ、マッピング、プロファイルAPIを用いて、Elasticsearchの遅いクエリを診断・改善します。

Elasticsearchの遅いクエリを最適化する:パフォーマンスチューニングのベストプラクティス

Elasticsearchの遅いクエリは、通常、以下の4つの原因のいずれかに起因します。クエリが要求しすぎている、マッピングがクエリを高コストにしている、クラスタのリソースが不足している、またはアプリケーションがキャッシュや再設計すべき高コストな検索を繰り返している。修正方法は、どの原因に該当するかによって異なります。

すべてを書き換える前に、実際の遅いリクエストを、そのインデックス、フィルター、ソート、集計、ページの深さ、応答サイズ、タイミングとともにキャプチャしてください。ダッシュボード集計、オートコンプリートクエリ、エクスポートジョブは、それぞれ異なる方法でElasticsearchに負荷をかけます。

クエリパフォーマンスのボトルネックを理解する

解決策に飛び込む前に、Elasticsearchのクエリが遅くなる一般的な理由を理解しておくと役立ちます。これらには以下のものが含まれます。

  • 複雑なクエリ: 複数のbool句、ネストされたクエリ、または大規模データセットに対するwildcardregexpのような高コストな操作を含むクエリ。
  • 非効率なデータ取得: 不必要に_sourceを取得したり、ページネーションのために大量のドキュメントを取得したりする。
  • リソース制約: データノードのCPU、メモリ、またはディスクI/Oの不足。
  • 最適でないマッピング: 不適切なデータ型の使用、または集計にdoc_valuesを活用していない。
  • シャードの不均衡または過負荷: シャードが多すぎる、少なすぎる、またはシャード/データの分散が不均一。
  • キャッシュミスまたはキャッシュの適合不良: リクエストキャッシュ、フィルターコンテキスト、または適切なアプリケーションレベルのキャッシュを使用せずに、高コストな検索を繰り返す。

クエリ構造の最適化

クエリの構築方法は、パフォーマンスに大きな影響を与えます。小さな変更でも大幅な改善につながる可能性があります。

1. 必要なフィールドのみを取得する(_sourceフィルタリングとstored_fields

デフォルトでは、Elasticsearchは一致する各ドキュメントの_sourceフィールド全体を返します。ドキュメントが大きく、UIにタイトル、ID、タイムスタンプのみが必要な場合、ドキュメント全体を取得するとネットワーク帯域幅と解析時間を浪費します。

  • _sourceフィルタリング: _sourceパラメータを使用して、含めるか除外するフィールドの配列を指定します。

    GET /my-index/_search
    {
      "_source": ["title", "author", "publish_date"],
      "query": {
        "match": {
          "content": "Elasticsearch performance"
        }
      }
    }
    
  • stored_fields: マッピングで特定のフィールドを明示的に保存している場合("store": true)、stored_fieldsでそれらを取得できます。ほとんどのデプロイメントではこの方法で多くのフィールドを保存しないため、_sourceフィルタリングの方が一般的な修正方法です。

    GET /my-index/_search
    {
      "stored_fields": ["title", "author"],
      "query": {
        "match": {
          "content": "Elasticsearch performance"
        }
      }
    }
    

2. 効率的なクエリタイプを優先する

クエリタイプによっては、本質的に他のものよりリソースを消費するものがあります。

  • 先頭ワイルドカードと広範な正規表現を避ける: wildcardregexpクエリは、特に*testのような先頭ワイルドカードがある場合、高コストになる可能性があります。プレフィックスクエリは通常、先頭ワイルドカード検索よりも管理しやすいですが、それでも適切なマッピングと制限された入力が必要です。

    # 非効率 - 先頭ワイルドカードを避ける
    {
      "query": {
        "wildcard": {
          "name.keyword": {
            "value": "*search"
          }
        }
      }
    }
    
    # より良い - プレフィックスがわかっている場合
    {
      "query": {
        "prefix": {
          "name.keyword": {
            "value": "Elastic"
          }
        }
      }
    }
    
  • フレーズ意図にはmatch_phraseを使用する: ユーザーが正確なフレーズを検索している場合、match_phraseは複数の無関係なmatch句よりもその意図を適切に表現します。常に安価であるとは限りませんが、単語が離れて含まれるドキュメントを返すことを避けます。

  • はい/いいえ条件にはフィルターコンテキスト: ドキュメントが条件に一致するかどうかだけを気にする場合は、その条件をfilterコンテキストに入れるか、constant_scoreを使用します。これにより、不要なスコアリング作業を回避し、キャッシュにより適したものになります。

    GET /my-index/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "term": {
              "status": "active"
            }
          }
        }
      }
    }
    

3. ブールクエリの最適化

  • 構造化制約にはフィルターを使用する: テナントID、ステータス値、日付範囲、正確なタグは、スコアリングが必要でない限り、mustではなくfilterに入れてください。Elasticsearchは内部的に句を並べ替えて最適化できるため、JSONの順序を主要なパフォーマンスツールとして依存しないでください。
  • minimum_should_matchを意図的に使用する: 関連性を向上させ、広範な一致を減らすことができますが、高く設定しすぎると有効な結果が隠れる可能性があります。

4. 効率的なページネーション(search_afterscroll

従来のfrom/sizeページネーションは、深いページ(例:from: 10000size: 10)では非常に非効率になります。Elasticsearchは各シャードでfrom + sizeまでのすべてのドキュメントを取得してソートし、その後from個のドキュメントを破棄する必要があります。

  • search_after: リアルタイムの深いページネーションには、search_afterが推奨されます。これは、従来のデータベースのカーソルと同様に、前のページの最後のドキュメントのソート順を使用して次の結果セットを見つけます。ステートレスで、スケーラビリティに優れています。

    # 最初のリクエスト
    GET /my-index/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
    # 最初のリクエストの最後のドキュメントのソート値を使用した後続のリクエスト
    GET /my-index/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "search_after": [1678886400000, "doc_id_XYZ"],
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
  • scroll API: 再インデックスやエクスポートなど、大規模データセットの一括取得には、scrollが依然として有用です。新しいElasticsearchバージョンや長時間実行される全インデックススキャンには、ポイントインタイムとsearch_afterの組み合わせも検討してください。Scrollはユーザー向けのリアルタイムページネーションには適していません。

5. 集計の最適化

集計は、特に高カーディナリティフィールドではリソースを消費する可能性があります。

  • 集計の事前計算: 複雑でリアルタイムではない集計は、インデックス作成時またはスケジュールに従って実行し、結果を事前計算して別のインデックスに保存することを検討してください。
  • doc_values: 集計で使用されるフィールドでdoc_valuesが有効になっていることを確認してください(これはほとんどの非テキストフィールドでデフォルトです)。これにより、Elasticsearchは_sourceをロードせずに効率的に集計用のデータをロードできます。
  • eager_global_ordinals: terms集計で頻繁に使用されるkeywordフィールドの場合、マッピングでeager_global_ordinals: trueを設定すると、グローバルオーディナルを事前構築することでパフォーマンスが向上します。これによりインデックスリフレッシュ時にコストが発生しますが、クエリ時の集計が高速化されます。

キャッシュ技術の活用

Elasticsearchは、繰り返しのクエリを大幅に高速化できるいくつかのキャッシュ層を提供しています。

1. ノードクエリキャッシュ

  • メカニズム: boolクエリ内で頻繁に使用されるフィルター句の結果をキャッシュします。ノードレベルのメモリ内キャッシュです。
  • 効果: 繰り返されるフィルター句に最も効果的です。すべてのクエリでこれを期待しないでください。Elasticsearchがキャッシュする価値があるものを決定します。
  • 設定: デフォルトで有効です。サイズはindices.queries.cache.size(デフォルトはヒープの10%)で制御できます。

2. シャードリクエストキャッシュ

  • メカニズム: シャードレベルの検索結果をキャッシュします。最も一般的には、size=0の集計が多いリクエストに使用されます。毎秒変化しないデータに対する繰り返しのダッシュボードクエリに適しています。

  • 効果: 同じリクエスト(集計を含む)が同一パラメータで繰り返し実行されるダッシュボードクエリや分析アプリケーションに最適です。

  • 使用方法: クエリで"request_cache": trueを使用して明示的に有効にします。

    GET /my-index/_search?request_cache=true
    {
      "size": 0,
      "query": {
        "bool": {
          "filter": [
            {"term": {"status.keyword": "active"}},
            {"range": {"timestamp": {"gte": "now-1h"}}}
          ]
        }
      },
      "aggs": {
        "messages_per_minute": {
          "date_histogram": {
            "field": "timestamp",
            "fixed_interval": "1m"
          }
        }
      }
    }
    
  • 注意点: シャードがリフレッシュされる(新しいドキュメントがインデックスされるか、既存のものが更新される)たびにキャッシュは無効になります。頻繁に同一の結果を返すクエリにのみ有用です。

3. ファイルシステムキャッシュ(OSレベル)

  • メカニズム: オペレーティングシステムのファイルシステムキャッシュは重要な役割を果たします。Elasticsearchは、頻繁にアクセスされるインデックスセグメントをキャッシュするためにこれに大きく依存しています。
  • 効果: クエリパフォーマンスにとって重要です。インデックスセグメントがRAMにある場合、ディスクI/Oが完全にバイパスされ、クエリ実行がはるかに高速になります。
  • ベストプラクティス: ファイルシステムキャッシュに十分なRAMを残してください。一般的な出発点は、JVMヒープをシステムメモリの約半分に保ち、通常のElasticsearchヒープ制限を考慮し、ワークロードで検証することです。

4. アプリケーションレベルのキャッシュ

  • メカニズム: 頻繁にリクエストされる検索結果に対して、アプリケーション層にキャッシュ(Redis、Memcached、またはインメモリキャッシュを使用)を実装します。
  • 効果: 繰り返しのリクエストに対してElasticsearchを完全にバイパスすることで、最速の応答時間を提供できます。静的またはゆっくり変化する検索結果に最適です。
  • 考慮事項: キャッシュ無効化戦略が鍵です。データの一貫性を確保するために慎重な設計が必要です。

ボトルネック特定のためのプロファイルAPIの使用

Profile APIは、Elasticsearchがクエリをどのように実行し、どこに時間が費やされているかを正確に理解するための貴重なツールです。クエリと集計の各コンポーネントの実行時間を分解します。

プロファイルAPIの使用方法

検索リクエスト本文に"profile": trueを追加するだけです。

GET /my-index/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Elasticsearch"}},
        {"term": {"status.keyword": "published"}}
      ],
      "filter": [
        {"range": {"publish_date": {"gte": "2023-01-01"}}}
      ]
    }
  },
  "aggs": {
    "top_authors": {
      "terms": {
        "field": "author.keyword",
        "size": 10
      }
    }
  }
}

プロファイルAPI結果の解釈

応答には、各シャードでのクエリと集計の実行を詳細に示すprofileセクションが含まれます。注目すべき主要なメトリクスは次のとおりです。

  • description: 特定のクエリまたは集計コンポーネント。
  • time_in_nanos: このコンポーネントの実行に費やされた時間。
  • breakdown: クエリのbuild_scorer_timecollect_timeset_weight_time、集計のreduce_timeなどの詳細なサブメトリクス。
  • children: ネストされたコンポーネント。複雑なクエリ内での時間の分布を示します。

解釈例:

WildcardQuerytime_in_nanosが高い場合、それがクエリの高コストな部分であることが確認されます。collect_timeが高い場合、一致後のドキュメントの取得と処理がボトルネックであることを示唆しており、_sourceの解析や深いページネーションが原因の可能性があります。集計のreduce_timeが高い場合、最終的なマージフェーズでの負荷が高いことを示している可能性があります。

これらのメトリクスを調べることで、最もリソースを消費している特定のクエリ句や集計フィールドを特定し、前述の最適化手法を適用できます。

パフォーマンスに関する一般的なベストプラクティス

クエリ固有の最適化に加えて、いくつかのクラスタ全体およびインデックスレベルのベストプラクティスが検索パフォーマンス全体に貢献します。

1. 最適なインデックスマッピング

  • text vs. keyword: 全文検索にはtextを、完全一致、ソート、集計にはkeywordを使用します。型の不一致は非効率なクエリにつながる可能性があります。
  • doc_values: ソートまたは集計する予定のフィールドでdoc_valuesが有効になっていることを確認してください。これらは、keyword、数値、日付、ブール値、IPフィールドなど、ソートと集計をサポートするほとんどのフィールドタイプでデフォルトで有効になっています。プレーンなtextフィールドは全文検索用です。完全一致や集計が必要な場合は、keywordサブフィールドを使用してください。
  • norms: ドキュメント長の正規化が不要なフィールド(例:IDフィールド)では、normsを無効にします("norms": false)。これによりディスク容量が節約され、インデックス速度が向上し、スコアリングを行わないクエリのパフォーマンスへの影響は最小限です。
  • index_options: textフィールドの場合、ドキュメント内に用語が存在するかどうかのみを知る必要がある場合はindex_options: docsを使用し、フレーズクエリや近接検索が必要な場合はindex_options: positions(デフォルト)を使用します。

2. クラスタの健全性とリソースの監視

  • クラスタステータス: Greenが目標です。Yellowは1つ以上のレプリカシャードが割り当てられていないことを意味します。検索は引き続き機能しますが、復元力が低下し、パフォーマンスが低下する可能性があります。Redはプライマリシャードが欠落しており、一部のデータが利用できないことを意味します。
  • リソース監視: データノードのCPU、RAM、ディスクI/O、ネットワーク使用量を定期的に監視してください。これらのメトリクスのスパイクは、多くの場合、遅いクエリと相関しています。
  • JVMヒープ: JVMヒープ使用量に注意してください。使用率が高いと、ガベージコレクションの一時停止が頻繁に発生し、クエリが遅くなる可能性があります。ヒープの負荷を減らすためにクエリを最適化してください。

3. 適切なシャード割り当て

  • シャードが多すぎる: 各シャードはリソースを消費します。多数の小さなシャードはオーバーヘッドを生み出します。数十ギガバイトのシャードが一般的ですが、適切なサイズはヒープ、クエリパターン、リカバリ目標、ハードウェアによって異なります。
  • シャードが少なすぎる: 並列処理が制限されます。シャードが少なすぎるインデックスに対するクエリは、利用可能なすべてのデータノードを効率的に活用できません。

4. インデックス戦略

  • リフレッシュ間隔: 低いrefresh_interval(デフォルト1秒)はデータをより早く表示可能にしますが、インデックスのオーバーヘッドが増加します。検索負荷の高いワークロードでは、リフレッシュの負荷を減らすために間隔を少し長く(例:5〜10秒)することを検討してください。

実用的なワークフローはシンプルです。実際の遅いクエリを見つけ、プロファイルし、それが触れるデータ量を減らし、ユーザーの検索方法に合わせてマッピングを調整します。クエリがすでにクリーンな場合は、シャードレイアウト、ヒープ負荷、ファイルシステムキャッシュ、ディスクI/Oを確認してください。Elasticsearchは、インデックス設計、クエリ形状、クラスタリソースが互いに一致しているときに高速です。