2016年12月13日 エンジニアリング

KibanaのScript FieldでPainlessを使う

著者 Tanya Bragin

Kibanaは、Elasticsearchに保存されたデータの検索や可視化を可能にする強力なツールです。可視化にあたって、KibanaはElasticsearch mappingに定義されているフィールドを探し、チャートを作成するユーザーにそのフィールドをオプションとして提示してくれます。でも、もし重要な値を別のフィールドとして定義し忘れてしまった場合はどうでしょう?あるいは、2つの異なるフィールドを1つのフィールドとして扱いたい場合は?こんなときこそ、KibanaのScript Fieldが活躍してくれます。

実はScript FieldはKibana 4のリリース当初からありました。これが導入された当時は、Lucene Expressionsという数値のみを使用するElasticsearchのスクリプト言語でしか定義することができなかったため、Script Fieldは一部のユースケースにのみその使用が限られていました。5.0ではPainlessという、様々な種類のデータ上で動作可能な安全でパワフルなスクリプト言語が導入されており、その結果、Kibana 5.0のScript Fieldはこれまでより遥かに強力なものとなりました。

ここからは一般的なユースケースでのScript Fieldの作成方法をご紹介します。データセットはKibanaのGetting Startedチュートリアルと同じものを使い、Elastic Cloudで無料で起動できる、ElasticsearchとKibanaのインスタンスを使用します。

次の動画は、Elastic Cloudから個人用ElasticsearchとKibanaのインスタンスを起動して、サンプルデータセットを読み込む方法を紹介しています。

Script Fieldの仕組み

Elasticsearchでは、すべてのリクエストにおいてScript Fieldを指定することができます。これをさらに使いやすくするため、Kibanaでは後に続く様々なUIで使用できるよう、Managementセクションで1つだけScript Fieldを定義できるようになっています。Script Fieldは他の設定と一緒に.kibanaインデックスに保存されますが、この設定はKibana特有のものなので、KibanaのScript FieldがElasticsearchのAPIユーザーに開示されることはありません。

KibanaでScript Fieldを定義する際は、動的スクリプト作成が可能なElasticsearchノードにインストールされている言語の中から、好きなスクリプト言語を選択することができます。5.0のデフォルトでは"expression"と"painless"で、2.xでは"expression"のみとなっています。他のスクリプト言語をインストールして動的スクリプト作成を有効にすることもできますが、それらは十分にサンドボックス化できずに廃止された言語なので、この方法はお勧めしません。

Script Fieldは一度につき1つのElasticsearchドキュメントで動作しますが、同一のドキュメント内であれば複数のフィールドを参照することができます。そのため、Script Fieldで単一のドキュメント内のフィールドを組み合わせたり変換したりはできますが、複数のドキュメントを参照して計算を行うことはできません(時系列計算など)。PainlessとLucene expressionsは共にdoc_valuesに保存されているフィールドで動作します。このため文字列データの場合は、keywordのデータタイプに文字列が保存されている必要があります。また、PainlessのScript Fieldは、_sourceで直接動作させることができません。

"Management"でScript Fieldを定義し終わったら、ユーザーはKibanaに保存されているその他のフィールドと同様にそのフィールドを扱うことができます。Script FieldはDiscoverのフィールドリストに自動的に表示され、Visualizeにて視覚化データの作成に使用することができます。定義されたScript Fieldは、クエリ時にElasticsearchに送られ、評価されます。抽出されたデータセットはElasticsearchから出されるその他のデータと組み合わされ、ユーザーに表またはチャートとして提示されます。

このブログを書いている時点で分かっているScript Fieldの制限事項がいくつかあります。Kibanaのビジュアルビルダーで使用できるElasticsearchのAggregationはほとんどScript Fieldに適用できますが、Significant Terms Aggregationは例外です。Script Fieldには、Discover、Visualize、Dashboardのフィルターバーにてフィルターをかけることができますが、明確な値を返すために、下に示す様にスクリプトを正しく書くよう注意が必要です。また、下記「ベストプラクティス」のセクションを参照して、Script Fieldを使用する際は安定した環境を保つように注意を払うことも大切です。

次の動画では、Kibanaを使ってScript Fieldを作成する方法を説明しています。

Script Fieldの例

このセクションでは、一般的なシナリオを用いて、KibanaでLucene expressionsとPainlessのScript Fieldを作成する例を説明します。すでにお伝えしていますが、これらの例はKibanaのGetting Startedチュートリアルで使用されたデータセットを流用しており、ElasticsearchとKibana 5.1.1を使用していることを前提に説明しています。それよりも古いバージョンではScript Fieldの種類によってはフィルターやソートに関する問題がいくつか発覚しているためです。

Elasticsearch 5.0ではデフォルトでLucene expressionsやPainlessが有効になっているため、ほとんどの場合Script Fieldは難しい設定無しにすぐに動作するはずです。唯一の例外は正規表現に基づくフィールドの構文解析を必要とするスクリプトのみです。このスクリプトでは、Painlessで正規表現マッチングを行うために、elasticsearch.ymlで次の値を設定する必要があります:script.painless.regex.enabled: true

単一のフィールドで計算を行う

  • : バイトからキロバイトを計算
  • 言語: expressions
  • 戻り値の型: number
 doc['bytes'].value / 1024

注:KibanaのScript Fieldは一度に1つのドキュメントでしか動作しないため、Script Fieldで時系列計算をする方法は無い、ということにご留意ください。 

結果が数値となる日付計算

  • : 日付から時刻を取得
  • 言語: expressions
  • 戻り値の型: number

Lucene expressionsには日付演算機能が備わっていますが、Lucene expressionsでは数値しか返されないため、曜日を表す文字列を返すにはPainlessを使用する必要があります(下記)。

 doc['@timestamp'].date.hourOfDay

注:上記のスクリプトでは1-24が返されます。 

doc['@timestamp'].date.dayOfWeek

注:上記のスクリプトでは1-7が返されます。 

2つの文字列値を結合する

  • : 送り元と行き先や、姓と名を結合する
  • 言語: painless
  • 戻り値の型: string
 doc['geo.dest.keyword'].value + ':' + doc['geo.src.keyword'].value

注:Script Fieldはdoc_valuesフィールドで動作する必要があるので、上記では文字列に.keywordを用いています。 

ロジックの導入

  • : 10,000バイトを超えるドキュメントに対して"big download"というラベルを返す
  • 言語: painless
  • 戻り値の型: string
 if (doc['bytes'].value > 10000) { 
    return "big download";
}
return "";

注:ロジックの導入の際は、必ずすべての実行パスに明確なreturn文と返り値を設定してください(nullは不可)。たとえば、上記Script Fieldで末尾にreturn文が無い場合やnullを返す場合は、Kibanaフィルターで使用するとコンパイルエラーとなってしまいます。また、KibanaのScript Fieldではロジックを関数に分割できない、ということにもご留意ください。 

部分文字列を返す

  • : URLの最後のスラッシュ以降を返す
  • 言語: painless
  • 戻り値の型: string
 def path = doc['url.keyword'].value;
if (path != null) {
    int lastSlashIndex = path.lastIndexOf('/');
    if (lastSlashIndex > 0) {
    return path.substring(lastSlashIndex+1);
    }
}
return "";

注:サブストリングの抽出には正規表現はなるべく使用しないようにしてください。indexOf()の方がリソースを消費せず、エラーとなりにくいためです。 

正規表現を使った文字列のマッチング、およびマッチに対するアクション

  • : "referer"フィールドに"error"を含めば文字列"error"を返し、それ以外は文字列"no error"を返す
  • 言語: painless
  • 戻り値の型: string
if (doc['referer.keyword'].value =~ /error/) { 
return "error"
} else {
return "no error"
}

注:正規表現マッチングに基づく条件文では簡易的な正規表現シンタックスが便利です。 

文字列をマッチングさせて、そのマッチを返す

  • : "host"フィールド内の最後のドットの後ろの文字列、ドメインを返す
  • 言語: painless
  • 戻り値の型: string
def m = /^.*\.([a-z]+)$/.matcher(doc['host.keyword'].value);
if ( m.matches() ) {
   return m.group(1)
} else {
   return "no match"
}

注:正規表現のmatcher()関数でオブジェクトを定義すると、正規表現とマッチした文字グループを抽出し、返すことができます。 

数字をマッチングさせて、そのマッチを返す

  • 例: IPアドレスの最初のオクテットを返し(文字列として保存)、数値として扱う
  • 言語: painless
  • 戻り値の型: number
 def m = /^([0-9]+)\..*$/.matcher(doc['clientip.keyword'].value);
if ( m.matches() ) {
   return Integer.parseInt(m.group(1))
} else {
   return 0
}

注:スクリプトでは正しいデータタイプを返すことが重要です。数値がマッチしたとしても、正規表現のマッチングでは文字列を返しますので、適切に整数に変換する必要があります。 

結果が文字列となる日付計算

  • : 日を曜日の文字列に構文解析
  • 言語: painless
  • 戻り値の型: string
LocalDateTime.ofInstant(Instant.ofEpochMilli(doc['@timestamp'].value), ZoneId.of('Z')).getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault())

注:PainlessはJavaのすべてのネイティブ形式に対応しており、LocalDateTime()などのネイティブ関数も使用できるので、より高度な日付計算に便利です。

ベストプラクティス

ここまででお分かりいただける通り、Painlessのスクリプト言語を用いることで、Kibanaの部分文字列でElasticsearchに保存された任意のフィールドから有用な情報を抽出することができます。ただし、この非常に便利な機能の力を最大限引き出すには細心の注意を払う必要があります。 

KibanaのScript Fieldを使用する際のベストプラクティスを以下にいくつかまとめます。

  • Script Fieldは必ず開発環境でお試しください。Script FieldはKibanaのManagementセクションに保存した瞬間からアクティブになるので(例:そのインデックスパターンの全ユーザーのDiscover画面に表示される)、製品環境で直接Script Fieldを展開しないようにしてください。まず開発環境でシンタックスを試して、実際的なデータセットとデータ量でScript Fieldのインパクトを評価してから、製品環境でお使いいただくことをお勧めします。 
  • 作成したScript Fieldをユーザーに使ってもらえる段階になったら、新しいデータではインデックス時にフィールドを抽出するよう、データ投入の方法を検討してみてください。これによりクエリ時のElasticsearchの処理が少なくなり、Kibanaユーザーにとってのレスポンス時間も短くなります。また、Elasticsearchの_reindexAPIを使用して既存のデータのインデックスを再構築することもできます。