はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

ElasticsearchのPainless Scriptのひとまず簡単な記述例

はじめに

Elasticsearch のPainless Scriptを「Score」コンテキストで、どんな記述ができそうかのさわりの部分を確認してみました。

という記事を書いておいてアレですが、この手のものは公式のリファレンスをしっかり読みましょう。

www.elastic.co

本格的に公式リファレンスを読む前に、ひとまず動作する例を見て準備運動になればというつもりで例をざっくり並べています。

サンプルデータ

こちらの冒頭の https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-examples.html のbulkロードデータを入れておくと良いでしょう。

なお試した環境はElasticsearch 6.4です。今回の範囲であれば、mapping等の設定は不要です。

//kibanaで実行。上記の引用元は、hockey/_bulk...となっているが、Elasticsearchの最近のバージョンだと「_doc」を入れた方が良いと思うというかここに何か入れないとエラーになる。

PUT hockey/_doc/_bulk?refresh
{"index":{"_id":1}}
{"first":"johnny","last":"gaudreau","goals":[9,27,1],"assists":[17,46,0],"gp":[26,82,1],"born":"1993/08/13"}
{"index":{"_id":2}}
{"first":"sean","last":"monohan","goals":[7,54,26],"assists":[11,26,13],"gp":[26,82,82],"born":"1994/10/12"}
{"index":{"_id":3}}
...

// 次のデータはこの記事オリジナル(numberは背番号を意識した数字)
{"index":{"_id":12}}
{"first":"太郎","last":"山田","number":18, "rand": "1234567890", goals":[7,54,26],"assists":[11,26,13],"gp":[26,82,82],"born":"1994/10/12"}

// 

値を取得

doc['フィールド名'] .valueで値が取得できる。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "doc['number'].value"
        }
      }
    }
  }
}
// ここではテーマが違うので説明しないが、検索条件なし(全件抽出)でfunction_scoreなので、要はスコアリングに、データ中の「number」の値を使うという意味になっている。// 検索結果イメージ(抜粋)

  "hits": {
    "total": 3,
    "max_score": 18,
    "hits": [
      {
        "_index": "hockey",
        "_type": "_doc",
        "_id": "12",
        "_score": 18,
        "_source": {
          "first": "太郎",
          "last": "山田",
          "number": 18,
          "rand": "1234567890",
          "goals": [
            7,
            54,
            26
          ]
        }
      }
  }

検索結果のscore値が id=12のデータは「18」、他は0になっている。... のでnumberの値が取得できた模様。

取得した値を使って計算

取得した値を使って計算はできるのかな。

できる。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "doc['number'].value * 2 + 5"
        }
      }
    }
  }
}

18 * 2 + 5 = 41 のようなデータが戻ってくる。
(18はそのドキュメントのnumberの値。numberフィールド自体を持たない文書の場合は、doc['number'].valueは0扱いの模様。エラーにはならない。)

文字列を取得できるか

ひとまず動く例。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "def x = doc['rand.keyword'].value; return Integer.parseInt(x)"
        }
      }
    }
  }
}

Scoreコンテキストなので、数字を返さなければならないので、def宣言で一度変数に入れる。
https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-types.html#dynamic-types

見てのとおり、複文を記述できる。

このElasticsearchのバージョンの場合、特にマッピング設定をしない時には、ドキュメントのデータ登録時に、「field名.keyword」でアナライズされない生のデータがインデックスに持ち込まれ、これを取り出すことになるので、「doc['rand.keyword']」のような指定方法になる。

また、defを使っているが、この例の場合は、

String x = doc['rand.keyword'].value;

とした方が良いだろう。

値が存在しない場合を考慮した記述

実は上記の場合、「山田太郎」のデータは良いのだが、他のrand.keywordを持たないデータはエラーになっている。

なので、値が存在しない場合を考慮してやる。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "def x = doc['rand.keyword'].size() == 0 ? '0': doc['rand.keyword'].value; return Integer.parseInt(x);"
        }
      }
    }
  }
}

ここでは3項演算子を用いた。 https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-examples.html#_missing_values

配列の値の取得(ひとまず実験)

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "doc['goals'].value"
        }
      }
    }
  }
}

↓

エラーにはならないが、配列中の最小の値が取得されるようだ。

doc['goals'][2]とすると、3番目に小さい値が取得される。

転置インデックスらに保持されている順序にアレンジされているのかな。

ちなみに、doc['goals'][2].valueはエラーになる。

続いてこれはどうだ。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "doc['goals'].value"
        }
      }
    }
  }
}

↓

エラーにはならないが、配列中の最小の値が取得されるようだ。

配列の値の取得(全項目取得後集計など)

実際は配列の何個目だけを取りたいというのは、この界隈の用途ではあまり発生しないかもしれない。

ひとまずJavaの配列なのは配列のようなので、次のように数を数えてみる。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "List l = new ArrayList(doc['goals']); return l.stream().count();"
        }
      }
    }
  }
}

→ 各ドキュメントのgoalsの要素数がscoreとして設定。

合計はこちら。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "List l = new ArrayList(doc['goals']); return l.stream().map(x -> x).mapToLong(x -> Long.valueOf(x)).sum();"
        }
      }
    }
  }
}

→ 各ドキュメントのgoalsの合計値がscoreとして設定。

forループなど

ここまで式っぽい書き方を志向していたが、文というかforループなども書けるし、(これはJavaがどうかというより、クエリがJSON形式なのでというところだが)ヒアドキュメント風に改行を入れてインデントなども行って書ける。

https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-examples.html#_accessing_doc_values_from_painless

文字列操作

前項では諦めたが、配列の要素の並びをインデックス時の値のまま取りたいことはそう無いと言ったものじ実はぼちぼちある。

そこでここでは、文字列のX桁目を抜き出すというやり方で代用する。

さて、文字列操作の関数などは使えるだろうか。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "source": "def x = doc['rand.keyword'].value; return Integer.parseInt( x.substring(8,9) + '' +  x.substring(7,8))"
        }
      }
    }
  }
}"hits": [
      {
        "_index": "hockey",
        "_type": "_doc",
        "_id": "12",
        "_score": 98,
        "_source": {
          "first": "太郎",
          "last": "山田",
          "number": 18,
          "rand": "1234567890",
          "goals": [
            7,
            54,
            26
          ],
          "assists": [
            11,
            26,
            13
          ],
          "gp": [
            26,
            82,
            82
          ],
          "born": "1994/10/12"
        }

使えた!

scoreに「1234567890」の9と8が入った。

※ただしこれはmissingデータ処理を考慮していないので上記以外の文書の分のエラーは出ます。

この記事では以上。