前回の続き。
なんだか時間が経つうちにどんどん収集対象が増えてしまった。
新しく Table や Chart.js でグラフを追加してみたが、こういったものをボタンポチポチで簡単に出力できる Kibana や Grafana は本当に便利なのだなと感じた。
今日は Elasticsearch のデータを JavaScript でグラフにしたときのことをメモっておく。
Kibana でグラフを作ってみる
まずは Kibana で作りたいグラフのイメージを固める。
グラフを作成するときに X-Axis(Date Histogram) と Split Series のどちらを前にするかで結果が変わってくる。
Split Series を優先した場合
最初にアイテムを分割し、それらを時系列で並べる。最初に抽出されたアイテムがその時間に通った順位になるので空きが出来ることがあるが、アイテムごとの最初から最後まで状態を追うことが出来る。アイテム数は「分割数」になる。
X-Axis を優先した場合
ある時間帯のなかで分割をするので順位に空きが出来ることは無いが、分割の範囲外に出てしまったものは線が切れてしまうことがある。時間帯ごとに分割をするのでアイテム数は「分割数 + α」になることがある。
Elasticsearch のクエリを書いてみる
Split Series を優先した場合のクエリを書いてみるが、慣れないうちは少し大変かもしれない。
例えばこんな感じでデータが入っているとする。
{ "@timestamp": "2019-02-25T00:00:00+09:00", "book": { "name": "五等分の花嫁", "rank": 1 } } { "@timestamp": "2019-02-25T01:00:00+09:00", "book": { "name": "五等分の花嫁", "rank": 2 } }
今回は書籍名 book.name
を基点に順位の推移を出していく。
まず、terms
で書籍名 book.name
を抽出して、それを date_histogram
で @timestamp
ごとに分割する。更にそこから min
で book.rank
の値を取得する。取得したいフィールドを同じレベルで拾うのか、ネストした aggs
以下で拾うのかによって意味が変わるのだが最初のうちはなかなか慣れない。
stats
というのは数値型のフィールドに対して使えるもので、min
、avg
、max
、sum
、count
を同時に取得することができる。これを上位の terms
から参照させることでソートキーとして使えるようになる。
Terms … ❶ | +-- Stats … ❷ | +-- Date Histogram … ❸ | +-- Min … ❹
データの取得と整形は Python でやっているので Python 用のコードをそのまま載せておくが、JSON もだいたい同じ。ポイントとしては terms
などの集計の種類と更にネストする場合の aggs
は同じレベルであること。今回は book.name
を基点にしているが、基点は複数指定することもできるので1回のクエリで複数の異なる集計結果を得ることも出来るはず。
query = { 'from': 0, 'size': 0, 'query': { 'query_string': { 'query': '抽出条件', } }, 'aggs': { '名前❶': { 'terms': { 'field': 'book.name.keyword', 'order': [ { '名前❷.min': 'asc' }, { '名前❷.avg': 'asc' }, { '名前❷.max': 'asc' }, ], 'size': 25, }, 'aggs': { '名前❷': { 'stats': { 'field': 'book.rank', } }, '名前❸': { 'date_histogram': { 'field': '@timestamp', 'interval': 'hour', }, 'aggs': { '名前❹': { 'min': { 'field': 'book.rank', } } } } } } } }
抽出結果
{ "took" : 82, "timed_out" : false, "_shards" : { "total" : 6, "successful" : 6, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 200, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "data" : { "doc_count_error_upper_bound" : -1, "sum_other_doc_count" : 196, "buckets" : [ { "key" : "薬屋のひとりごと 4巻 (デジタル版ビッグガンガンコミックス)", "doc_count" : 2, "histogram" : { "buckets" : [ { "key_as_string" : "2019-02-25T09:00:00.000Z", "key" : 1551085200000, "doc_count" : 1, "rank" : { "value" : 1.0 } }, { "key_as_string" : "2019-02-25T10:00:00.000Z", "key" : 1551088800000, "doc_count" : 1, "rank" : { "value" : 1.0 } } ] }, "rank" : { "count" : 2, "min" : 1.0, "max" : 1.0, "avg" : 1.0, "sum" : 2.0 } }, { "key" : "たとえばラストダンジョン前の村の少年が序盤の街で暮らすような物語 3巻 (デジタル版ガンガンコミックスONLINE)", "doc_count" : 2, "histogram" : { "buckets" : [ { "key_as_string" : "2019-02-25T09:00:00.000Z", "key" : 1551085200000, "doc_count" : 1, "rank" : { "value" : 2.0 } }, { "key_as_string" : "2019-02-25T10:00:00.000Z", "key" : 1551088800000, "doc_count" : 1, "rank" : { "value" : 2.0 } } ] }, "rank" : { "count" : 2, "min" : 2.0, "max" : 2.0, "avg" : 2.0, "sum" : 4.0 } } ] } } }
Chart.js 用に整形する
抽出したデータを今度は Chart.js 用に整形していく。Chart.js の書式は公式のサンプルでは下記のようになっている。
Chart.js Example
var ctx = document.getElementById('myChart').getContext('2d'); var chart = new Chart(ctx, { // The type of chart we want to create type: 'line', // The data for our dataset data: { labels: ["January", "February", "March", "April", "May", "June", "July"], datasets: [{ label: "My First dataset", backgroundColor: 'rgb(255, 99, 132)', borderColor: 'rgb(255, 99, 132)', data: [0, 10, 5, 2, 20, 30, 45], }] }, // Configuration options go here options: {} });
datasets
の指定方法は値を配列で与える方法と xy で指定する方法があるが、ポイントが決まっているので xy の方を採用する。
Chart.js Example
https://www.chartjs.org/docs/latest/charts/line.html#point
data: [{ x: 10, y: 20 }, { x: 15, y: 10 }]
Elasticsearch で取り出したデータをそのまま Python で整形する。Date Histogram にはタイムスタンプが格納された key
とタイムゾーン情報付きの key_as_string
があり、タイムスタンプはミリ秒まで入っているので 1000 で割る。クエリの段階で date_histogram.format
で key_as_string
の書式を指定しておくのもいいかもしれない。詳しくは https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html を参照。線色は特に思いつかなかったのでランダムにする。
response = es.search(index=index, body=query) labels = [] datasets = [] for top_bucket in response['aggregations']['data']['buckets']: d = { 'label': top_bucket['key'], 'data': [ { 'x': datetime.fromtimestamp(x['key']/1000).strftime('%Y-%m-%d %H:%M'), 'y': x['rank']['value'], } for x in top_bucket['histogram']['buckets'] ], 'borderColor': 'rgba({}, {}, {}, 0.8)'.format(*[int(uniform(128, 255)) for i in range(3)]), 'borderWidth': 2, 'fill': False, } labels += [x['x'] for x in d['data']] datasets.append(d) print(json.dumps({ 'labels': sorted(list(set(labels))), 'datasets': datasets, }, ensure_ascii=False)
整形後はこんな感じになる。labels
とか label
とか、慣れないうちは困惑するが、labels
は横軸の名称で datasets
の中の label
は凡例。
{ "datasets": [ { "borderColor": "rgba(241, 233, 170, 0.8)", "borderWidth": 2, "data": [ { "x": "2019-02-25 21:00", "y": 2 }, { "x": "2019-02-25 22:00", "y": 2 } ], "fill": false, "label": "たとえばラストダンジョン前の村の少年が序盤の街で暮らすような物語 3巻 (デジタル版ガンガンコミックスONLINE)" } ], "labels": [ "2019-02-25 21:00", "2019-02-25 22:00" ] }
Bucket Aggregation 難しい…🤔