Kibana + Elasticsearch で地図を使いたい




はじめに
------------

**社内で「Elasticsearch ハンズオン」を開催した - kakakakakku blog**
http://kakakakakku.hatenablog.com/entry/2015/09/09/231956
をやってみた。緯度経度のデータが入ってたのでKibanaで地図を使いたくなった。

このハンズオン資料で使用している `livedoor グルメ` データをconvertする処理を少しいじってkibana / Elasticsearch で地図が使えそう。

スクリプト群
------------------------

https://github.com/imura81gt/elasticsearch-hands-on
forkさせていただきました。


なぜ少しいじるのか
------------------------


**Lat/Lon Formats**
https://www.elastic.co/guide/en/elasticsearch/guide/current/lat-lon-formats.html

この３つのフォーマットで地図が使える。

```json:format
PUT /attractions/restaurant/1
{
  "name":     "Chipotle Mexican Grill",
  "location": "40.715, -74.011" 
}

PUT /attractions/restaurant/2
{
  "name":     "Pala Pizza",
  "location": { 
    "lat":     40.722,
    "lon":    -73.989
  }
}

PUT /attractions/restaurant/3
{
  "name":     "Mini Munchies Pizza",
  "location": [ -73.983, 40.719 ] 
}
```

ハンズオンの資料のままやると以下のようにelasticsearchに格納されて
Kibanaの `visualize` - `Tile Map` で `Geohash` として使えなかった。
（使い方を知らないだけかも…）

```json:bulk_restaurants.json
{
  "id": "6",
  (省略)
  "north_latitude": "35.38.43.555",
  "east_longitude": "139.42.59.224",
  (省略)
}  
```


どうやるか
------------------------

(1)コンバートするスクリプトを修正
(2)mappingファイルも修正
(3)再度indexing

(1)コンバートするスクリプトを修正
---------------------------------------------------------------

* 緯度経度を60進数（度分秒）から10進数に変換する必要があった
* 出力フォーマットも `{"pin":{"lat": "111.111", "lon": "222.222"}` 変更


```ruby:convert_bulk_data_lat_lon.rb
require 'csv'
require 'json'

ES_INDEX_NAME = 'gourmet'
ES_TYPE_NAME = 'restaurants'

row_csv = CSV.read('restaurants.csv')
header = row_csv.slice!(0)
data = row_csv

OUTPUT_FILENAME = 'bulk_restaurants_lat_lon.json'

File.delete(OUTPUT_FILENAME) if File.exist?(OUTPUT_FILENAME)
File.open(OUTPUT_FILENAME, 'a') do |file|
  data.each do |row|
    index = { index: { _index: ES_INDEX_NAME, _type: ES_TYPE_NAME, _id: row[0] } }
    file.puts(JSON.dump(index))
    hash = Hash[header.zip(row)]
		lon = ""
		lat = ""
		mod_hash = {}
		hash.each do |key, value|
			if key == "north_latitude" then
				value =~ /([0-9]+)\.([0-9]+)\.(.+)/
				lat = ($1.to_f + ($2.to_f / 60) +  ($3.to_f / 60**2)).to_s
			elsif key == "east_longitude" then
				value =~ /([0-9]+)\.([0-9]+)\.(.+)/
				lon = ($1.to_f + ($2.to_f / 60) +  ($3.to_f / 60**2)).to_s
			else
				mod_hash[key] =  value
			end
		end
		mod_hash["pin"] = {"lat" => lat, "lon" => lon}
    file.puts(JSON.dump(mod_hash))
  end
end
```

実行する

```console:convert
$  ruby convert_bulk_data_lat_lon.rb
```

bulk_restaurants_lat_lon.json が出来ていれば成功

意図したフォーマットになっているか確認

```json:
$ head -n 2 bulk_restaurants_lat_lon.json | jq .
{
  "index": {
    "_index": "gourmet",
    "_type": "restaurants",
    "_id": "2"
  }
}
{
  "id": "2",
  "name": "ラ・マーレ・ド・茶屋",
  "property": "2F・3F",
  "alphabet": "LA MAREE DE CHAYA",
  "name_kana": "らまーれどちゃや",
  "pref_id": "14",
  "area_id": "1013",
  "station_id1": "2338",
  "station_time1": "22",
  "station_distance1": "1789",
  "station_id2": "2401",
  "station_time2": "28",
  "station_distance2": "2240",
  "station_id3": "2867",
  "station_time3": "47",
  "station_distance3": "3755",
  "category_id1": "201",
  "category_id2": "0",
  "category_id3": "0",
  "category_id4": "0",
  "category_id5": "0",
  "zip": "240-0113",
  "address": "三浦郡葉山町堀内24-3",
  "description": "こちら2.3Ｆのレストランへのコメントになります。  『ラ・マーレ・ド・茶屋』1F(テラス&バー)へのコメントはそちらにお願いします。    駐車場15台(専用)    06/06/19　営業時間等更新（From東京グルメ）",
  "purpose": null,
  "open_morning": "0",
  "open_lunch": "1",
  "open_late": "0",
  "photo_count": "1",
  "special_count": "0",
  "menu_count": "0",
  "fan_count": "5",
  "access_count": "6535",
  "created_on": "2000-09-10 11:22:02",
  "modified_on": "2011-04-22 16:05:12",
  "closed": "0",
  "pin": {
    "lat": "35.28154611111111",
    "lon": "139.57225805555555"
  }
}
$
```

ここチェック。

```json:抜粋
  "pin": {
    "lat": "35.28154611111111",
    "lon": "139.57225805555555"
  }
```


(2)mappingファイルも修正
---------------------------------------------------------------

```console:
$ cp -p elasticsearch-hands-on/mappings/restaurants.json elasticsearch-hands-on/mappings/restaurants_lat_lon.json
```

ファイルを修正する。
以下の２つは削除。
  * north_latitude
  * east_longitude

代わりに以下を追加。

```json:
$  tail -n 7 elasticsearch-hands-on/mappings/restaurants_lat_lon.json
                "pin": {
                    "type" : "geo_point"
                }
            }
        }
    }
}
```


(3)再度indexing(mapping削除、再登録)
---------------------------------------------------------------

mapping削除

```console:
$ curl -X DELETE http://192.168.99.100:19200/gourmet/_mapping/restaurants
```

index削除

```console:
$ curl -X DELETE http://192.168.99.100:19200/gourmet/
```

再作成

```console:
$ curl -X POST http://192.168.99.100:19200/gourmet -d @elasticsearch-hands-on/mappings/restaurants_lat_lon.json
```

mapping確認

```console:
$ curl -X GET http://192.168.99.100:19200/gourmet/restaurants/_mapping\?pretty
```

kibanaでインデックスを登録している場合は一回削除して作りなおす

![kibana_index.png](https://qiita-image-store.s3.amazonaws.com/0/17518/d988deaf-ea74-43cc-df86-9cf621f8c11f.png "kibana_index.png")


再投入

```console:
$ curl -X POST http://192.168.99.100:19200/_bulk --data-binary @datasets/bulk_restaurants_lat_lon.json
```

表示させてみる

pin のとなりに add が表示されるのでクリック。
クリックした後pinをクリックすると `Visualize` ボタンが出てくるのでクリック。

![0001_Discover_-_Kibana_4.png](https://qiita-image-store.s3.amazonaws.com/0/17518/9046e55f-97ef-5dde-cf02-1825dce7156e.png "0001_Discover_-_Kibana_4.png")

表示できた！

![0002_Visualize_-_Kibana_4.png](https://qiita-image-store.s3.amazonaws.com/0/17518/99f8d367-2873-44fd-64ab-5660524d9b8c.png "0002_Visualize_-_Kibana_4.png")