Elastic Certified Engineer Exam対策 – PainlessScript & Runtime

Training banner showing an instructor icon pointing at a board over a blue tech dashboard background; main text reads 'Elastic Certified Engineer Exam' and 'PainlessScript & Runtime' with 'TRAINING' vertical on the right. トレーニング

このブログの目的

Elastic Certified Engineer Examでは、Runtime FieldやUpdate API、Reindex APIなど、Painless Scriptを利用する機能が出題範囲に含まれています。Painless ScriptはElasticsearchで利用される標準のスクリプト言語ですが、試験では高度なプログラミング技術が問われるわけではありません。要件に応じて適切なScriptを実装・修正できることが求められます。

本記事では、Painless Scriptの基本構文から、Search API・Aggregation・Runtime Field・Update API・Reindex APIにおける実践的な利用方法までを体系的に解説します。特に、Runtime Fieldについては、基本的な利用方法はもちろん、文字列加工や数値計算、条件分岐、検索・集計への活用方法など、試験でそのまま活用できる実践例を中心に取り上げています。

また、本記事で扱うサンプルは、KibanaのDev Toolsでそのまま実行できる形式で掲載しています。実際に手を動かしながら確認することで、Painless Scriptの記述方法だけでなく、各機能で利用するContext(doc、ctx._source、params、emit()など)の違いについても理解を深められる構成としています。

本記事を通して、Elastic Certified Engineer Examで求められるPainless ScriptとRuntime Fieldの基本事項を確実に身につけ、試験本番で要件に応じたScriptを実装できるレベルを目指しましょう。

当記事の、Elastic Certified Engineer Examの試験範囲は以下が該当します。

Searching data – Write and execute a search that utilizes a runtime field
Data processing – Define runtime fields to retrieve custom values using Painless scripting
・Data processing – Use the Reindex API and Update By Query API to reindex and/or update documents

試験情報は以下サイトで確認可能です。

Painless Scriptの基礎

Painless Scriptとは

Painless Scriptは、Elasticsearchで利用される標準のスクリプト言語です。検索時のスコア計算や条件判定、Runtime Fieldによる値の生成、Aggregationでの集計処理、Ingest Pipelineでのデータ加工、Update APIやReindex APIによるドキュメント更新など、さまざまな場面で利用されます。

Elastic Certified Engineer Examでは、Painless Scriptの文法を細かく暗記していることよりも、要件に応じて適切な場所でScriptを利用し、公式ドキュメントを参照しながら実装できることが重視されます。そのため、Javaのような構文をすべて習得する必要はなく、変数・条件分岐・フィールドへのアクセス方法など、基本的な書き方を理解しておけば十分です。

本章では、後続の章でRuntime FieldやSearch API、Aggregation、Ingest PipelineなどへPainless Scriptを実装できるようになることを目的として、基本構文や代表的な記述方法について解説します。特にElastic Certified Engineer Examでは、Update APIやReindex APIでPainless Scriptを記述する問題が出題される可能性があるため、基本的な構文やフィールドの参照方法を確実に理解しておきましょう。

Painless Scriptの基本構文

Painless ScriptはJavaに似た構文を採用しており、変数の宣言や四則演算、条件分岐、繰り返し処理などを組み合わせてScriptを記述します。Elastic Certified Engineer Examでは、複雑なプログラムを実装することは求められず、基本的な構文を理解し、要件に応じてScriptを記述・修正できることが重要です。

Painless Scriptは、KibanaのDev Toolsから手軽に動作確認できます。Dev Toolsには、Painless Scriptをインタラクティブに実行できる「Painless Lab」と、_scripts/painless/_execute APIなどを実行できる「Console」が用意されています。どちらを利用しても基本的な構文を試すことができるため、まずは簡単なScriptを実行してPainless Scriptに慣れておきましょう。

例えば、Consoleから_scripts/painless/_execute APIを実行すると、次のようにparamsで渡した値を利用したScriptを実行できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": "params.count / params.total",
    "params": {
      "count": 100.0,
      "total": 1000.0
    }
  }
}

この例では、paramsで渡された値を利用して計算を行い、結果として0.1が返却されます。

続いて、Painless Scriptの基本的な構文を確認してみましょう。
変数を宣言し、その合計値を返却する例です。Painless Labで確認します。

int x = 10;
int y = 20;

return x + y;

型を明示せず、自動推論されるdefを利用することもできます。

def x = 10;
def y = 20;

return x + y;

条件分岐は、Javaと同様にif文で記述します。

int score = 85;

if (score >= 80) {
    return "Pass";
} else {
    return "Fail";
}

また、繰り返し処理にはfor文やwhile文を利用できます。

int total = 0;

for (int i = 1; i <= 5; i++) {
    total += i;
}

return total;

このように、Painless Scriptの基本的な構文はJavaと非常によく似ています。一方で、Elastic Certified Engineer Examでは、変数宣言や条件分岐だけを問う問題が出題されることはほとんどありません。Search API、Runtime Field、Ingest Pipeline、Update APIなど、それぞれの機能で利用できる変数や記述方法を理解することが重要です。次節では、各機能で頻繁に利用するフィールドや変数へのアクセス方法について解説します。

フィールド・変数へのアクセス

Painless Scriptでは、Scriptを実行する機能(Context)によって利用できる変数が異なります。そのため、同じPainless Scriptであっても、Search API、Runtime Field、Ingest Pipeline、Update APIなどでは、参照できる値や記述方法が変わります。Elastic Certified Engineer Examでも、この違いを理解し、適切な変数を利用できることが重要です。

例えば、Search APIやRuntime Fieldでは、インデックス済みのFieldへアクセスするためにdocを利用します。

return doc['taxful_total_price'].value;

Scriptへ外部から値を渡したい場合は、paramsを利用します。

return doc['taxful_total_price'].value * params.discount_rate;

例えば、_scripts/painless/_execute APIでは、paramsへ値を渡してScriptを実行できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": "params.count / params.total",
    "params": {
      "count": 100.0,
      "total": 1000.0
    }
  }
}

一方、Update APIやReindex APIでは、ドキュメントを参照・更新するためにctx._sourceを利用します。

ctx._source.discount_price = ctx._source.taxful_total_price * 0.9;

代表的なContextと利用する変数の関係をまとめると、次のようになります。

Context主に利用する変数
Search APIdoc、params
Aggregationdoc、params
Runtime Fielddoc、emit()
Ingest Pipeline (※)ctx
Update APIctx._source、params
Reindex APIctx._source、params

すべてのContextで利用できる変数が共通というわけではなく、利用できる変数や処理内容はContextによって異なります。Elastic Certified Engineer Examでは、特にSearch API・Runtime Fieldではdoc、Update API・Reindex APIではctx._sourceを利用する場面が多く見られます。まずは代表的な変数の役割を理解し、どのContextで利用するのかを整理して覚えておきましょう。

※ Ingest PipelineでPainless Scriptを実装する問題は、現在のElastic Certified Engineer Examの出題範囲からは除外されています。

条件分岐・繰り返し

Painless Scriptでは、Javaと同様に条件分岐や繰り返し処理を利用できます。これらを組み合わせることで、条件に応じた値の計算や、複数のデータを順番に処理することが可能です。

条件分岐にはif文を利用します。例えば、Dev ToolsのConsoleから_scripts/painless/_execute APIを実行すると、条件に応じた値を返却するScriptを確認できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      if (params.score >= 80) {
        return "Pass";
      } else {
        return "Fail";
      }
    """,
    "params": {
      "score": 85
    }
  }
}

実行すると、次のような結果が返却されます。

繰り返し処理にはfor文やwhile文を利用できます。例えば、1から5までの合計値を計算する場合は次のように記述します。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      int total = 0;

      for (int i = 1; i <= 5; i++) {
        total += i;
      }

      return total;
    """
  }
}

実行結果は次のようになります。

また、条件に応じて繰り返し処理をスキップするcontinueや、繰り返しを終了するbreakも利用できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": """
      int total = 0;

      for (int i = 1; i <= 10; i++) {
        if (i == 5) {
          continue;
        }

        if (i == 8) {
          break;
        }

        total += i;
      }

      return total;
    """
  }
}

この例では、5は加算せず、8に到達した時点で繰り返し処理を終了します。

1 + 2 + 3 + 4 + 6 +7 = 23

Elastic Certified Engineer Examでは、複雑なアルゴリズムや高度な制御構文を実装する問題が出題される可能性は高くありません。

Elastic Certified Engineer Examでは、if文による条件分岐を利用したScriptを実装する問題が出題される可能性があります。一方、for文やwhile文による繰り返し処理が問われる可能性は高くありません。まずは条件分岐を確実に理解し、余裕があれば繰り返し処理についても基本的な書き方を習得しておきましょう。

主な組み込みAPI・関数

Painless Scriptでは、Javaと同様にさまざまな組み込みAPIや関数を利用できます。文字列操作や数値計算、日付・時刻の操作などを簡潔に記述できるため、Scriptを実装する際に頻繁に利用されます。

Mathクラスを利用した数値計算

数値計算にはMathクラスを利用できます。次の例では、平方根を計算しています。

POST /_scripts/painless/_execute
{
  "script": {
    "source": "return Math.sqrt(params.value);",
    "params": {
      "value": 144
    }
  }
}

実行すると、12.0が返却されます。

length()を利用した文字列長の取得

文字列の長さを取得する場合は、length()を利用できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": "return params.message.length();",
    "params": {
      "message": "Elastic"
    }
  }
}

実行すると、7が返却されます。

substring()を利用した文字列の切り出し

文字列の一部を取得するには、substring()を利用できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": "return params.message.substring(0, 5);",
    "params": {
      "message": "Elasticsearch"
    }
  }
}

実行すると、Elastが返却されます。

toUpperCase()を利用した大文字変換

文字列を大文字へ変換するには、toUpperCase()を利用できます。

POST /_scripts/painless/_execute
{
  "script": {
    "source": "return params.message.toUpperCase();",
    "params": {
      "message": "elastic"
    }
  }
}

実行すると、ELASTICが返却されます。

このように、Painless ScriptではJavaで利用できる代表的なAPIやメソッドを利用できます。Elastic Certified Engineer Examでは、これらのAPIやメソッドを暗記する必要はありません。試験中に公式ドキュメントを参照しながら適切なAPIを選択できれば十分です。まずはMathクラスや文字列操作など、実際のScriptでよく利用される代表的なAPI・関数があることを理解しておきましょう。

Elastic Docs > Reference > Elasticsearch > Scripting languages > Painless

構文や記述方法をすべて暗記しておく必要はありません。公式ドキュメントにはサンプルコードが多数掲載されているため、試験中に記述方法で迷った場合は、適宜参照しながら実装するとよいでしょう。また、どのドキュメントのどのページに、どのようなサンプルコードが掲載されているかをあらかじめ把握しておくことで、試験中に必要な情報へ素早くたどり着けるようになります。

Painless Scriptの活用

Searchで利用するScript

Search APIでは、Painless Scriptを利用することで、検索時にFieldの値を基に計算を行ったり、検索結果へ動的なFieldを追加したりできます。代表的な用途として、script_fieldsによる計算結果の追加、script_scoreによるスコアのカスタマイズ、script queryによる条件検索などがあります。

Elastic Certified Engineer Examでは、Search APIでPainless Scriptを利用する問題が出題される可能性があります。ただし、高度なアルゴリズムを実装することは求められず、docparamsを利用して簡単な計算を行える程度の知識があれば十分です。

例えば、次の例では、taxful_total_priceを10%割り引いた価格をdiscount_priceというField名で検索結果へ追加しています。

GET kibana_sample_data_ecommerce/_search
{
  "size": 3,
  "script_fields": {
    "discount_price": {
      "script": {
        "source": "doc['taxful_total_price'].value * params.rate",
        "params": {
          "rate": 0.9
        }
      }
    }
  }
}

実行結果

この例では、docからtaxful_total_priceを取得し、paramsで渡した割引率を乗算しています。script_fieldsで生成したFieldは検索結果にのみ追加され、インデックスへ保存されることはありません。

size: 3を指定しているため、検索結果は3件のみ返却されます。検索条件を省略した場合はmatch_allクエリが暗黙的に実行されるため、この例ではscript_fieldsの動作確認を目的として返却件数を3件に制限しています。
なお、検索条件を指定した場合は、通常、関連度スコア(_score)の高い順に結果が返却されます。

Aggregationで利用するScript

Aggregationでは、Painless Scriptを利用することで、Fieldの値を加工した上で集計を実行できます。通常はFieldの値をそのまま集計しますが、Scriptを利用することで、値を動的に変換したり計算したりしてから集計することが可能になります。

まずは、Scriptを利用しない一般的なavg Aggregationを見てみます。次の例では、taxful_total_priceの平均値を算出しています。

GET kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "taxful_total_price"
      }
    }
  }
}

この場合、avg Aggregationはtaxful_total_priceの値をそのまま利用して平均値を計算します。

一方、次の例では、Painless Scriptを利用して、価格を10%割り引いた値を平均値として集計しています。

GET kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "aggs": {
    "avg_discount_price": {
      "avg": {
        "script": {
          "source": "doc['taxful_total_price'].value * params.rate",
          "params": {
            "rate": 0.9
          }
        }
      }
    }
  }
}

この例では、各ドキュメントに対して

taxful_total_price × 0.9

を計算し、その計算結果をavg Aggregationの入力値として利用しています。つまり、Scriptによって算出された値を集計対象として平均値を計算しています。

このように、Aggregationではfieldまたはscriptのいずれかを指定できます。

指定方法集計対象
fieldFieldの値をそのまま集計する
scriptScriptで計算・加工した値を集計する

fieldだけで要件を満たせる場合はScriptを利用する必要はありません。一方、値を変換してから集計したい場合や、複数のFieldを組み合わせた計算結果を集計したい場合は、Painless Scriptを利用することで柔軟なAggregationを実装できます。

Runtime Fieldで利用するScript

Runtime Fieldでは、Painless Scriptを利用して、検索時に動的なFieldを生成できます。インデックスへ新たなFieldを保存することなく、既存のFieldを加工した値や、複数のFieldを組み合わせた値を検索やAggregation、ソートの対象として利用できる点が特徴です。

Runtime Fieldで利用するPainless Scriptは、Search APIやAggregationで利用するScriptと同様にdocからFieldへアクセスします。一方、計算結果を返却する際はreturnではなくemit()を利用する点が大きな違いです。

例えば、次の例では、taxful_total_priceが存在し、かつ値が格納されている場合のみ、100以上の価格を10%割り引いた値をdiscount_priceというRuntime Fieldとして生成し、その値が高い順に検索結果を並び替えています。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "discount_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price') &&
              doc['taxful_total_price'].size() > 0) {

            double price = doc['taxful_total_price'].value;

            if (price >= 100) {
              emit(price * 0.9);
            } else {
              emit(price);
            }
          }
        """
      }
    }
  },
  "fields": [
    "discount_price"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price"
  ],
  "sort": [
    {
      "discount_price": {
        "order": "desc"
      }
    }
  ],
  "size": 3
}

実行すると、discount_priceが検索結果へ追加され、その値が高い順に上位3件が返却されます。

この例では、まずdoc.containsKey()でFieldが存在するかを確認し、さらにsize() > 0で値が格納されていることを確認しています。その後、if文で価格による条件分岐を行い、100以上の商品は10%割り引いた価格、それ以外の商品は元の価格をemit()でRuntime Fieldとして返却しています。

Search APIのscript_fieldsも検索結果へ動的なFieldを追加できますが、Runtime Fieldは生成したFieldを通常のFieldと同様に検索・Aggregation・ソートの対象として利用できる点が大きく異なります。そのため、既存のデータを変更することなく、新しいFieldを追加したかのように扱えることがRuntime Fieldの大きなメリットです。

Elastic Certified Engineer Examでは、Runtime Fieldでdocemit()を利用したPainless Scriptを実装できることが重要です。また、if文による条件分岐やFieldの存在・空値チェックを組み合わせたScriptの基本的な書き方に加え、生成したRuntime Fieldを検索やAggregation、ソートに利用できることも理解しておきましょう。

Runtime Fieldについては、別記事「Elastic Certified Engineer Exam対策 – Runtime Field」で概要を解説していますので、あわせて参考にしてください。

Ingest Pipelineで利用するScript

Ingest Pipelineでは、Script Processorを利用することで、ドキュメントをインデックスへ登録する前に値を加工できます。例えば、Fieldの値を変換したり、新しいFieldを追加したり、条件に応じて値を変更したりといった前処理をPainless Scriptで実装できます。

Search APIやAggregationではdocを利用してFieldへアクセスしましたが、Ingest Pipelineではまだインデックスへ登録されていないドキュメントを処理するため、ctxを利用してFieldへアクセスします。これは、Update APIやReindex APIで利用するctx._sourceとは異なる点です。

例えば、次の例では、taxful_total_priceが存在する場合のみ、10%割り引いた価格をdiscount_priceという新しいFieldとして追加しています。

PUT _ingest/pipeline/add-discount-price
{
  "processors": [
    {
      "script": {
        "source": """
          if (ctx.containsKey('taxful_total_price')) {
            ctx.discount_price = ctx.taxful_total_price * 0.9;
          }
        """
      }
    }
  ]
}

作成したPipelineは、_simulate APIを利用することで簡単に動作確認できます。

POST _ingest/pipeline/add-discount-price/_simulate
{
  "docs": [
    {
      "_source": {
        "taxful_total_price": 200
      }
    }
  ]
}

実行すると、discount_priceが追加されたドキュメントが返却されます。

この例では、ctx.containsKey()でFieldの存在を確認した後、ctx.taxful_total_priceの値を利用して新しいFieldを生成しています。Ingest Pipelineでは、ctxへ値を設定すると、その内容がインデックスへ登録されるドキュメントへ反映されます。

Search APIやRuntime Fieldでは検索時に動的な値を生成していましたが、Ingest Pipelineではインデックス登録前にドキュメントそのものを書き換える点が大きく異なります。そのため、一度登録された値は以降の検索やAggregationで通常のFieldとして利用できます。

現在のElastic Certified Engineer Examでは、Ingest PipelineでPainless Scriptを実装する問題は出題範囲から除外されています。ただし、Painless Scriptを利用できる代表的なContextの一つであることや、docではなくctxを利用することは理解しておくとよいでしょう。

Update APIで利用するScript (_update_by_query)

本章では、Painless Scriptを利用した更新方法として、_update_by_query APIを例に解説します。

Update APIでは、Painless Scriptを利用して既存のドキュメントを更新できます。Fieldの値を変更したり、新しいFieldを追加したり、条件に応じて更新内容を切り替えたりすることが可能です。Elastic Certified Engineer Examでは、Painless Scriptを利用したUpdate APIの実装が出題される可能性が高いため、本節の内容は特に重要です。

Search APIやAggregationでは、Fieldへアクセスする際にdocを利用しました。一方、Update APIでは、更新対象のドキュメントを表すctx._sourceを利用します。また、Scriptへ外部から値を渡す場合は、他のContextと同様にparamsを利用します。

例えば、次の例では、taxful_total_priceが存在する場合のみ、10%割り引いた価格をdiscount_priceというFieldへ追加しています。

POST kibana_sample_data_ecommerce/_update_by_query
{
  "script": {
    "source": """
      if (ctx._source.containsKey('taxful_total_price')) {
        ctx._source.discount_price =
          ctx._source.taxful_total_price * params.rate;
      }
    """,
    "params": {
      "rate": 0.9
    }
  }
}

この例では、ctx._source.containsKey()でtaxful_total_priceの存在を確認し、その値を基にdiscount_priceを追加しています。ctx._sourceに存在しないField名を指定して値を代入すると、ドキュメントに新しいFieldが追加されます。

また、条件分岐を利用して、価格帯を表すFieldを追加することもできます。

POST kibana_sample_data_ecommerce/_update_by_query
{
  "script": {
    "source": """
      if (ctx._source.taxful_total_price >= 100) {
        ctx._source.price_category = "High";
      } else {
        ctx._source.price_category = "Low";
      }
    """
  }
}

この例では、taxful_total_priceが100以上の場合はprice_categoryにHighを設定し、それ以外の場合はLowを設定しています。更新後のドキュメントには、price_categoryという新しいFieldが追加されます。

Update APIでは、ctx._sourceへ値を代入すると、その内容が実際のドキュメントへ反映されます。Search APIやRuntime Fieldでは検索時のみ計算結果を生成していましたが、Update APIではドキュメントそのものを書き換える点が大きく異なります。そのため、一度更新された内容は永続的に保存され、以降の検索やAggregationでも通常のFieldとして利用できます。

Elastic Certified Engineer Examでは、ctx._sourceによるFieldの参照・更新方法や、paramsを利用した値の受け渡し、if文による条件分岐を組み合わせた基本的なScriptを実装できることが重要です。Painless Scriptが利用されるさまざまなContextの中でも、Update APIは特に出題される可能性が高いため、docではなくctx._sourceを利用することを確実に理解しておきましょう。

Reindexで利用するScript

Reindex APIでは、Painless Scriptを利用して、コピー先のインデックスへドキュメントを書き込む前に内容を加工できます。Update APIと同様に既存Fieldの更新や新しいFieldの追加が可能ですが、Reindex APIは「新しいインデックスを作成しながらデータを変換できる」点が大きな特徴です。

例えば、インデックス設計を変更したい場合や、新しいMappingへデータを移行したい場合には、Reindex APIとPainless Scriptを組み合わせることで、データを変換しながら新しいインデックスへ移行できます。

Reindex APIでも、Update APIと同様に更新対象のドキュメントはctx._sourceで参照します。また、Scriptへ外部から値を渡す場合はparamsを利用します。

例えば、次の例では、コピー先インデックスへ登録する前に、taxful_total_priceが存在する場合のみ10%割り引いた価格をdiscount_priceというFieldとして追加しています。

POST _reindex
{
  "source": {
    "index": "kibana_sample_data_ecommerce"
  },
  "dest": {
    "index": "kibana_sample_data_ecommerce_reindexed"
  },
  "script": {
    "source": """
      if (ctx._source.containsKey('taxful_total_price')) {
        ctx._source.discount_price =
          ctx._source.taxful_total_price * params.rate;
      }
    """,
    "params": {
      "rate": 0.9
    }
  }
}

Reindex APIでは、コピー対象のドキュメントをQueryで絞り込むこともできます。さらに、Painless Scriptを組み合わせることで、対象ドキュメントのみを変換しながら新しいインデックスへ移行できます。

POST _reindex
{
  "source": {
    "index": "kibana_sample_data_ecommerce",
    "query": {
      "range": {
        "taxful_total_price": {
          "gte": 100
        }
      }
    }
  },
  "dest": {
    "index": "kibana_sample_data_ecommerce_reindexed"
  },
  "script": {
    "source": """
      ctx._source.price =
        ctx._source.remove('taxful_total_price');
    """
  }
}

この例では、taxful_total_priceが100以上のドキュメントだけをコピー対象としています。また、コピー時にtaxful_total_priceを削除し(※)、その値をpriceというFieldへ設定しています。これにより、不要なドキュメントを除外しながら、Field構成も変更した新しいインデックスを作成できます。

Reindexによって作成されたkibana_sample_data_ecommerce_reindexedインデックスでは、remove()によってtaxful_total_priceフィールドが削除され、その値がpriceフィールドへ設定されるため、taxful_total_priceフィールドは存在しません。

Update APIとReindex APIはどちらもctx._sourceを利用してドキュメントを更新しますが、用途は異なります。

API主な用途
Update API既存インデックス内のドキュメントを更新する
Reindex APIQueryで対象データを絞り込み、新しいインデックスへコピーしながらデータを変換する

Elastic Certified Engineer Examでは、Reindex APIでもUpdate APIと同様にctx._sourceやparamsを利用してPainless Scriptを記述できることを理解しておくことが重要です。加えて、Reindex APIではsource.queryによってコピー対象を絞り込めることや、Painless Scriptを利用してFieldの追加・削除や値の加工を行いながら、新しいインデックスへデータを移行できることも押さえておきましょう。

Reindex APIは、Elastic Certified Engineer Examでも出題される可能性が高い機能の一つです。特に、Queryで対象ドキュメントを絞り込みながら、新しいインデックスへデータを移行するような問題が出題される可能性があります。Painless Scriptと組み合わせた基本的な実装方法を含め、確実に理解しておきましょう。

Runtime Field

Runtime Fieldの基本構文

Runtime Fieldでは、Painless Scriptを利用して検索時に動的なFieldを生成できます。基本的な考え方は第3章で解説したSearch APIやAggregationと同じで、インデックス済みのFieldへアクセスする際は doc を利用します。一方、Runtime Fieldでは計算結果を返却する方法が異なり、return ではなく emit() を利用する点が最大の特徴です。

また、Runtime FieldのScriptは、Mappingへ定義する場合とSearch APIの runtime_mappings で一時的に定義する場合がありますが、Scriptの記述方法はどちらも同じです。本節では、以降の節で利用する基本構文を確認しておきましょう。

まずは、taxful_total_price の値をそのまま price というRuntime Fieldとして返却する最もシンプルな例です。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "price": {
      "type": "double",
      "script": {
        "source": """
          emit(doc['taxful_total_price'].value);
        """
      }
    }
  },
  "fields": [
    "price"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price"
  ],
  "size": 3
}

実行すると、検索結果には _source に含まれる taxful_total_price に加えて、新しく生成された pricefields として返却されます。Runtime Fieldは検索時に生成されるため、インデックスへ保存されることはありません。

続いて、実際の運用ではFieldが存在しないドキュメントや、値が設定されていないドキュメントも考慮する必要があります。そのため、doc.containsKey()size() を利用して存在チェック・空値チェックを行うことが一般的です。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "price": {
      "type": "double",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price') && doc['taxful_total_price'].size() > 0) 
          {
              emit(doc['taxful_total_price'].value);
          }
        """
      }
    }
  },
  "fields": [
    "price"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price"
  ],
  "size": 3
}

この例では、まず doc.containsKey() でFieldが存在することを確認し、続いて size() > 0 によって値が格納されていることを確認しています。条件を満たした場合のみ emit() を実行することで、存在しないFieldや空値を参照した際のエラーを回避できます。これはRuntime Fieldを実装する際によく利用される基本的な記述パターンです。

文字列を加工する

Runtime Fieldでは、Painless Scriptを利用して文字列を加工した結果を新しいFieldとして生成できます。例えば、一部の文字列だけを切り出したり、大文字・小文字を変換したり、複数のFieldを連結したりといった処理が可能です。これらの処理はインデックスへ保存されることはなく、検索時にのみ実行されます。そのため、元のデータを変更することなく、用途に応じた文字列を柔軟に生成できます。

まずは、customer_full_name.keyword の先頭10文字を切り出し、customer_short_name というRuntime Fieldとして生成してみます。

文字列を切り出す

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "customer_short_name": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc.containsKey('customer_full_name.keyword')
              && doc['customer_full_name.keyword'].size() > 0) {

            String name = doc['customer_full_name.keyword'].value;
            int end = name.length() > 10 ? 10 : name.length();
            emit(name.substring(0, end));
          }
        """
      }
    }
  },
  "fields": [
    "customer_short_name"
  ],
  "_source": [
    "customer_full_name"
  ],
  "size": 3
}

この例では、まずFieldの存在と値の有無を確認した後、文字列の長さを確認し、10文字を超える場合は先頭10文字、10文字以下の場合は文字列全体を取得しています。これにより、10文字未満の名前に対しても安全にsubstring()を実行でき、実行時エラーを防ぐことができます。

文字列を大文字へ変換する

続いて、文字列をすべて大文字へ変換する例です。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "customer_upper_name": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc.containsKey('customer_full_name.keyword')
              && doc['customer_full_name.keyword'].size() > 0) {

            emit(doc['customer_full_name.keyword'].value.toUpperCase());
          }
        """
      }
    }
  },
  "fields": [
    "customer_upper_name"
  ],
  "_source": [
    "customer_full_name"
  ],
  "size": 3
}

英字を含む文字列であれば、toUpperCase() を利用して大文字へ変換できます。同様に、toLowerCase() を利用すれば小文字への変換も可能です。

複数のFieldを連結する

さらに、複数のFieldを組み合わせて新しい文字列を生成することもできます。次の例では、氏名とメールアドレスを連結し、1つのRuntime Fieldとして返却しています。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "customer_info": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc['customer_full_name.keyword'].size() > 0 &&
              doc['email'].size() > 0) {

            emit(
              doc['customer_full_name.keyword'].value +
              " <" +
              doc['email'].value +
              ">"
            );
          }
        """
      }
    }
  },
  "fields": [
    "customer_info"
  ],
  "_source": [
    "customer_full_name",
    "email"
  ],
  "size": 3
}

このように、Painless ScriptではJavaと同様の文字列操作メソッドを利用できるため、既存Fieldを加工した値を容易に生成できます。Runtime Fieldで生成した文字列は、通常のFieldと同様に検索やAggregation、ソートの対象として利用できるため、既存のインデックス構造を変更せずに新しい文字列Fieldを追加したい場合に有効です。

条件分岐を利用する

Runtime Fieldでは、Painless Scriptのif文を利用することで、Fieldの値に応じて異なる値を生成できます。例えば、価格帯の分類や商品のランク付け、条件に応じたフラグの付与など、実務でもよく利用される処理です。

価格帯を分類する

次の例では、taxful_total_priceの値に応じて、「High」「Medium」「Low」の3段階に分類したRuntime Fieldを生成しています。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "price_rank": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc['taxful_total_price'].size() > 0) {

            double price = doc['taxful_total_price'].value;

            if (price >= 100) {
              emit("High");
            } else if (price >= 50) {
              emit("Medium");
            } else {
              emit("Low");
            }
          }
        """
      }
    }
  },
  "fields": [
    "price_rank"
  ],
  "_source": [
    "taxful_total_price"
  ],
  "size": 5
}

この例では、まずtaxful_total_priceに値が格納されていることを確認した後、その値に応じて返却する文字列を切り替えています。emit()で返却された値がRuntime Fieldの値となり、検索結果のfields.price_rankへ追加されます。

条件に応じて割引価格を切り替える

条件分岐では、文字列だけでなく数値を返却することもできます。次の例では、100以上の商品だけ10%割引し、それ以外は元の価格を返却しています。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "discount_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc['taxful_total_price'].size() > 0) {

            double price = doc['taxful_total_price'].value;

            if (price >= 100) {
              emit(price * 0.9);
            } else {
              emit(price);
            }
          }
        """
      }
    }
  },
  "fields": [
    "discount_price"
  ],
  "_source": [
    "taxful_total_price"
  ],
  "size": 5
}

この例では、価格が100以上の商品のみ10%割引した値を返却しています。Runtime Fieldでは、文字列だけでなく数値や日付など、定義した型に応じた値をemit()で返却できます。

生成したRuntime Fieldで検索する

Runtime Fieldは、生成した値を検索条件として利用することもできます。次の例では、先ほど生成したprice_rankを対象に、「High」に分類されたドキュメントだけを検索しています。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "price_rank": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc['taxful_total_price'].size() > 0) {

            double price = doc['taxful_total_price'].value;

            if (price >= 100) {
              emit("High");
            } else if (price >= 50) {
              emit("Medium");
            } else {
              emit("Low");
            }
          }
        """
      }
    }
  },
  "query": {
    "term": {
      "price_rank": "High"
    }
  },
  "fields": [
    "price_rank"
  ],
  "_source": [
    "taxful_total_price"
  ]
}

Runtime Fieldは検索時に動的に生成されますが、通常のFieldと同様に検索やAggregation、ソートの対象として利用できます。そのため、既存のMappingを変更することなく、条件分岐によって生成した値を検索ロジックへ組み込める点が大きな特徴です。

数値を計算する

Runtime Fieldでは、Painless Scriptを利用して数値を動的に計算できます。例えば、商品の割引価格や平均単価、利益率など、既存のFieldを組み合わせて新しい数値Fieldを生成したい場面で活用されます。生成された値はインデックスへ保存されることはなく、検索時に計算されるため、元のデータを変更することなく柔軟な計算結果を利用できます。

次の例では、税込合計金額(taxful_total_price)を購入数量(total_quantity)で割って1商品あたりの価格を算出し、その価格へ10%割引を適用した後、送料500円を加算した値をfinal_priceというRuntime Fieldとして生成しています。

四則演算を組み合わせて計算する

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "final_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc['taxful_total_price'].size() > 0 &&
              doc['total_quantity'].size() > 0 &&
              doc['total_quantity'].value > 0) {

            double unitPrice =
              doc['taxful_total_price'].value / doc['total_quantity'].value;

            double finalPrice =
              ((unitPrice * 0.9) + 500) - 100;

            emit(finalPrice);
          }
        """
      }
    }
  },
  "fields": [
    "final_price"
  ],
  "_source": [
    "taxful_total_price",
    "total_quantity"
  ],
  "size": 5
}

この例では、まずtaxful_total_pricetotal_quantityで割ることで1商品あたりの価格を算出しています。その後、10%割引を適用し、送料500円を加算したうえで、クーポンとして100円を減算しています。このように、Runtime Fieldでは加算(+)、減算(-)、乗算(*)、除算(/)を自由に組み合わせることができ、複数のFieldを利用した実践的な計算を実装できます。

また、total_quantityが0の場合に除算エラーが発生しないよう、doc['total_quantity'].value > 0で事前にチェックしている点も重要です。Runtime Fieldでは検索時にScriptが実行されるため、実運用ではこのような入力値のチェックを行うことで、安全に計算を実行できます。

日付・時刻を利用する

Runtime Fieldでは、日付型Fieldに対して年・月・曜日・時間などの情報を取得したり、日付を加工して新しいFieldとして生成したりできます。例えば、「曜日ごとに集計したい」「営業時間内のデータだけを分析したい」「注文日時から年月だけを抽出したい」といった要件を、インデックスを変更することなく実現できます。

Elastic Certified Engineer Examでも、日付型Fieldを利用したRuntime Fieldを実装する問題が出題される可能性があります。特に、dayOfWeekEnumhourなどの日付情報を取得し、検索やAggregationで利用できるRuntime Fieldを作成できることを理解しておきましょう。

曜日名をRuntime Fieldとして生成する

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "full_day_name": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc['order_date'].size() > 0) {
            emit(
              doc['order_date'].value
                .dayOfWeekEnum
                .getDisplayName(TextStyle.FULL, Locale.ROOT)
            );
          }
        """
      }
    }
  },
  "fields": [
    "order_date",
    "full_day_name"
  ],
  "_source": false,
  "size": 5
}

この例では、order_dateから曜日を取得し、MondayTuesdayなどの曜日名をRuntime Fieldとして生成しています。dayOfWeekEnumで曜日の列挙値を取得し、getDisplayName()を利用することで、人が読みやすい文字列へ変換しています。

生成したfull_day_nameは通常のFieldと同様にAggregationや検索条件で利用できます。例えば、曜日ごとの注文件数を集計したい場合でも、新たなFieldをインデックスへ追加する必要はありません。

なお、日付型Fieldでも値が存在しないドキュメントではエラーになる可能性があるため、doc['order_date'].size() > 0のように値の有無を確認してから処理することを推奨します。

Runtime Fieldを検索・集計で利用する

Runtime Fieldの大きな特徴は、生成した値を単に検索結果へ表示するだけではなく、通常のFieldと同様に検索条件やAggregationの対象として利用できる点です。script_fieldsも検索結果へ動的な値を追加できますが、検索・集計・ソートには利用できません。一方、Runtime Fieldは検索時に生成された値をFieldとして扱えるため、既存のインデックスを変更することなく柔軟な分析を実現できます。

例えば、第4.5節で作成した曜日を表すRuntime Fieldを利用すると、曜日ごとの注文件数を集計できます。

GET kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "runtime_mappings": {
    "full_day_name": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc['order_date'].size() > 0) {
            emit(
              doc['order_date'].value
                .dayOfWeekEnum
                .getDisplayName(TextStyle.FULL, Locale.ROOT)
            );
          }
        """
      }
    }
  },
  "aggs": {
    "orders_by_day": {
      "terms": {
        "field": "full_day_name"
      }
    }
  }
}

この例では、runtime_mappingsで生成したfull_day_nameterms Aggregationの対象として利用しています。full_day_nameはインデックスへ保存されていませんが、検索実行時に動的に生成されるため、通常のkeyword型Fieldと同じように集計できます。

また、Runtime Fieldは検索条件にも利用できます。例えば、価格を10%割り引いたRuntime Fieldを生成し、その割引後価格が100以上の商品だけを検索するといったことも可能です。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "discount_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc['taxful_total_price'].size() > 0) {
            emit(doc['taxful_total_price'].value * 0.9);
          }
        """
      }
    }
  },
  "query": {
    "range": {
      "discount_price": {
        "gte": 100
      }
    }
  },
  "fields": [
    "discount_price"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price"
  ]
}

このように、Runtime Fieldは検索時に生成されるFieldでありながら、QueryやAggregation、Sortの対象として利用できます。そのため、「インデックス構造は変更したくないが、新しいFieldを使って分析したい」というケースで非常に有効です。一方で、検索のたびにPainless Scriptが実行されるため、大量データに対して頻繁に利用する場合は実行コストを考慮する必要があります。検索性能を重視する場合は、Runtime Fieldではなく通常のFieldやIndexed Runtime Fieldを検討することも重要です。

grok()を利用して文字列を抽出する

Runtime Fieldでは、Painless Scriptからgrok()を利用して文字列を解析し、抽出した値を新しいFieldとして生成できます。Grokは主にログ解析で利用される機能ですが、Runtime Fieldでも利用できるため、既存の文字列Fieldから必要な情報だけを取り出したい場合に役立ちます。

Elastic Certified Engineer Examでは、grok()を利用した高度なScriptを実装する問題が出題される可能性は高くありません。一方で、Painless ScriptでもGrokパターンを利用できることや、grok()extract()の基本的な使い方を知っておくと、公式ドキュメントを参照しながら実装できるようになります。

Grokパターンで先頭の単語を抽出する

次の例では、customer_full_name.keywordから先頭の単語を抽出し、extracted_first_nameというRuntime Fieldを生成しています。

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "extracted_first_name": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc.containsKey('customer_full_name.keyword')
              && doc['customer_full_name.keyword'].size() > 0) {

            def pattern = grok('%{WORD:first_name}');
            def matches = pattern.extract(doc['customer_full_name.keyword'].value);

            if (matches != null) {
              emit(matches['first_name']);
            }
          }
        """
      }
    }
  },
  "fields": [
    "extracted_first_name"
  ],
  "_source": [
    "customer_full_name"
  ],
  "size": 3
}

このScriptでは、まずgrok()を利用して%{WORD:first_name}というGrokパターンを生成しています。続いて、pattern.extract()customer_full_name.keywordの値を渡すことで、パターンに一致した文字列を抽出しています。

extract()の戻り値はMap形式となるため、抽出結果はmatchesへ格納されます。抽出に成功した場合は、matches['first_name']から値を取得し、emit()によってRuntime Fieldとして返却しています。

処理の流れは次のとおりです。

処理説明
grok()Grokパターンを生成する
pattern.extract()対象文字列からパターンに一致する値を抽出する
matches抽出結果を保持するMap
matches[‘first_name’]抽出した値を取得する
emit()Runtime Fieldとして返却する

例えば、customer_full_name.keywordに次の値が格納されていた場合、

Mary Thompson

Runtime Fieldには次の値が生成されます。

Mary

このように、grok()を利用することで、文字列をパターンに従って解析し、必要な情報だけをRuntime Fieldとして利用できます。Elastic Certified Engineer Examでは、Grokパターンそのものを暗記する必要はありませんが、grok()でパターンを生成し、pattern.extract()で値を抽出する基本的な流れは理解しておきましょう。また、必要なGrokパターンは公式ドキュメントを参照しながら実装できるようになっておくことが重要です。

Runtime Query CHECK LIST
emit() を使用しているか(returnではない)
doc を利用してFieldへアクセスしているか
ctx._source と混同していないか
□ Fieldの存在確認(doc.containsKey())を行っているか
□ 値の有無(size() > 0)を確認しているか
□ 除算を行う場合は、0除算を考慮しているか
text Fieldではなく、必要に応じて keyword Fieldを参照しているか
emit()で返却する値と、Runtime Fieldのtypeが一致しているか

実践問題

Practice Problem 1

Using the Search API, define a Runtime Field named discount_price using runtime_mappings.

Requirements:

  • Use the kibana_sample_data_ecommerce index.
  • The Runtime Field type must be double.
  • If taxful_total_price exists and contains a value, calculate a 10% discount.
  • Return the calculated value using the appropriate Painless function.
  • Include the following fields in the search results:
    • customer_full_name
    • taxful_total_price
    • discount_price
  • Return only the first 5 documents.

Answer

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "discount_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price')
              && doc['taxful_total_price'].size() > 0) {

            emit(doc['taxful_total_price'].value * 0.9);
          }
        """
      }
    }
  },
  "fields": [
    "discount_price"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price"
  ],
  "size": 5
}

解説

この問題では、Search APIのruntime_mappingsを利用して、検索時にのみ有効なRuntime Fieldを定義しています。Runtime Fieldの型はdoubleで定義し、taxful_total_priceが存在する場合のみ10%割引した価格をemit()で返却しています。

また、doc.containsKey()size() > 0を組み合わせることで、Fieldが存在しないドキュメントや値が設定されていないドキュメントでも安全にScriptを実行できます。

最後に、Runtime Fieldは_sourceには保存されないため、検索結果へ表示するにはfieldsへ指定する必要があります。

Practice Problem 2

Using the Search API, define a Runtime Field named price_category using runtime_mappings.

Requirements:

  • Use the kibana_sample_data_ecommerce index.
  • The Runtime Field type must be keyword.
  • If taxful_total_price is greater than or equal to 100, emit "High".
  • Otherwise, emit "Low".
  • Include the following fields in the search results:
    • customer_full_name
    • taxful_total_price
    • price_category
  • Return only the first 5 documents.

Answer

GET kibana_sample_data_ecommerce/_search
{
  "runtime_mappings": {
    "price_category": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price')
              && doc['taxful_total_price'].size() > 0) {

            if (doc['taxful_total_price'].value >= 100) {
              emit("High");
            } else {
              emit("Low");
            }
          }
        """
      }
    }
  },
  "fields": [
    "price_category"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price"
  ],
  "size": 5
}

解説

この問題では、keyword型のRuntime Fieldを生成し、if文による条件分岐を実装しています。数値の条件によって異なる文字列を返却するのは、Runtime Fieldでよく利用されるパターンです。emit()で返却する値の型は、Runtime Fieldのtypeと一致している必要があります。

Practice Problem 3

Using the Search API, define a Runtime Field named price_category using runtime_mappings.

Requirements:

  • Use the kibana_sample_data_ecommerce index.
  • The Runtime Field type must be keyword.
  • If taxful_total_price is greater than or equal to 100, emit "High".
  • Otherwise, emit "Low".
  • Use a terms aggregation to count the number of documents for each price_category.
  • Return only aggregation results, not search hits.

Answer

GET kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "runtime_mappings": {
    "price_category": {
      "type": "keyword",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price')
              && doc['taxful_total_price'].size() > 0) {

            if (doc['taxful_total_price'].value >= 100) {
              emit("High");
            } else {
              emit("Low");
            }
          }
        """
      }
    }
  },
  "aggs": {
    "orders_by_price_category": {
      "terms": {
        "field": "price_category"
      }
    }
  }
}

解説

この問題では、runtime_mappingsで定義したprice_categoryterms Aggregationの対象として利用しています。Runtime Fieldはインデックスに保存されませんが、検索時にFieldとして扱えるため、通常のkeyword Fieldと同じようにAggregationで集計できます。

size: 0を指定しているため、検索結果のドキュメント一覧は返却されず、Aggregation結果のみが返ります。これは集計結果だけを確認したい場合によく使う指定です。

Practice Problem 4

Using the Search API, define a Runtime Field named discount_price using runtime_mappings.

Requirements:

  • Use the kibana_sample_data_ecommerce index.
  • The Runtime Field type must be double.
  • If taxful_total_price exists and contains a value:
    • If the value is greater than or equal to 100, apply a 10% discount.
    • Otherwise, return the original value.
  • Search only documents where customer_gender is FEMALE.
  • Sort the results by discount_price in descending order.
  • Return the second page of results, assuming each page contains 5 documents.
  • Include the following fields in the search results:
    • customer_full_name
    • customer_gender
    • taxful_total_price
    • discount_price

Answer

GET kibana_sample_data_ecommerce/_search
{
  "from": 5,
  "size": 5,
  "runtime_mappings": {
    "discount_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price')
              && doc['taxful_total_price'].size() > 0) {

            double price = doc['taxful_total_price'].value;

            if (price >= 100) {
              emit(price * 0.9);
            } else {
              emit(price);
            }
          }
        """
      }
    }
  },
  "query": {
    "term": {
      "customer_gender": "FEMALE"
    }
  },
  "fields": [
    "discount_price"
  ],
  "_source": [
    "customer_full_name",
    "customer_gender",
    "taxful_total_price"
  ],
  "sort": [
    {
      "discount_price": {
        "order": "desc"
      }
    }
  ]
}

解説

この問題では、Runtime Fieldの定義に加えて、検索条件、ソート、ページネーションを組み合わせています。

ポイントは次のとおりです。

  • runtime_mappingsで検索時のみ有効なRuntime Fieldを定義している
  • discount_pricedouble型なので、emit()でも数値を返却している
  • customer_genderで検索対象を絞り込んでいる
  • Runtime Fieldであるdiscount_pricesortに利用している
  • 2ページ目を返すために、from: 5size: 5を指定している

Practice Problem 5

Using the Search API, define a Runtime Field named unit_price using runtime_mappings.

Requirements:

  • Use the kibana_sample_data_ecommerce index.
  • The Runtime Field type must be double.
  • Calculate unit_price by dividing taxful_total_price by total_quantity.
  • Avoid division by zero.
  • Search only documents where taxful_total_price is greater than or equal to 50.
  • Sort the results by unit_price in ascending order.
  • Return the third page of results, assuming each page contains 5 documents.
  • Include the following fields in the search results:
    • customer_full_name
    • taxful_total_price
    • total_quantity
    • unit_price

Answer

GET kibana_sample_data_ecommerce/_search
{
  "from": 10,
  "size": 5,
  "runtime_mappings": {
    "unit_price": {
      "type": "double",
      "script": {
        "source": """
          if (doc.containsKey('taxful_total_price')
              && doc.containsKey('total_quantity')
              && doc['taxful_total_price'].size() > 0
              && doc['total_quantity'].size() > 0
              && doc['total_quantity'].value > 0) {

            emit(
              doc['taxful_total_price'].value /
              doc['total_quantity'].value
            );
          }
        """
      }
    }
  },
  "query": {
    "range": {
      "taxful_total_price": {
        "gte": 50
      }
    }
  },
  "fields": [
    "unit_price"
  ],
  "_source": [
    "customer_full_name",
    "taxful_total_price",
    "total_quantity"
  ],
  "sort": [
    {
      "unit_price": {
        "order": "asc"
      }
    }
  ]
}

解説

この問題では、複数Fieldを利用したRuntime Fieldの計算に加えて、0除算対策、検索条件、ソート、ページネーションを組み合わせています。

ポイントは次のとおりです。

  • taxful_total_price / total_quantityで単価を計算している
  • total_quantity > 0を確認し、0除算を防いでいる
  • rangeクエリで対象ドキュメントを絞り込んでいる
  • Runtime Fieldであるunit_priceを昇順ソートに利用している
  • 3ページ目を返すために、from: 10size: 5を指定している

Elastic社が提供する無料Hands-on Trainingで理解を深めよう

本記事では、Painless Scriptの基本構文から、Runtime FieldやUpdate API、Reindex APIでの活用方法までを解説しました。Elastic Certified Engineer Examでは、これらを組み合わせて実装する問題が出題される可能性があるため、実際に手を動かしながら確認しておくことが重要です。

Elastic社では、無料で利用できるHands-on Trainingが公開されています。Painless ScriptやRuntime Fieldを題材とした内容も含まれており、本記事で学習した内容を実際の操作を通じて確認する際の参考になります。

また、試験本番では公式ドキュメントを参照できます。Hands-onや公式ドキュメントをあわせて活用しながら、必要なサンプルコードや実装方法を調べることにも慣れておくとよいでしょう。

Elastic Cloud アカウント作成

Elastic Training

Elastic Learning Portal

試験で問われるポイント

  • Runtime Fieldで、docからFieldを参照し、emit()を利用して値を返却できること。
  • Runtime Fieldで、文字列加工・数値計算・条件分岐・日付操作などのPainless Scriptを実装できること。
  • Runtime Fieldで生成したFieldを、検索・Aggregation・Sortで通常のFieldと同様に利用できること。
  • Search API・Runtime Field・Update API・Reindex APIで利用する変数(doc、emit()、ctx._source、params)の違いを理解し、適切に使い分けられること。
  • Runtime Fieldを実装する際に、Fieldの存在確認やsize()による空値チェックなど、安全にScriptを記述するための基本的な実装方法を理解していること。
  • Runtimeには、「Mappingに定義する方法」と「runtime_mappingsで検索時に定義する方法」の2種類存在し、いずれも要件に併せて定義できること。

当ドキュメントで扱った内容を理解し、基本的なPainless Scriptを実装できるレベルに達していれば、試験対策としては十分です。まずは基本事項を確実に押さえておきましょう。