Elasticsearch Query DSL マスターガイド:データ検索のための必須コマンド

Elasticsearch の Query DSL をマスターして、データ検索の可能性を最大限に引き出しましょう。このガイドでは、`match`、`term`、範囲クエリの実用的な使い方を中心に、基本的な JSON クエリ構造を解説します。また、`bool` クエリ内の `must`(スコアリング)と `filter`(キャッシュ)句の重要な違いを学び、複雑で高性能なデータ検索を効率的に構築できるようになります。

Elasticsearch Query DSL マスターガイド:データ検索のための必須コマンド

Elasticsearch Query DSL は、単純な検索ボックスでは不十分な場合に使用する JSON 言語です。これにより、全文検索、正確なフィルター、日付範囲、並べ替え、ページネーション、集計を 1 つのリクエストで組み合わせることができます。この柔軟性は便利ですが、間違ったドキュメントを返したり、テストでは問題なく動作しても本番環境で遅くなったりするクエリを簡単に作成できてしまうという側面もあります。

Query DSL を学ぶ最善の方法は、「関連性のためにテキストを検索しているのか?」「正確な値をフィルタリングしているのか?」という 2 つの質問を常に念頭に置くことです。ほとんどのクエリの選択は、この区別に基づいています。

Elasticsearch 検索リクエストの構造

すべての Elasticsearch 検索は、特定のインデックス(または複数のインデックス)の _search エンドポイントに対して実行されます。基本的な検索リクエストは、クエリパラメータを定義する JSON 本文を含む POST リクエストです。この本文の中で最も重要な部分は query オブジェクトです。

基本構造:

POST /your_index_name/_search
{
  "query": { ... ここにクエリ構造を定義 ... },
  "size": 10, 
  "from": 0
}

コアクエリタイプ:精度と関連性

Query DSL は、さまざまなデータタイプとマッチングニーズに合わせて調整された多種多様なクエリを提供します。クエリの選択は、関連性スコアリングとパフォーマンスの両方に大きな影響を与えます。

1. 全文検索:match クエリ

match クエリは、分析済みフィールドに対する全文検索の標準です。検索語をトークン化し、指定されたフィールド内で一致するトークンをチェックします。

ユースケース: 関連性スコアリングが重要な自然言語テキストの検索。

例: 'description' フィールドに 'cloud' または 'computing' という単語を含むドキュメントを検索します。

GET /products/_search
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

2. 完全一致値のマッチング:term クエリ

term クエリは、指定された完全な語句を含むドキュメントを検索します。match とは異なり、検索文字列に対して分析を実行しないため、キーワード、ID、または数値でインデックスされたフィールドの完全一致に最適です。

ユースケース: 非分析フィールド(keyword フィールドや数値など)の正確な値によるフィルタリング。

例: 正確な ID SKU10021 を持つ製品を取得します。

GET /products/_search
{
  "query": {
    "term": {
      "product_id": "SKU10021"
    }
  }
}

3. 範囲クエリ

範囲クエリを使用すると、フィールドの値が指定された範囲(数値、日付、または文字列)内にあるドキュメントをフィルタリングできます。

構文: gt(より大きい)、gte(以上)、lt(より小さい)、lte(以下)を使用します。

例: 2024 年 1 月 1 日以降に発注された注文を検索します。

GET /orders/_search
{
  "query": {
    "range": {
      "order_date": {
        "gte": "2024-01-01",
        "lt": "2025-01-01"
      }
    }
  }
}

4. 存在によるフィルタリング:exists クエリ

exists クエリは、特定のフィールドが存在する(つまり、null または欠落していない)ドキュメントを識別します。

例: メールアドレスを提供しているすべてのユーザーを検索します。

GET /users/_search
{
  "query": {
    "exists": {
      "field": "email_address"
    }
  }
}

bool クエリによる複雑なロジックの構築

実質的にすべての実世界の検索アプリケーションでは、複数の条件を組み合わせる必要があります。bool クエリはそのための必須ツールであり、ブール論理を使用して他のクエリ句を組み合わせることができます。

bool 内の句

bool クエリは、主に 4 つの句を受け入れます。

  1. must: この配列内のすべての句が一致する必要がありますmust 内の句は関連性スコアに貢献します。
  2. filter: この配列内のすべての句が一致する必要がありますが、スコアリングされないコンテキストで実行されます。これにより、厳格な包含/除外基準に対してはるかに高速になります。
  3. should: この配列内の少なくとも 1 つの句が一致する必要があります(推奨)。これらの句は関連性スコアに影響を与えますが、マッチングには必須ではありません。
  4. must_not: この配列内のどの句も一致してはなりません(論理 NOT に相当)。

実用的な bool クエリの例

いくつかの概念を組み合わせて、「security」に言及し、下書きを除外し、'US' リージョンで利用可能な優先度の高いドキュメントを検索してみましょう。

GET /logs/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "security breach"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "region.keyword": "US"
          }
        }
      ],
      "should": [
        {
          "term": {
            "priority": 5
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "status.keyword": "DRAFT"
          }
        }
      ]
    }
  }
}

例の説明:

  • Must: ドキュメントは、分析されたコンテンツフィールドに「security breach」というフレーズを含んでいる必要があります。
  • Filter: ドキュメントは 'US' リージョン用にタグ付けされている必要があります(高速で完全一致)。
  • Should: priority: 5 に一致するドキュメントは関連性スコアが上昇しますが、must 句と filter 句を満たす優先度の低いドキュメントも引き続き返されます。
  • Must Not: 'DRAFT' とマークされたドキュメントは厳密に除外されます。

クエリ構築のベストプラクティス

検索の精度とパフォーマンスの両方を確保するには、次のガイドラインに従ってください。

  • スコアリングされない条件には must よりも filter を優先する。 包含/除外のみをチェックする場合(例:ID、正確な日付、ステータスによるフィルタリング)は、常に bool クエリ内で filter 句を使用してください。これにより、キャッシュが活用され、高価なスコアリング計算が回避されます。
  • 正確なクエリを賢く使用する: text(分析済み)としてマッピングされたフィールドには match を使用します。keyword(未分析)としてマッピングされたフィールドには term または範囲クエリを使用します。
  • 深いネストを避ける: 可能ではありますが、深くネストされた bool クエリは読み取りやデバッグが困難になり、パフォーマンスが低下する可能性があります。
  • minimum_should_match を活用する: should 句の場合、minimum_should_match(例:1 または 2)を設定すると、それらのオプション条件のうち一定数が満たされることが強制され、実質的に必須条件に変換されつつ、スコアリングに貢献できるようになります。

マッピングが適切なクエリを決定する

Query DSL の間違いのほとんどは、マッピングに起因します。フィールドが想定とは異なる方法でマッピングされている場合、クエリは正しく見えても、混乱を招く結果を返す可能性があります。

一般的なパターンは、キーワードサブフィールドを持つテキストフィールドです。

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "status": { "type": "keyword" },
      "created_at": { "type": "date" },
      "price": { "type": "double" }
    }
  }
}

分析済みの全文検索動作が必要な場合は、titlematch を使用します。正確なタイトル値が必要な場合は、title.keywordterm を使用します。status はすでにキーワードであるため、term を使用します。created_at または price は日付および数値であるため、range を使用します。

テキストフィールドに対する term クエリが期待どおりに機能しない場合、問題は多くの場合分析にあります。保存されたトークンは、小文字化、分割、ステミング、またはその他の方法で変更されている可能性があります。クエリを変更する前にマッピングを確認してください。

GET /products/_mapping

テキスト分析の問題については、_analyze が便利です。

GET /products/_analyze
{
  "field": "description",
  "text": "Cloud Computing"
}

これにより、Elasticsearch が検索対象とするトークンが表示されます。

matchmatch_phrase、および multi_match

match は日常的な全文検索クエリですが、使用するクエリはこれだけではありません。

単語の順序が重要な場合は、match_phrase を使用します。

GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "wireless charging stand"
    }
  }
}

これは、製品名、ログメッセージ、ドキュメントタイトル、および正確な順序が意味を持つフレーズに役立ちます。match よりも厳格であるため、返されるドキュメントが少なくなる可能性があります。

同じユーザー入力を複数のフィールドで検索する必要がある場合は、multi_match を使用します。

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "noise cancelling headphones",
      "fields": ["title^3", "description", "brand^2"]
    }
  }
}

^3^2 のブーストは、titlebrand での一致が description での一致よりも重要であることを Elasticsearch に伝えます。ブーストは、ドキュメントが確実に 1 位になることを保証するものではありません。これはスコアリングのヒントです。ブーストを過度に調整する前に、実際のクエリでテストしてください。

クラスターに負荷をかけないページネーション

基本的な from および size パラメータは、浅いページネーションには適しています。

GET /products/_search
{
  "from": 20,
  "size": 10,
  "query": {
    "match": {
      "description": "laptop sleeve"
    }
  }
}

深いページネーションは異なります。1000 ページ目を要求すると、Elasticsearch は多くの結果を並べ替えてスキップする必要があります。ユーザー向けの検索では、無制限の深いページングは避けてください。エクスポートやバックグラウンドスキャンの場合は、安定した並べ替えとともに search_after を使用します。

GET /products/_search
{
  "size": 100,
  "sort": [
    { "created_at": "asc" },
    { "_id": "asc" }
  ],
  "search_after": ["2025-01-10T12:00:00Z", "abc123"],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

search_after の値は、前の応答の最後のヒットの sort 配列から取得されます。このアプローチは、大量の結果セットを移動する場合により安定しています。

ソースフィルタリングで応答を有用に保つ

検索パフォーマンスはクエリの実行だけではありません。巨大なドキュメントを返すと、クライアント、ネットワーク、およびコーディネーティングノードが遅くなる可能性があります。UI で必要なフィールドが少数のみの場合は、それらのフィールドのみを要求します。

GET /orders/_search
{
  "_source": ["order_id", "customer_id", "total", "created_at", "status"],
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "paid" } },
        { "range": { "created_at": { "gte": "now-7d/d" } } }
      ]
    }
  }
}

これにより、応答が読みやすくなり、ペイロードサイズを削減できます。適切なインデックス設計に代わるものではありませんが、ドキュメントに現在のページで不要な大きな説明、メタデータブロブ、またはネストされた配列が含まれている場合に役立ちます。

並べ替えと集計には適切なフィールドが必要

分析済みテキストの並べ替えは、通常は間違いです。キーワード、数値、または日付フィールドで並べ替えます。

GET /products/_search
{
  "sort": [
    { "price": "asc" },
    { "title.keyword": "asc" }
  ],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

同じことが多くの集計にも当てはまります。ステータスごとのカウントが必要な場合は、キーワードフィールドで集計します。

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_by_status": {
      "terms": {
        "field": "status"
      }
    }
  },
  "query": {
    "range": {
      "created_at": {
        "gte": "now-30d/d"
      }
    }
  }
}

size: 0 は、一致するドキュメントではなく集計結果のみが必要であることを Elasticsearch に伝えます。これは、応答をよりクリーンに保つための小さな習慣です。

explainprofile を使用したクエリのデバッグ

結果のランキングが奇妙な場合は、単一のドキュメントで explain を使用します。

GET /products/_explain/SKU10021
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

クエリが遅い場合は、非本番環境または注意深く制御された本番環境テストで profile を使用します。

GET /products/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        { "match": { "description": "cloud computing" } }
      ],
      "filter": [
        { "term": { "status": "active" } }
      ]
    }
  }
}

プロファイル出力は冗長ですが、時間がテキストクエリ、フィルター、スクリプト、またはリクエストの他の部分のどこで費やされているかを示すことができます。アプリケーションコードでプロファイリングを有効にしたままにしないでください。デバッグツールとして使用してください。

賢明なクエリ構築の習慣

ほとんどのアプリケーション検索では、次の順序でリクエストを構築します。

  1. 正確な制約を filter に配置します:テナント ID、ステータス、リージョン、日付ウィンドウ、権限。
  2. ユーザーが入力したテキストを mustmatchmatch_phrase、または multi_match とともに配置します。
  3. ランキングの優先順位には should を使用します。minimum_should_match を設定しない限り、ハード要件には使用しません。
  4. _source を呼び出し元が必要とするフィールドに制限します。
  5. ページネーションまたはエクスポートが重要な場合は、安定した並べ替えを追加します。
  6. Elasticsearch を非難する前にマッピングを確認します。

Query DSL は、フィルタリング、スコアリング、並べ替え、応答の整形を分離するため、強力です。これらのジョブを分離しておくと、クエリの読み取り、調整が容易になり、本番環境での驚きが少なくなります。

小さなトラブルシューティングの例

ユーザーが ACME-1000 を検索しても結果が得られず、製品が存在するとします。すぐにワイルドカードを追加しないでください。最初にマッピングを確認します。skukeyword の場合、これは機能するはずです。

GET /products/_search
{
  "query": {
    "term": {
      "sku": "ACME-1000"
    }
  }
}

sku が誤って text としてマッピングされた場合、分析によって値が分割または変更された可能性があります。場合によってはクエリを実行することもできますが、より良い修正は通常、将来のインデックスのマッピングを変更することです。正確な識別子、ステータス、リージョン、テナント ID は、キーワードのようなフィールドである必要があります。人間が作成した説明やタイトルは、テキストフィールドである必要があります。マッピングが人々が実際にデータを取得する方法と一致している場合、Query DSL ははるかに簡単になります。