[Python] Elasticsearchを用いた位置情報検索

# はじめに
Elasticsearchは緯度経度を用いた位置情報検索ができます。
今回は山手線の緯度経度データをElasticsearchに投入し、位置情報を用いた範囲検索を実装してみます。
# インデックスの作成
- 今回は山手線の駅名と緯度経度情報を持つデータを投入します。
- geo_pointタイプはdynamic mappingされず、事前に定義する必要があるので注意です。

```python
from elasticsearch import Elasticsearch

# Elasticsearchインスタンスの作成
es = Elasticsearch(
    "elasticsearchのURL",
    http_auth=("user_id", "password")
)

# マッピング定義
mapping = {
    "mappings": {
        "properties": {
            "name": {
                "type": "text"
            },
            "location": {
                "type": "geo_point"
            },
        }
    }
}

# インデックスの作成
es.indices.create(index="yamanote_station", body=mapping)
```

# テストデータのバルクインサート
- 作成したインデックスにバルクインサートを用いて山手線のデータを投入します。
```python
from elasticsearch import helpers

def bulk_insert(index, geo_location_dict):
    yamanote_info_list = [{"name": station, "location": geo_location} for station, geo_location in geo_location_dict.items()]

    # bulkで扱えるデータ構造に変換します
    for yamanote_info in yamanote_info_list:
        yield {
            "_op_type": "create",
            "_index": index,
            "_source": yamanote_info
        }

yamanote_station_dict = {
 '上野駅': {'lat': 35.7141672, 'lon': 139.7774091},
 '五反田駅': {'lat': 35.6261591, 'lon': 139.7236022},
 '代々木駅': {'lat': 35.683033, 'lon': 139.7020555},
 '原宿駅': {'lat': 35.6702285, 'lon': 139.7026976},
 '品川駅': {'lat': 35.6284713, 'lon': 139.7387597},
 '大塚駅': {'lat': 35.7318309, 'lon': 139.7281112},
 '大崎駅': {'lat': 35.6198513, 'lon': 139.7281892},
 '巣鴨駅': {'lat': 35.7334192, 'lon': 139.7392848},
 '御徒町駅': {'lat': 35.7075185, 'lon': 139.7748564},
 '恵比寿駅': {'lat': 35.6467139, 'lon': 139.7100777},
 '新大久保駅': {'lat': 35.7012459, 'lon': 139.7002258},
 '新宿駅': {'lat': 35.6896067, 'lon': 139.7005713},
 '新橋駅': {'lat': 35.666379, 'lon': 139.7583398},
 '日暮里駅': {'lat': 35.7281578, 'lon': 139.7706414},
 '有楽町駅': {'lat': 35.6749187, 'lon': 139.7628199},
 '東京駅': {'lat': 35.68123620000001, 'lon': 139.7671248},
 '池袋駅': {'lat': 35.7295028, 'lon': 139.7109001},
 '浜松町駅': {'lat': 35.6553809, 'lon': 139.7571289},
 '渋谷駅': {'lat': 35.6580339, 'lon': 139.7016358},
 '田町駅': {'lat': 35.6457361, 'lon': 139.7475624},
 '田端駅': {'lat': 35.7381581, 'lon': 139.7608154},
 '目白駅': {'lat': 35.7212199, 'lon': 139.7066115},
 '目黒駅': {'lat': 35.6340929, 'lon': 139.7158331},
 '神田駅': {'lat': 35.6918216, 'lon': 139.7709318},
 '秋葉原駅': {'lat': 35.698383, 'lon': 139.7730717},
 '西日暮里駅': {'lat': 35.73200569999999, 'lon': 139.7668856},
 '駒込駅': {'lat': 35.7365665, 'lon': 139.7470098},
 '高田馬場駅': {'lat': 35.7125654, 'lon': 139.7038615},
 '高輪ゲートウェイ駅': {'lat': 35.6355207, 'lon': 139.7406809},
 '鶯谷駅': {'lat': 35.7214573, 'lon': 139.7780133}
 }

helpers.bulk(es, bulk_insert("yamanote_station", yamanote_station_dict))
```

# 位置情報検索
- 今回は東京駅から半径3km以内に存在する山手線の駅を検索してみます
- 円形範囲検索は**geo_distance**クエリを使用します。（その他の位置情報検索は[公式](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html)を参照してみてください）

```python
query = {
  "query": {
    "geo_distance": {
      "distance": "3km",
      "location": {
        "lat": yamanote_station_dict['東京駅']['lat'],
        "lon": yamanote_station_dict['東京駅']['lon']
      }
    }
  }
}

res = es.search(index="yamanote_station", body=query, size=10)
```

- 検索結果
  - 新橋・有楽町・神田・秋葉原と東京駅から3km以内の駅がしっかり検索できていますね。
```python
{'hits': [{'_id': 'JeizK38BB0c_3d13lkrt',
   '_index': 'yamanote_station',
   '_score': 1.0,
   '_source': {'location': {'lat': 35.666379, 'lon': 139.7583398},
    'name': '新橋駅'},
   '_type': '_doc'},
  {'_id': 'J-izK38BB0c_3d13lkrt',
   '_index': 'yamanote_station',
   '_score': 1.0,
   '_source': {'location': {'lat': 35.6749187, 'lon': 139.7628199},
    'name': '有楽町駅'},
   '_type': '_doc'},
  {'_id': 'KOizK38BB0c_3d13lkrt',
   '_index': 'yamanote_station',
   '_score': 1.0,
   '_source': {'location': {'lat': 35.68123620000001, 'lon': 139.7671248},
    'name': '東京駅'},
   '_type': '_doc'},
  {'_id': 'MOizK38BB0c_3d13lkrt',
   '_index': 'yamanote_station',
   '_score': 1.0,
   '_source': {'location': {'lat': 35.6918216, 'lon': 139.7709318},
    'name': '神田駅'},
   '_type': '_doc'},
  {'_id': 'MeizK38BB0c_3d13lkrt',
   '_index': 'yamanote_station',
   '_score': 1.0,
   '_source': {'location': {'lat': 35.698383, 'lon': 139.7730717},
    'name': '秋葉原駅'},
   '_type': '_doc'}],
 'max_score': 1.0,
 'total': {'relation': 'eq', 'value': 5}}
```

# 参考記事
- Geo queries | Elasticsearch Guide [8.0] | Elastic https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html#geo-queries
- Elasticsearch の位置検索（Geolocation）を学ぶ | DevelopersIO https://dev.classmethod.jp/articles/study-elasticsearch-geolocation/