Geometric HashingでクラスタリングしてGoogle Mapに表示するタグをまとめる、その放浪記。

# 背景
うん千万あるレコードをGoogle Map上に表示させたいという話があり、単純にピンを貼ると、ブラウザが重くて動かないくなる。
さあ、どうしよう。

---

# やったこと
Elasticsearchの位置検索（Geolocation）を使って、クラスタリングを行い、重心を取得、それをピンとして表示するということをやりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270964/f14f710f-f4bd-5f38-0ba1-046a81ebf438.png)

---

# Geometric Hashingを使った検索とは
インデックスはWebやってる方は言わずもがなご存じな仕組みだとは思いますが、データの格納箇所をポインタに保存しておくことで、検索を早くするあれですね。

Geometric Hashingを使った検索というのも、インデックスを使う方法なのですが、インデックスを2DのHash Table(geohash grid)に対して貼り付けて、その中にx,y座標を格納しておくことで、速く、座標を取得する技術です。

※Hash Tableのなかにx, y座標を格納することを[Geometric Hashing](https://en.wikipedia.org/wiki/Geometric_hashing)と言います。

---

絵にするとこのような形で点座標にグリッドを重ね合わせて、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270964/8306e66a-3b86-d480-adcd-e02c7f83ae98.png)

こんな形でポインタを貼ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270964/57c1594f-3ac0-a3a7-6b5e-9faa33561126.png)

---

# Geometoryに関する仕組みを提供しているものとして有名どころはこの二つかなと。
1. MongoDB
2. Elasticsearch

この二つから、Geometric Hashingを使える方法を模索していきます。

---

## MongoDB
MongoDBには様々なインデックスの貼り方が提供されてれいるのですが、その中の一つにgeospatial (地理空間)用のインデックス**があります。

種類は3種類。
- 2Dインデックス
- 2DSphereインデックス
- geoHaystackインデックス。

この3つのインデックスが提供されています。

---

## 2Dインデックス

以下のコードは、[公式](https://docs.mongodb.com/manual/core/geospatial-indexes/) のサンプルコードそのままですが、`[数字、数字]`もしくは`[lng: 数字、lat: 数字]`と言う形で座標を登録し、

```js

db.places.save( {
  locs : [ [ 55.5 , 42.3 ] ,
           [ -74 , 44.74 ] ,
           { lng : 55.5 , lat : 42.3 } ]
} )
```

locsフィールドに対して、2dというインデックスを貼る関数を実行するだけです。
```js

db.places.createIndex( { "locs": "2d" } )
```

---

## 2DSphereインデックス
実は先に話た2Dインデックスというのはレガシーな仕組みで16bitで値が保存されています。
それに対し、2DSphereインデックスは32bitで値が保存されるため、より巨大な座標系を扱う場合は2DSphereを扱うのがいいかと思います。

こちらはlocフィールドに対して以下のような形で座標を登録し、

```js

db.places.insert(
   {
      loc : { type: "Point", coordinates: [ -73.97, 40.77 ] },
      name: "Central Park",
      category : "Parks"
   }
)
```

このような形でインデックスを貼ります。


```js

db.places.createIndex( { loc : "2dsphere" } )
```

---

### 値の取得

一定の範囲内の座標を取得したい場合は、`$geoWithin`を使い、Polygonを利用して範囲指定をして取得します。

```js

db.places.find( { loc :
                  { $geoWithin :
                    { $geometry :
                      { type : "Polygon" ,
                        coordinates : [ [
                                          [ 0 , 0 ] ,
                                          [ 3 , 6 ] ,
                                          [ 6 , 1 ] ,
                                          [ 0 , 0 ]
                                        ] ]
                } } } } )
```

任意の点から近い座標を取得したい場合は、`$near`を使います。


```js

db.<collection>.find( { loc :
                         { $near :
                           { $geometry :
                              { type : "Point" ,
                                coordinates : [ 12 , 34 ] } ,
                             $maxDistance : 100
                      } } } )

```

---

### GeoJson

このlocの値にtypeやらcoordinatesというkeyが入っているのですが、GeoJsonという形式で保存する必要があるため、このような形になっています。

GeoJsonは点情報であれば、

```json
{ type: "Point", coordinates: [ 40, 5 ] }
```

線情報であれば、

```json
{ type: "LineString", coordinates: [ [ 40, 5 ], [ 41, 6 ] ] }
```

という形をとります。
Geometory Hashingを行うような形の検索は…行えなさそうですね。


---

## geoHaystackインデックス

そう、これが、追い求めていた検索ができる用のインデックスなのでしょうか…？


以下のような形でデータをmongoにインサートします。

```js

{ _id : 100, pos: { lng : 126.9, lat : 35.2 } , type : "restaurant"}
{ _id : 200, pos: { lng : 127.5, lat : 36.1 } , type : "restaurant"}
{ _id : 300, pos: { lng : 128.0, lat : 36.7 } , type : "national park"}
```

それのデータに大して、このような形でgeoHatstackインデックスをはることができます。

```js
db.places.createIndex( { pos : "geoHaystack", type : 1 } ,
                       { bucketSize : 1 } )
```

[説明]()を読んでみると、欲しかったのはこれなのですが、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270964/e76360c3-88da-6249-20e7-4019a1cba425.png)

こうなるもよう。200が2つのバケットに入るようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270964/3998ceb8-f21f-3cfa-c434-0bc8dd063c1a.png)


これはこれで便利ですが、今回やりたかったものは、もっとMECEな分割がされたものなので、すこし違うようです。

---

## Elasticsearch

Elasticsearchもmongoと同様にgeospatialなインデックスを持つことができ、mongoよりも多様な検索方法が行えます。
どうやら今回の目的のブツもElasticsearchにあるようです。

座標の検索方法は以下の通り、
1. 円形範囲検索（geo_distance）
2. ドーナツ型範囲検索（geo_distance_range）
3. 四角範囲検索（geo_bound_box）
4. 任意の範囲検索（geo_polygon）

1.はmongoで話した`$near`の検索と、4.はmongoで話した`$geoWithin`の検索と同じですね。

そして、目的のブツであるGeometory Hashingが行える検索方法ですが、
**geohash_grid Aggregation**というまさしく目的の名前が付いた検索方法があるようです。

参考にしたページは[こちら](https://dev.classmethod.jp/articles/study-elasticsearch-geolocation/)

---

今回はLaravelで実装する必要があったので、
composerでelasticsearchのライブラリをインストール。

```shell
composer require elasticsearch/elasticsearch
```

インデックスの貼り方はこんな感じで、typeに`geo_point`を指定してやることで、geospatialなインデックスを貼ることができます。

```php
// clientを作成
$client = \Elasticsearch\ClientBuilder::create()->setHosts(\Config::get('elasticquent.config.hosts'))->build();

// index作成用のパラメータを作成
$createParams = [
    'index' => 'hoge',
    'body'  => [
        'mappings' => [
            'properties' => [
                'hogeId' => [
                    'type' => 'keyword',
                ],
                'location' => [
                    'type' => 'geo_point',
                ],
                'fuga' => [
                    'type' => 'nested',
                ],
            ];,
        ],
    ],
];

// indexを作成
$client->indices()->create($createParams);
```

こそにデータを登録すると、

```php

$params = [
    'index' => $index,
    'type'  => '_doc',
    'id'    => $id,
    'body'  => [
        'location' => [
            'lon' => 132,
            'lat' => 36,
        ]
    ],
];

$result = $client->index($params);
```

このような形で`geohash_grid`を指定したクエリをなげることで、grid内の座標を取得することができます。

```json

{
  "size": 0, 
  "aggs": {
    "1": {
      "geohash_grid": {
        "field": "location",
        "precision": 5
      }
    }
  }
}
```


また、`geohash_grid`毎にaggs（集計）させ、かつ、`geo_centroid`でaggs(集計)することで、grid内の点の重心を得ることもできました。

詳細はまた、時間があるときにここに追加します。