Elasticsearchの検索結果をgeohash gridで集計し地図にプロットする

Elasticsearchの検索結果をgeohash gridで集計し地図にプロットする

前回、Graylogのgeolocationをgeo_pointに変換したので、Elasticsearch内のgeolocationを有効活用できるようになりました。
早速使ってみようと思います。

環境情報

Graylogサーバー

・CentOS7
・Graylog v3.0.1
・ElasticSearch 5.5.2-1

クライアント

・MacOS Mojave
・Pytyhon3.6.8
・jupyter notebook5.7.4
・elasticsearch7.0.0
・pandas0.24.1
・folium0.9.1
・python-geohash0.8.5

必要なパッケージをインストール

ここで使用するpythonパッケージは、elasticsearch、pandas、folium、python-geohashです。
全てpipからインストールできます。

Elasticsearchから集計データを取得する

Graylogに取り込んだデータは大量にありますので、その中で絞り込んだデータを集計して使用することにします。

from elasticsearch import Elasticsearch
from datetime import datetime, timedelta
import pandas as pd
import geohash
import folium

es = Elasticsearch("hostname:9200")

# 直近1日分のデータを取得
timespan = 1440
stime = datetime.now() - timedelta(hours=9) - timedelta(minutes=timespan)
etime = str(stime + timedelta(minutes=timespan))[:-3]
stime = str(stime)[:-3]

body = {
    "size":0,
    "query": 
    {
        "bool" :
        {
            "must":
            [
                {
                    "bool" :
                    {
                        "must" :
                        [
                            {"match" : { "Type" : "app-ctrl" }},
                            {"range" : {"timestamp" : {"from" : stime , "to" : etime}}}
                        ]
                    }
                },
                {
                    "bool":
                    {
                        "must_not":
                        [
                            {"match" : { "Action" : "block" }}
                        ]
                    }
                }
            ]
        }
    },
    "aggs":
    {
        "large-grid" : 
        {
            "geohash_grid" :
            {
                "field" : "DestinationIP_geolocation",
                "precision" : 3
            }
        }
    }
}


data = es.search(index="graylog_16", body=body)

display(data)

ここではqueryとaggsの組み合わせでデータを取得しています。
typeがapp-ctrlで、直近1440分(1日)かつ、Actionがblockではないデータを絞り込んでいます。分ベースでやっているのは、もっと短いスパンで取得もしたくなるかもしれないからです。

そして、aggsはgeohash gridというAggregationを使用します。
geohashは文字列が短いほど広範囲になり、長いほど範囲が狭くなります。

例えば、9ydというgeohashは3文字ですが、156km×156.5kmの範囲をカバーしてくれますが、xn7749pjだと8文字となり、38.2m×19mとかなり狭くなります。

ここでは3文字でprecisionを設定します。

出力結果

{'took': 276,
 'timed_out': False,
 '_shards': {'total': 4, 'successful': 4, 'failed': 0},
 'hits': {'total': 995902, 'max_score': 0.0, 'hits': []},
 'aggregations': {'large-gid': {'buckets': [{'key': 'xn7',
     'doc_count': 432618},
    {'key': '9yd', 'doc_count': 264831},
    {'key': '9q9', 'doc_count': 69779},
    {'key': 'w21', 'doc_count': 60051},
    {'key': 'c22', 'doc_count': 43110},
    {'key': 'wec', 'doc_count': 21977},
    {'key': 'c24', 'doc_count': 21957},
    {'key': 'xn0', 'doc_count': 21452},
    {'key': 'dqb', 'doc_count': 9988},
    {'key': '9qh', 'doc_count': 4175},
    {'key': 'dp3', 'doc_count': 3761},
   
       (途中省略)


    {'key': 'sr8', 'doc_count': 1},
    {'key': 'rck', 'doc_count': 1},
    {'key': '6gk', 'doc_count': 1}]}}}

このようにgeohashエリア内に属する件数が集計されて出てきました。

geohashのデータを地図にプロット

これまではGoogle Mapにプロットといったところでしょうが、Google Mapの仕様が変わって有償部分が増えてしまったため、OpenStreetMapを使用します。

pythonからOpenStreetMapにプロットするにはfoliumが便利です。

copyright_osm = '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
fmap = folium.Map(attr=copyright_osm, zoom_start=1)

max_count = data['aggregations']['large-grid']['buckets'][0]['doc_count'] #一番多い件数を透明度算出用の分母とする

def addgeohash(gh, cnt, map):
    
    ret = geohash.bbox(gh)
    folium.vector_layers.Rectangle([[ret['s'], ret['w']], [ret['n'], ret['e']]], popup=gh, tooltip=cnt,
                                   fill_color="red", color="red", fill_opacity=cnt/max_count*0.8).add_to(map)
    
for line in data['aggregations']['large-grid']['buckets']:
    addgeohash(line['key'], line['doc_count'], fmap)

display(fmap)

件数が多いエリアを濃く塗りつぶすため、最初に一番多い件数をmax_countとして変数にセットしておきます。

cnt / max_count * 0.8

で塗りつぶしの濃さを決定します。
0.8をかけているのは、fill_opacityが1.0になってしまうと、真っ赤になってしまい、地図が全く見えなくなってしまうため調整しています。

python3でgeohashを取得できるパッケージは色々とあったのですが、bboxを取得できるpython-geohashが今回の用途には合っていました。
例えば次のようにbboxにgeohashを渡すと

geohash.bbox("9yd")

次のような結果が返ってきます。

{'s': 36.5625, 'w': -98.4375, 'n': 37.96875, 'e': -97.03125}

gethashエリアの南西の緯度経度と北東の緯度経度を求めることができるのです。これを利用してfoliumで四角形をプロットしてやります。

日本を確認してみましょう。

件数が多いエリアは濃く表示されています。また、Tooltipに件数を表示するように設定しているため、マウスカーソルを合わせると、件数が表示されます。

クリックするとgeohashを表示するようにpopupを設定もしてあります。

このようにGraylogのgeolocationをgeo_pointに変換することによって有効活用できるようになりました。