ようへいの日々精進XP

よかろうもん

Elasticsearch でフィールド同士の差分で検索したい (例えば, 開始から終了の経過時間で検索したいとか)

tl;dr

分かりづらいタイトルですいませぬ.

以下のようなデータを Elasticsearch に突っ込んでいたとして, startend の差分 (経過時間) を算出して, その結果から N 秒以上とか, N 秒以上, N 秒以下を検索条件として利用する方法を検討したのでメモっておく.

PUT /sample1/sample/1
{
  "title" : "test1",
  "start": "2018-07-30T12:00:00+09:00",
  "end": "2018-07-30T12:03:00+09:00",
}

PUT /sample1/sample/2
{
  "title" : "test2",
  "start": "2018-07-30T12:10:00+09:00",
  "end": "2018-07-30T12:15:00+09:00",
}

PUT /sample1/sample/3
{
  "title" : "test3",
  "start": "2018-07-30T13:10:00+09:00",
  "end": "2018-07-30T13:45:00+09:00",
}

PUT /sample1/sample/4
{
  "title" : "test4",
  "start": "2018-07-30T14:10:00+09:00",
  "end": "2018-07-30T15:10:00+09:00",
}

PUT /sample1/sample/5
{
  "title" : "test5",
  "start": "2018-07-30T14:10:00+09:00",
  "end": "2018-07-30T14:11:00+09:00",
}

尚, 検証に利用した Elasticsearch 環境は以下の通りで, Kibana のバージョンは 6.2.4 を利用している.

{
  "name": "UTIlghF",
  "cluster_name": "docker-cluster",
  "cluster_uuid": "FtmlW5cTRdidMnQZQ9sOuA",
  "version": {
    "number": "6.2.2",
    "build_hash": "10b1edd",
    "build_date": "2018-02-16T19:01:30.685723Z",
    "build_snapshot": false,
    "lucene_version": "7.2.1",
    "minimum_wire_compatibility_version": "5.6.0",
    "minimum_index_compatibility_version": "5.0.0"
  },
  "tagline": "You Know, for Search"
}

また, 全ての Elasticsearch クエリは Kibana の Dev Tools コンソールから実行することを前提としている. Dev Tools マジ便利.

確認

ドキュメントを確認

登録しているドキュメントを確認する.

GET sample1/_search

以下のように登録されている.

{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 5,
    "max_score": 1,
    "hits": [
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "5",
        "_score": 1,
        "_source": {
          "title": "test4",
          "start": "2018-07-30T14:10:00+09:00",
          "end": "2018-07-30T14:11:00+09:00"
        }
      },
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "2",
        "_score": 1,
        "_source": {
          "title": "test2",
          "start": "2018-07-30T12:10:00+09:00",
          "end": "2018-07-30T12:15:00+09:00"
        }
      },
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "4",
        "_score": 1,
        "_source": {
          "title": "test4",
          "start": "2018-07-30T14:10:00+09:00",
          "end": "2018-07-30T15:10:00+09:00"
        }
      },
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "test1",
          "start": "2018-07-30T12:00:00+09:00",
          "end": "2018-07-30T12:03:00+09:00"
        }
      },
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "3",
        "_score": 1,
        "_source": {
          "title": "test3",
          "start": "2018-07-30T13:10:00+09:00",
          "end": "2018-07-30T13:45:00+09:00"
        }
      }
    ]
  }
}

マッピングを確認

インデックスのマッピングを確認する.

GET sample1/_mapping

今回は特にテンプレートは定義していないが, 以下のようなマッピングとなっている.

{
  "sample1": {
    "mappings": {
      "sample": {
        "properties": {
          "end": {
            "type": "date"
          },
          "start": {
            "type": "date"
          },
          "title": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

startend はそれぞれ Date データタイプで登録されているのでありがたい.

どうしたか (Script Query を利用する)

要件 (やりたいこと)

  • 各ドキュメントで経過時間(startend の差分) を取得する
  • 経過時間 N 秒以上, N 秒以下という検索条件でドキュメントを検索出来るようにしたい

Script Query を利用した.

Script Query を利用した.

www.elastic.co

以下のような Script Query を考えてみた. end から start の差分が 300 以上のドキュメントを検索するクエリとなる.

GET /sample1/_search
{
    "query": {
        "bool" : {
            "must" : [
                {
                    "script" : {
                        "script" : {
                            "lang": "painless",
                            "source": "(doc['end'].date.getMillis()/1000 - doc['start'].date.getMillis()/1000) > 300"
                        }
                    }
                }
            ]
        }
    }
}

doc['end'].date.getMillis()/1000 及び doc['start'].date.getMillis()/1000 は, 以下のような挙動となる.

  1. doc['end']doc['start'] でドキュメントのフィールドにアクセスしている
  2. .date.getMillis() でフィールドの値 (Date Type) を epoch タイムに変換して, さらに /1000 で 1000 で割って秒に変換
  3. end から start を差し引いた時間 (経過時間) を比較する (> 300)

検索してみると, 以下のような結果が返ってくる.

{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 1,
    "hits": [
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "4",
        "_score": 1,
        "_source": {
          "title": "test4",
          "start": "2018-07-30T14:10:00+09:00",
          "end": "2018-07-30T15:10:00+09:00"
        }
      },
      {
        "_index": "sample1",
        "_type": "sample",
        "_id": "3",
        "_score": 1,
        "_source": {
          "title": "test3",
          "start": "2018-07-30T13:10:00+09:00",
          "end": "2018-07-30T13:45:00+09:00"
        }
      }
    ]
  }
}

いい感じ.

例えば, title: test3 の場合には 2018-07-30T13:45:00+09:00 から 2018-07-30T13:10:00+09:00 を差し引いた時間は 35 分 (1500 秒) となり, 300 秒よりも長い為, 対象のドキュメントが検索結果として出力されている.

Script Query

前後してしまうが, 簡単に Script Query について触れる. 内容はドキュメントからの抜粋となる為, オフィシャルな情報が欲しい場合にはドキュメントを読みましょう. Elasticsearch のドキュメントは英語ということを除けば (汗) 細かく書かれていると思うし, 日本語で質問が投げられる disscuss.elastic.co というサイトもあるので, 何かあればこのサイトを利用すれば良いんだなって, 今更ながらに思った次第.

で, Script Query とは

www.elastic.co

Elasticsearch には, 以前のバージョンから検索クエリにスクリプトを埋め込むことが出来ていた. 以前のバージョンでは, Groovy という JVM 上で動作する言語が利用されていたが, 自分は実際に使ったことがないので, 使い勝手等については言及出来ない. ちなみに, elasticsearch scriptググると Groovy による実装についての記事がちらほら検索結果の上位に現れてくるので, 注意が必要だと思う.

で, 今回は検証に使っているのは Elasticsearch 6.2.2 となるので, Groovy ではなく, Painless Script Language というスクリプト言語を利用して記述することになる. 尚, ドキュメントによると, Painless Script Language がデフォルトで利用可能で, lang プラグインを有効にすることで, 以下のような言語を利用することが可能とのこと.

  • expression
  • mustache
  • java

尚, これらの言語での実装は柔軟性に欠けるものの, 特定のタスクにおいては, 高いパフォーマンスを発揮するらしい.

Painless Script Language

Painless Script Language について, ドキュメントを確認したところ, Painless Script Language について言及されているのは, Elasticsearch 5.0 からである為, Painless Script Language がサポートされたのは Elasticsearch 5.0 からだと推測される.

www.elastic.co

Painless Scripting Language については, 以下のような特徴がある. (上記のドキュメントをざっくり意訳して抜粋)

  • 高いパフォーマンス
  • 安全性 (Fine-grained whitelist with method call/field granularity がよく理解出来なかった)
  • 変数とパラメータは明示的又は動的な型が利用可能
  • Groovy スタイルのスクリプト言語機能
  • Elasticsearch に最適化

実装にあたっては, 以下の example を参考にしながら実装すると良さそう.

www.elastic.co

再掲, Script Query とは

改めて...

Script Query とは Elasticsearch のクエリに Painless Script Language というスクリプト言語で書いたスクリプトを埋め込んだもので, 以下のようなクエリとなる (以下のクエリはドキュメントより引用).

GET /_search
{
    "query": {
        "bool" : {
            "must" : {
                "script" : {
                    "script" : {
                        "source": "doc['num1'].value > 1",
                        "lang": "painless"
                     }
                }
            }
        }
    }
}

ドキュメントの値へのアクセスは doc という名前のマップを利用する. また, パラメータを渡す場合には, 以下のように params キーを利用する. 尚, params キーには複数のパラメータを渡すことも出来る.

GET /_search
{
    "query": {
        "bool" : {
            "must" : {
                "script" : {
                    "script" : {
                        "source" : "doc['num1'].value > params.param1",
                        "lang"   : "painless",
                        "params" : {
                            "param1" : 5
                        }
                    }
                }
            }
        }
    }
}

Script Query を利用することで, 今回のようなフィールド間の計算等のより柔軟なクエリを利用することが出来ると思う.

ちなみに

Range Aggregation

Aggregation と Script を組み合わせることで, N 秒以上は N 件, N 秒以上, N 秒以下は N 件のような集計も可能となる.

GET /sample1/_search
{
    "aggs" : {
        "time_ranges" : {
            "range" : {
                "script" : {
                    "lang": "painless",
                    "source": "doc[\"end\"].date.getMillis()/1000 - doc[\"start\"].date.getMillis()/1000"
                },
                "ranges" : [
                    { "from" : 0, "to" : 100 },
                    { "from" : 101, "to" : 500 },
                    { "from" : 501, "to" : 7200 }
                ]
            }
        }
    }
}

以下のように出力される.

{
... 略 ...
  "aggregations": {
    "time_ranges": {
      "buckets": [
        {
          "key": "0.0-100.0",
          "from": 0,
          "to": 100,
          "doc_count": 1
        },
        {
          "key": "101.0-500.0",
          "from": 101,
          "to": 500,
          "doc_count": 2
        },
        {
          "key": "501.0-7200.0",
          "from": 501,
          "to": 7200,
          "doc_count": 2
        }
      ]
    }
  }
}

フムフム.

以上

かなりざっくりとだけど, Script Query や Script を触れてみた. Script を多用した場合の検索パフォーマンス等, 気になるところはあるが, 用法用量を守って Script をうまく使うことで, Elasticsearch をより便利に利用出来るようになると思う.

Elasticsearch 楽しい.