小さなエンドウ豆

まだまだいろいろ勉強中

ElasticSearch と Kibana で位置情報(geo_point)を扱う

ElasticSearch と Kibana で位置情報(geo_point)を扱う

前に MySQL 8 で GIS 関数を使ってみたが機能によってはインデックスが無効になってしまうものがあるためパフォーマンスの面で懸念をしました。

h-piiice16.hatenablog.com

これらを解消すべく僕の中で白羽の矢が立ったのが ElasticSearch(ES)であります。

今回は ElasticSearch と Kibana を使って MySQL の記事みたく半径 1 km 圏内のマクドナルドを絞るクエリを書いてみようと思います。

ElasticSearch と Kibana

どちらも Elastic 社によって開発されたデータ分析用の OSS です。

ElasticSeach

分散型でオープンソースな検索・分析エンジンです。

と公式サイトに説明がありますが、扱えるデータとしてテキストや数値以外にも地理空間情報なども扱うことができます。
特徴としてはシンプルな REST API になっており開発者からすると非常に扱いやすい点と、
クラスタ構成が前提になっているためスケーラブルな点が挙げられます。

ElasticSearch を使うには以下の用語の理解が必要です。

  • ノード ... 一つのサーバーに該当
  • クラスタ ... ノードの集合体でトラフィックが増加した際にノードを増やすことで負荷分散することができる
  • インデックス ... RGB でいうデータベースのこと
  • タイプ ... テーブルのこと
  • ドキュメント ... 行のこと

Kibana

ES にためたデータを可視化するためのツールです。

環境構築

ES, Kibana それぞれ docker イメージが存在するのでそれらを使って環境構築していきます。
以下が docker-compose.yml です。

version: "3.3"

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.6.1
    environment:
      - discovery.type=single-node
      - cluster.name=docker-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9200:9200
    volumes:
      - es-data:/usr/share/elasticsearch/data
  kibana:
    image: docker.elastic.co/kibana/kibana:7.6.1
    ports:
      - 5601:5601

volumes:
  es-data:
    driver: local

ES のコンテナを複数に増やすとクラスタ構成を組むことが可能なようですが今回はスキップします。

ES 上の操作

構築した ES にインデックスを登録します。
インデックスの登録も RESTful なエンドポイントで提供されています。

www.elastic.co

curl -H "Content-Type: application/json" -XPUT 'http://localhost:9200/mc' -d @mc.json

mc というのが index 名です。
mc.jsonマッピングといって RDB でいうスキーマのようなものです。 以下がその内容です。

{
  "mappings": {
    "properties": {
      "id": {
        "type": "integer"
      },
      "name": {
        "type": "text"
      },
      "address": {
        "type": "text"
      },
      "latidude": {
        "type": "double"
      },
      "longitude": {
        "type": "double"
      },
      "geo_location": {
        "type": "geo_point"
      }
    }
  }
}

geo_location というフィールドを用意し位置情報を管理します。

データの作成

データは前回も用いたマクドナルドの位置情報を使用しました。
マクドルドの情報を以下の用に整形します。

{"index":{}}
{"name":"西町店","address":"北海道札幌市西区西町北2-1-6","latitude":43.0773043276,"longitude":141.292534094,"geo_location":[141.292534094,43.0773043276]}

index には先程登録したインデックスの情報を書くのだがエンドポイントで指定が可能なのでここは空にします。
geo_point 型の値の書き方は以下のパターンが有ります。

www.elastic.co

今回は array 方式でいきました。
注意として緯度経度の順番が各方法で違うことが挙げられます。

データが揃ったら以下のエンドポイントを叩き登録していきます。

curl -H "Content-Type: application/json" -XPOST 'http://localhost:9200/mc/_bulk' -d @result.json

_bulk はデータを複数一括で登録するための機能となります。
result.json には全国のマクドナルドの位置情報が上記のフォーマットで記されています。

途中間違えたとなったらインデックスごと消すと良いでしょう。

curl -XDELETE 'localhost:9200/mc'

半径 1 km 圏内にある店舗

以下のようなエンドポイントで出すことができました。

curl -H "Content-Type: application/json" -XPOST 'http://localhost:9200/mc/_search' --data-binary @query.json

query.json の中身

{
  "query": {
    "geo_distance": {
      "distance": "1km",
      "geo_location": {
        "lat": 35.6812362,
        "lon": 139.764936
      }
    }
  }
}

結果

{"took":18,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":4,"relation":"eq"},"max_score":1.0,"hits":[{"_index":"mc","_type":"_doc","_id":"yb9MAHEB7mQhkr5uWG6K","_score":1.0,"_source":{"name":"丸の内国際ビルヂング店","address":"東京都千代田区丸の内3-1-1","latitude":35.6770328213,"longitude":139.761160999,"geo_location":[139.761160999,35.6770328213]}},{"_index":"mc","_type":"_doc","_id":"yr9MAHEB7mQhkr5uWG6K","_score":1.0,"_source":{"name":"有楽町ビルヂング店","address":"東京都千代田区有楽町1-10-1","latitude":35.6750116772,"longitude":139.761805747,"geo_location":[139.761805747,35.6750116772]}},{"_index":"mc","_type":"_doc","_id":"HL9MAHEB7mQhkr5uWG-M","_score":1.0,"_source":{"name":"銀座インズ店","address":"東京都中央区銀座西1-2銀座インズ3内","latitude":35.6753447943,"longitude":139.765509864,"geo_location":[139.765509864,35.6753447943]}},{"_index":"mc","_type":"_doc","_id":"N79MAHEB7mQhkr5uWG-M","_score":1.0,"_source":{"name":"JR東京駅店","address":"東京都千代田区丸の内1-9-1東京駅一番街","latitude":35.6797146357,"longitude":139.767587012,"geo_location":[139.767587012,35.6797146357]}}]}}

MySQL のときと結果が一緒になっています。

四方のエリアで集計

ES では位置情報を集計することができます。
下記を参考に 5km 四方の四角内にどれだけの数のマクドナルドがあるか集計します。

curl -H "Content-Type: application/json" -XPOST 'http://localhost:9200/mc/_search?pretty' --data-binary @aggregate.json

aggregate.json の中身

{
  "size": 0,
  "aggs": {
    "1": {
      "geohash_grid": {
        "field": "geo_location",
        "precision": 5
      },
      "aggs": {
        "2": {
          "geo_bounds": {
            "field": "geo_location"
          }
        }
      }
    }
  }
}

ちなみに密集していた地域は大阪の動物園前付近のエリアでした。

f:id:h-piiice16:20200329084934p:plain

kibana による可視化

先程の docker-compose.yml を元にサービスを立ち上げると以下の URL でマップを見ることができます。 http://localhost:5601/app/maps

まず Create Map を押すと以下のような地図が表示されます。

f:id:h-piiice16:20200329085943p:plain

次に Add Layer を押し先程登録したインデックスを指定します。

f:id:h-piiice16:20200329090025p:plain

すると表示されます。

f:id:h-piiice16:20200329090052p:plain

上部にある検索バー?みたいなところに Kibana 特有のクエリを書くと表示する店舗を絞ることもできます。
例えばフォッサマグナ以東で佐渡ヶ島以南のマクドナルドを調べたいときは以下のようなクエリを入力します。

longitude >= 138.23553 and latitude <= 37.0670589

f:id:h-piiice16:20200329090312p:plain

できてそうですね!

まとめ

ElasticSearch で位置情報を取り扱うことができました。
クラスタマッピングについて詳しく学習するとパフォーマンスの面で目覚ましいことがわかるかもしれないです。
次回勉強してみようと思います。

また今回 map の部分しか使わなかった kibana ですが BI ツールとしても使えそうなのでその他の機能も深ぼっていきたいところです。