最近は Elastic Stack のソースコードを読むのが趣味になりつつあります。今日は Painless 。以前から気になっていた疑問や、調査中に派生した疑問を追ってみました。
- Java API 使えるのに、なぜ
java.lang.String.split
が使えない? - Painless での
dissect
やgrok
の実装はどうなってる? - Runtime field で他のインデックスを参照できるようになる!?
lookup
- 各コンテキストで利用できる API を手っ取り早く探したい
- Kibana Painless Lab の使い所...
- Runtime field でもスクリプトを保存しておいて使いまわしたいのだが...
なるべく該当箇所のソースコードへリンクを付けてます。
1. Java API 使えるのに、なぜ java.lang.String.split
が使えない?
正規表現を安心して使える ように利用を制限している。
Painless では java.lang.txt のようなファイルが沢山あり、利用できる API を制限している。また、 splitOnToken
のような Painless 独自のメソッドも注入している。
class java.lang.String {
()
int codePointAt(int)
int codePointBefore(int)
(中略)
String org.elasticsearch.painless.api.Augmentation decodeBase64()
String org.elasticsearch.painless.api.Augmentation encodeBase64()
String[] org.elasticsearch.painless.api.Augmentation splitOnToken(String)
String[] org.elasticsearch.painless.api.Augmentation splitOnToken(String, int)
Painless では各コンテキストで利用可能な API が異なる。どこで、何が使えるかは painless-string_script_field_script_field.json のようなコンテキストごとのファイルがあり、これで管理している様子。自動生成されているっぽいけれどどこで紐付けられているかは未開拓。
Runtime field では出力するデータ型によってコンテキストの実装が分かれている。 keyword
型のコンテキストは StringFieldScript 。
2. Painless での dissect
や grok
の実装はどうなってる?
dissect と grok は NamedGroupExtractor という仕組みで Painless と繋がっている。 Runtime field 向けの white list。
3. Runtime field で他のインデックスを参照できるようになる!? lookup
Painless の公式ドキュメント master ブランチを見ると、 lookup が追加されている。2022-03-11 時点ではまだリリースされていないが、これは便利!
4. 各コンテキストで利用できる API を手っ取り早く探したい
Painless のドキュメントで 使える API を探しても良いのだが、リンクを行ったりきたりするのがちょっと面倒な時も。
今回ソースコードを見ていて面白い内部向け API エンドポイント、 PainlessContextAction を発見。
# コンテキストの一覧を取得
GET /_scripts/painless/_context
# レスポンス
{
"contexts" : [
"aggregation_selector",
"aggs",
"aggs_combine",
"aggs_init",
...
"keyword_field",
(以下略)
コンテキスト毎の情報を取得するには:
GET /_scripts/painless/_context?context=keyword_field
Painless スクリプト書いている時にもう一枚 Kibana の Console を開いておいて確認すると便利。 "name" : "java.time.LocalDateTime"
などで検索すると使えるメソッドの一覧が確認できる。imported: true
のクラスはいちいちパッケージ書かなくてもクラス名だけで参照可能。
5. Kibana Painless Lab の使い所...
Painless Lab ではコンテキスト選んで Painless を実行できる。のだけど、実行時のコンテキストに大きく依存するのと、インデックスを選んだり、サンプルドキュメントを記述する UI 部品が分かれていたり、一つしか記憶してくれないとか、Filter コンテキストのテストなどは結果が true/false しか分からず、スクリプトで参照している値が確認できないなど、ちょっと使いづらい感じ。
Kibana Painless Lab では、 PainlessExecuteAction の API を呼び出している。ソースコードを見ると、 Kibana からは指定できない Runtime field も API ではサポートしていることを発見。
フィールドのデータ型を参照するために、インデックスは事前に存在する必要があるが、テスト用のドキュメントを指定できたり、 emit()
を使えば任意の変数をデバッグできたりして重宝するかも。
そして Dev Console から実行するので記録に残しやすい。
例えばこんな感じで実行できる:
# 何月?
POST _scripts/painless/_execute
{
"script": {
"source": """
def timestamp = doc['order_date'].value;
def localTimestamp = timestamp.withZoneSameInstant(ZoneId.of('Asia/Tokyo')).toLocalDateTime();
emit(localTimestamp.getMonthValue());
"""
},
"context": "long_field",
"context_setup": {
"index": "kibana_sample_data_ecommerce",
"document": {
"order_date": "2022-03-11T15:17:23.123+09:00"
}
}
}
# 結果
{
"result" : [
3
]
}
Runtime field は _search
実行時に定義できるので、すでにドキュメントが保存されているなら検索リクエストでテストするのも良い。
GET kibana_sample_data_ecommerce/_search
{
"size": 1,
"_source": false,
"fields": [
"order_date", "month"
],
"runtime_mappings": {
"month": {
"type": "long",
"script": {
"source": """
def timestamp = doc['order_date'].value;
def localTimestamp = timestamp.withZoneSameInstant(ZoneId.of('Asia/Tokyo')).toLocalDateTime();
emit(localTimestamp.getMonthValue());
"""
}
}
}
}
# 結果
{
"took" : 2,
...
"hits" : {
...
"hits" : [
{
"_index" : "kibana_sample_data_ecommerce",
"_id" : "HI7IiX4BdgfmkI_X6zlv",
"_score" : 1.0,
"fields" : {
"order_date" : [
"2022-02-07T09:28:48.000Z"
],
"month" : [
2
]
}
}
]
}
}
6. Runtime field でもスクリプトを保存しておいて使いまわしたいのだが...
タイムスタンプから、年月や曜日、時間だけを切り出したいというのはよくある話。 ID 指定で保存したスクリプトを呼び出して、いろいろな Runtime field で使いまわせたら便利。
Ingest node pipeline なら、呼出時にパラメータで対象のフィールドや、取得単位を指定する形で、コンパイル回数も最小にできる再利用可能な部品が定義できる。 保存したスクリプトを Ingest node pipeline で再利用する例。
Runtime field でも同じ様なことできないか試してみたら以下のエラーに。 残念!
stored scripts are not supported for runtime field
コード読んでると何か作りたくなってくるなー。