KibanaのスクリプトフィールドでPainlessを使用する

KibanaはElasticsearchに保存されたデータを検索して可視化するための強力な機能を備えています。Kibanaでは、可視化するために、Elasticsearchマッピングで定義されたフィールドを検索し、グラフを作成しているユーザーにそのフィールドをオプションとして表示します。スキーマで重要な値を別のフィールドとして定義することを忘れた場合はどうなるでしょうか?あるいは、2つのフィールドを結合し、そのフィールドを1つのフィールドとして処理したい場合はどうすればよいでしょうか?そこで、Kibanaのスクリプトフィールドの出番です。

実際には、スクリプトフィールドはKibana 4という早い段階から導入されてきました。スクリプトフィールドが導入されたときには、フィールドを定義する方法は、数値の処理しか行わない、Elasticsearchのスクリプト言語であるLucene Expressionsを使用するしかありませんでした。このため、スクリプトフィールドの能力は、一部のユースケースに限られていました。Elasticsearchは、5.0でPainlessを導入しました。これは安全かつ強力なスクリプト言語であり、さまざまなデータ型に対応します。このため、Kibana 5.0のスクリプトフィールドの能力が大幅に向上しました。

このブログでは、ここから、一般的なユースケースでスクリプトフィールドを作成する方法について説明します。そのために、Kibana基本操作チュートリアルのデータセットを利用し、Elastic Cloudで実行されているElasticsearchとKibanaのインスタンスを使用します。Elastic Cloudは無料で導入できます。

次の動画では、Elastic Cloudで個人用のElasticsearchとKibanaのインスタンスを設定し、サンプルデータセットをそのインスタンスに読み込む方法について説明します。

スクリプトフィールドの仕組み

Elasticsearchでは、すべてのリクエストでスクリプトフィールドを指定できます。Kibanaはこの点について改善を進め、[管理]セクションで1度スクリプトフィールドを定義すると、その後はUIの複数の場所でそのフィールドを使用できるようになりました。Kibanaでは、スクリプトフィールドは、.kibanaインデックスの他の構成と一緒に保存されます。この構成はKibana固有の構成であり、KibanaのスクリプトフィールドはElasticsearchのAPIユーザーには公開されません。

Kibanaでスクリプトフィールドを定義するときには、スクリプト言語を選択できます。動的スクリプトが有効なElasticsearchノードにインストールされているすべての言語の中から選ぶことができます。5.0のデフォルト言語は「expression」と「painless」です。2.xでは「expression」だけでした。他のスクリプト言語をインストールし、動的スクリプトを有効化できますが、十分にサンドボックスでテストができず、廃止予定になっているため、この方法は推奨されません。

スクリプトフィールドは一度に1つのElasticsearchドキュメントでしか動作しませんが、そのドキュメントの複数のフィールドを参照する場合があります。このため、スクリプトフィールドを使用して、1つのドキュメント内でフィールド結合または変換するか、複数のドキュメントに基づいた計算(例:時系列演算)を実行しないことをお勧めします。PainlessとLuceneの両方で、式はdoc_valuesに格納されたフィールドで動作します。このため、文字列データでは、データ型キーワードに格納される文字列が必要です。Painlessに基づくスクリプトフィールドは、直接on _sourceでも動作しません。

スクリプトフィールドが[管理]で定義されると、ユーザーはKibanaのその他のフィールドと同じ方法でスクリプトフィールドを操作できます。スクリプトフィールドは自動的にDiscoverのフィールドリストに表示され、Visualizeで可視化を作成するために使用できます。Kibanaは、クエリ実行時に、評価対象のスクリプトフィールド定義をElasticsearchに渡すだけです。結果のデータセットはElasticsearchから戻された他の結果と結合され、表またはグラフでユーザーに表示されます。

このブログの執筆時点では、スクリプトフィールドの操作時にいくつかの制限があることが確認されています。Kibanaビジュアルビルダーで使用できるほとんどのElasticsearch集約をスクリプトフィールドに適用できます。ただし、最も顕著な例外として、重要な用語の集約は除きます。また、Discover、Visualize、ダッシュボードのフィルターバーを使用して、スクリプトフィールドでフィルターを適応できますが、次のように、適切に定義された値を返す正しいスクリプトを作成するように注意する必要があります。さらに、次の「ベストプラクティス」セクションを参考に、スクリプトフィールドを作成するときに環境を不安定化させないことを確認することが重要です。

次の動画では、Kibanaを使用してスクリプトフィールドを作成する方法について説明します。

スクリプトフィールドの例

このセクションでは、一般的なシナリオで、Kibanaで使用されるLucene式とPainlessスクリプトフィールドの例をいくつか取り上げます。前述のように、これらの例は、Kibana基本操作チュートリアルのデータセットを基に作成されました。そして、前のバージョンでは、特定のタイプのスクリプトフィールドでフィルターと並べ替えに関連する問題がいくつか確認されているため、ElasticsearchとKibana 5.1.1を使用していることが前提となっています。

Elasticsearch 5.0ではLucene式とPainlessがデフォルトで有効化されているため、ほとんどの場合、スクリプトフィールドはそのままで動作します。ただし、唯一の例外として、フィールドを正規表現に基づいて解析する必要があるスクリプトがあります。このようなスクリプトでは、elasticsearch.ymlに「script.painless.regex.enabled: true」という設定を加え、Painlessで正規表現一致をオンにする必要があります。

単一のフィールドで計算を実行する

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

注:Kibanaのスクリプトフィールドは一度に1つのドキュメントでしか機能しないため、スクリプトフィールドで時系列演算を実行する方法はありません。

日付演算で数値を返す

  • :日付を1日の時間に解析する
  • 言語:式
  • 戻り値の型:数値

Lucene式では、さまざまな日付操作関数をそのまま使用できます。ただし、Lucene式では数値のみが返されるため、文字列の曜日を返すには、Painlessを使用する必要があります(以下を参照)。

 doc['@timestamp'].date.hourOfDay

注:上記のスクリプトは1-24を返します

doc['@timestamp'].date.dayOfWeek

注:上記のスクリプトは1-7を返します

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

  • :ソースとターゲット、または名と姓を結合する
  • 言語:Painless
  • 戻り値の型:文字列
 doc['geo.dest.keyword'].value + ':' + doc['geo.src.keyword'].value

注:スクリプトフィールドはdoc_valuesのフィールドで動作する必要があるため、上記の文字列の.keywordバージョンを使用しています。

ロジックの導入

  • :10000バイトを超えるすべてのドキュメントで「big download」というラベルを返す
  • 言語:Painless
  • 戻り値の型:文字列
 if (doc['bytes'].value > 10000) { 
return "big download";
}
return "";

注:ロジックを導入するときには、すべての実行パスに適切に定義されたreturn文と適切に定義された戻り値(Null以外)が設定されていることを確認してください。たとえば、上記のスクリプトフィールドをKibanaフィルターで使用するときに、最後のreturn文が抜けていたり、文がNullを返したりする場合は、コンパイルエラーで失敗します。また、Kibanaスクリプトフィールドでは、ロジックを関数に分解することができません。

サブ文字列を返す

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

注:indexOf()演算の方がリソースの使用が少なく、エラーが発生しにくいため、可能であれば、正規表現を使用してサブ文字列を抽出する方法は避けてください。

正規表現を使用して文字列を照合し、一致したときにアクションを実行する

  • :フィールドでサブ文字列「error」が見つかった場合は、文字列「error」を返します。それ以外の場合は、文字列「no-error」を返します。
  • 言語:Painless
  • 戻り値の型:文字列
if (doc['referer.keyword'].value =~ /jp/error/) { 
return "error"
} else {
return "no error"
}

注:正規表現一致に基づく条件文では、簡易版の正規表現構文が便利です。

文字列を照合し、一致する文字列を返す

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

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

数値を照合し、一致する結果を返す

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

注:スクリプトで正しいデータ型を返すことが重要です。正規表現一致では、数値が一致した場合でも、文字列が返されます。このため、返すときに、明示的にその文字列を整数に変換する必要があります。

日付演算で文字列を返す

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

注:PainlessではJavaのネイティブ型がすべてサポートされるため、高度な日付演算を実行するときに便利なLocalDateTime()などのネイティブ型に関連するネイティブ関数を利用できます。

ベストプラクティス

おわかりのとおり、Painlessスクリプト言語は、Kibanaスクリプトフィールドを使用して、Elasticsearchに格納された任意のフィールドから有用な情報を抽出するための強力な手段です。ただし、強力な機能である分、大きな責任を伴います。

次に、Kibanaスクリプトフィールドの使用に関するベストプラクティスをいくつか概説します。

  • 必ず開発環境を使用して、スクリプトフィールドを実験すること。Kibanaの[管理]セクションで保存するとすぐに、スクリプトフィールドが有効になる(例:Discover画面ですべてのユーザーのインデックスパターンに表示)ため、直接本番環境でスクリプトフィールドを開発しないでください。最初に開発環境で構文を試し、スクリプトフィールドが実際のデータセットとデータ量に与える影響をステージングで評価してから、本番環境に転送することをお勧めします。
  • スクリプトフィールドがユーザーにとっての価値を提供するという確信が得られたら、インジェストを修正し、新しいデータのインデックス時にフィールドを抽出するように検討すること。これにより、クエリ時のElasticsearchの処理が減り、Kibanaユーザーにとって応答時間が短くなります。また、Elasticsearchで_reindex APIを使用して、既存のデータにインデックスを再作成することもできます。