Embulk + Elasticsearch + kibana + OpenStreetMapでお手軽に地理情報を可視化

# はじめに

「地理情報をまとめて地図上にプロットしたやつを見たい」ときに、お手軽にやっつける方法です。
Embulk + Elasticsearch(以下ES)の備忘録を兼ねて。

# 事前準備

## 入力データ

プロット対象となるデータを用意します。

今回は[「AEDオープンデータプラットフォーム」](http://hatsunejournal.jp/w8/AEDOpendata/)から、東京都におけるAEDについてのデータを使わせていただくことにしました。

WebAPIを毎回叩いてしまうのは気が引けるので、ローカルにデータを保存しておきます。

```
// "https://aed.azure-mobile.net/api/aedinfo/東京都/" を取得
$ curl https://aed.azure-mobile.net/api/aedinfo/%E6%9D%B1%E4%BA%AC%E9%83%BD/ -o aedinfo_tokyo.json
```

## Elasticsearch + kibana インストール

2018/01時点では6.x系の情報が少なかったので、5.6系を使います。
今回はめんどくさかったのでbrewで入れていますが、Docker使ってもいいと思います。お好みで。

```
$ brew install elasticsearch@5.6 kibana@5.6

$ brew services start elasticsearch@5.6
$ brew services start kibana@5.6
```

http://localhost:5601/  にアクセスして起動を確認しておきます。

<img width="1354" alt="スクリーンショット 2018-01-06 15.28.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/fa98adb0-94f1-9f34-1771-b281a2df6723.png">


## Embulk インストール

こちらもbrewで。

```
$ brew install embulk
$ embulk --version
embulk 0.8.30
```

今回利用するプラグインも合わせて入れておきます。

```
// in(JSON), out(ES)
$ embulk gem install embulk-parser-jsonpath embulk-output-elasticsearch

// filters
$ embulk gem install embulk-filter-insert embulk-filter-ruby_proc embulk-filter-column
```

これで準備完了です。

# スキーマ定義、データ投入

早速Embulkのconfig.ymlを書いていきたいところですが、ESで地理情報を扱うgeo_point形式にマッピングするには事前のスキーマ定義(template定義)[^2]が必要となるので、まずはこちらを作成します。

[^2]: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html

今回は以下のようなスキーマを定義しました。シンプルに名称、住所、地理情報の3フィールドです。

```schema.json
{
  "template": "aedmap*",
  "mappings": {
    "aed_schema": {
      "properties": {
        "LocationName": {
          "type": "string"
        },
        "address": {
          "type": "string"
        },
        "location": {
          "type": "geo_point"
        }
      }
    }
  }
}
```

ESに登録します。

```
$ curl -XPUT localhost:9200/_template/aedmap -d @schema.json
{"acknowledged":true}
```

これで、"aedmap*"にマッチするindex名に対し、"aed_schema"が有効になります。


# config.yml作成

`in:`でjsonから取得した項目を`filters:`で変換しつつ、`out:`でESへと投入します。
`filters:`の変換ルールは以下の通りです。

* `location`, `address` フィールドを新規追加(embulk-filter-insert)
* フォーマット変換(embulk-filter-ruby_proc)
  * JSONの`Latitude`と`Longitude`をgeo_point書式[^3]に文字列フォーマット、ESの`location`フィールドに変換
  * JSONの`Prefecture`と`City`と`AddressAres`を文字列連結、ESの`address`フィールドに変換
* 出力対象フィールド指定(embulk-filter-column)

[^3]: [Geo\-point datatype \| Elasticsearch Reference \[6\.1\] \| Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html)


```config.yml
in:
  type: file
  path_prefix: ./aedinfo_tokyo.json
  parser:
    charset: UTF-8
    newline: LF
    type: jsonpath
    columns:
    - {name: Id, type: long}
    - {name: LocationName, type: string}
    - {name: Perfecture, type: string}
    - {name: City, type: string}
    - {name: AddressArea, type: string}
    - {name: Latitude, type: double}
    - {name: Longitude, type: double}

filters:
  - type: insert
    columns:
      - location: ''
      - address: ''
  - type: ruby_proc
    columns:
      - name: location
        proc: |
          ->(_,record) do
            return record['Latitude'].to_s + "," + record['Longitude'].to_s
          end
        skip_nil: false
        type: string
      - name: address
        proc: |
          ->(_,record) do
            return record['Perfecture'].to_s + record['City'].to_s + record['AddressArea'].to_s
          end
        skip_nil: false
        type: string
  - type: column
    columns:
      - {name: LocationName, type: string}
      - {name: address, type: string}
      - {name: location, type: string}

#out: {type: stdout}
out:
  type: elasticsearch
  index: aedmap-tokyo-index
  index_type: aed_schema
  nodes:
  - {host: 127.0.0.1}
````

実際のデータを利用したサンプルを以下に示します。

```JSON:in(JSON)
{
  "Id": 68655,
  "LocationName": "みずほ銀行　東青梅支店",
  "Perfecture": "東京都",
  "City": "青梅市",
  "AddressArea": "東青梅2-13-1",
  "Latitude": 35.7902422,
  "Longitude": 139.2730937,
  "FacilityId": null,
  "FacilityName": null,
  "FacilityPlace": null,
  "ScheduleDayType": null,
  "ScheduleDayStartTime": null,
  "ScheduleDayEndTime": null,
  "AccessAvailabilityOfPad": null,
  "FacilityUser": null,
  "PhotoOfAedUrl": null,
  "Url": null,
  "FacilityOwner": null,
  "FacilityOperater": null,
  "ContactPoint": null,
  "ContactTelephone": null,
  "ContactExtension": null,
  "FacilityNote": null,
  "TypeOfPad": null,
  "ExpiryDate": null,
  "ExpiryDateOfPads": null,
  "ExpiryDateOfBatteries": null,
  "TypeOfDefibrillator": null,
  "ModelNumber": null,
  "SerialNumber": null,
  "Source": "http://shop.www.mizuhobank.co.jp/b/mizuho/",
  "VenueId": null,
  "DateOfUpdatingInformation": "2014-09-29T21:44:14.840Z"
}
```

```JSON:out(Elasticsearch):
{
  "LocationName": "みずほ銀行　東青梅支店",
  "address": "東京都青梅市東青梅2-13-1",
  "location": "35.7902422,139.2730937"
}
```

`out: {type: stdout}`を使って動作確認後、問題なさそうならESに投入します。

```
$ embulk run config.yml
2018-01-10 14:13:03.181 +0900: Embulk v0.8.30
2018-01-10 14:13:09.413 +0900 [INFO] (0001:transaction): Loaded plugin embulk-output-elasticsearch (0.4.5)
2018-01-10 14:13:09.462 +0900 [INFO] (0001:transaction): Loaded plugin embulk-filter-insert (1.1.1)
2018-01-10 14:13:09.572 +0900 [INFO] (0001:transaction): Loaded plugin embulk-filter-ruby_proc (0.7.0)
2018-01-10 14:13:09.620 +0900 [INFO] (0001:transaction): Loaded plugin embulk-filter-column (0.7.1)
2018-01-10 14:13:09.752 +0900 [INFO] (0001:transaction): Loaded plugin embulk-parser-jsonpath (0.2.0)
2018-01-10 14:13:09.783 +0900 [INFO] (0001:transaction): Listing local files at directory '.' filtering filename by prefix 'aedinfo_tokyo.json'
2018-01-10 14:13:09.785 +0900 [INFO] (0001:transaction): "follow_symlinks" is set false. Note that symbolic links to directories are skipped.
2018-01-10 14:13:09.790 +0900 [INFO] (0001:transaction): Loading files [aedinfo_tokyo.json]
2018-01-10 14:13:09.949 +0900 [INFO] (0001:transaction): Using local thread executor with max_threads=8 / output tasks 4 = input tasks 1 * 4
2018-01-10 14:13:10.001 +0900 [INFO] (0001:transaction): Logging initialized @7060ms
2018-01-10 14:13:10.447 +0900 [INFO] (0001:transaction): Connecting to Elasticsearch version:5.6.5
2018-01-10 14:13:10.448 +0900 [INFO] (0001:transaction): Executing plugin with 'insert' mode.
2018-01-10 14:13:10.448 +0900 [INFO] (0001:transaction): Inserting data into index[aedmap-tokyo-index]
2018-01-10 14:13:10.539 +0900 [INFO] (0001:transaction): {done:  0 / 1, running: 0}
2018-01-10 14:13:10.697 +0900 [INFO] (0023:task-0000): JSONPath = $
2018-01-10 14:13:12.642 +0900 [INFO] (0023:task-0000): Inserted 954 records
2018-01-10 14:13:13.021 +0900 [INFO] (0023:task-0000): Inserted 851 records
2018-01-10 14:13:13.319 +0900 [INFO] (0023:task-0000): Inserted 852 records
2018-01-10 14:13:13.597 +0900 [INFO] (0023:task-0000): Inserted 860 records
2018-01-10 14:13:13.598 +0900 [INFO] (0001:transaction): {done:  1 / 1, running: 0}
2018-01-10 14:13:13.599 +0900 [INFO] (0001:transaction): Insert completed. 3517 records
2018-01-10 14:13:13.606 +0900 [INFO] (main): Committed.
2018-01-10 14:13:13.607 +0900 [INFO] (main): Next config diff: {"in":{"last_path":"aedinfo_tokyo.json"},"out":{}}
```

kibana経由できちんと地理情報が確認できればOK。記事作成時点では3517件のデータが投入されました。

<img width="1358" alt="スクリーンショット 2018-01-10 14.15.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/e53b9597-6ca0-e133-3269-0b0af83ecbd5.png">

<img width="1359" alt="スクリーンショット 2018-01-10 14.15.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/d1cc394b-0cb1-9967-817c-d5113a129c12.png">

<img width="1350" alt="スクリーンショット 2018-01-10 14.17.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/ac0ca9b5-e142-6040-0d2f-7a80a6d0f4a3.png">

# 地図表示(kibana + OpenStreetMap)

## Coodinate Map 設定

Visualize -> Create a Visualization -> Coodinate Map を選択。

<img width="1325" alt="スクリーンショット 2018-01-10 14.27.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/15ab5600-f5b3-70c9-dac7-1cb11241f8eb.png">

Filedに地理情報を保持するフィールド(今回は`location`)を選びます。▶ボタンを押して、データが地図上にプロットされていればOKです。

<img width="1360" alt="スクリーンショット 2018-01-10 14.32.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/0553132b-4e48-7953-6f08-7a43b35bf255.png">

## OpenStreetMap TileServer設定

これでもそれなりにいい感じですが、もう少しズームしたい場合や、地図表記は日本語がいい場合もあるでしょう。kibanaのconfigファイルを編集して対応します。

```:/usr/local/Cellar/kibana@5.6/5.6.5/config/kibana.yml
(追記)
tilemap.url: "http://tile.openstreetmap.jp/{z}/{x}/{y}.png"
tilemap.options.maxZoom: 18
```

今回はとりあえず動作確認ということで、openstreetmap.jpのTilemapServerをお借りしています。ヘビーに使う場合はTileMapServerを自分で立てるなどしてください。

> 参考: 
> * [OpenStreetMapのタイルレンダリングサーバをお手軽に立ち上げたい \- Qiita](https://qiita.com/mattak/items/2b128c442c6abd15ad60)
> * [OpenStreetMapのタイルサーバを構築する \(CentOS7\) \- Qiita](https://qiita.com/Syn256/items/633aef930455bfd4be0d)

kibanaを再起動します。

```
$ brew services restart kibana@5.6
```

ページを再読込して、さらに細かくズームできるようになっていればOKです。

<img width="1361" alt="スクリーンショット 2018-01-10 14.50.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/e158952c-16b7-7777-7759-8f030bda48e7.png">

あとは「足立区にやたら多いのはなんでだろう…？」「都心部に少ないのは単にデータが登録されてないのか、それともAEDがよく配置されている病院や学校などの公共施設が少ないから…？」という感じで色々推測しつつお楽しみください。

<img width="1430" alt="AED.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/3e78b872-59e6-6fff-2529-22a05a37e83b.png">

# ■おまけ

embulkの汎用性が高いので一度やり方を覚えてしまえばかなり応用が効きます。参考までに。

## 都内コンビニ数(ヒートマップ)

[Sinfonica \- 2011年6月号「フリーソフトによるデータ実践GIS」東京都コンビニエンスストアの緯度経度データのダウンロード](http://www.sinfonica.or.jp/kanko/estrela/refer/s29/index.html) ページで公開されていた [tokyo3.csv](http://www.sinfonica.or.jp/kanko/estrela/refer/s29/tokyo3.csv) をお借りしたものです。Optionからヒートマップを選ぶとこんな感じのプロットもできます。(少々パラメータの追い込みがめんどうですが...)

![スクリーンショット 2018-01-06 14.26.27.png](https://qiita-image-store.s3.amazonaws.com/0/50184/793c3093-edc1-a8b1-3abc-42329baeb04b.png)

やっぱ都心はコンビニ多いよね（小並感）

## 都内IT系(Web系)求人情報数

仕事探しがめんどくさくなって自作した求人情報クローラーがMySQLに放り込んでた案件情報を、逆ジオコーディングしてマッピングしたものです。10サイトくらいから取得したデータを、合計20,000件くらい。

<img width="700" alt="求人.png" src="https://qiita-image-store.s3.amazonaws.com/0/50184/c1b8c894-68d2-8d35-dac3-f087b2a75780.png">

求人の性質上同じ会社が複数回求人を出していたり、最寄り駅や区名レベルでの地理情報しかないものも多々あったりして、データとしての正確性はお察しレベルですが、その辺差し置いてもやっぱり渋谷、新宿あたりは強いってことなんでしょうか。次に住むなら東京の南寄りかなあ。。。


とまあこんな感じで、色々遊んでみてください！

お役に立てば幸いです。

---

参考:

* [elasticsearch \- elastic search converting and inserting lat lon as geo\_point in bulk \- Stack Overflow](https://stackoverflow.com/questions/37787270/elastic-search-converting-and-inserting-lat-lon-as-geo-point-in-bulk)
* [KibanaのTileMapにOpenStreetMapを利用する ｜ Developers\.IO](https://dev.classmethod.jp/server-side/elasticsearch/kibana-openstreetmap/)