OpenSearchに「全国の道の駅」の位置情報を突っ込んでサクッと可視化する

:::note info
これは[MIERUNE Advent Calendar 2023](https://qiita.com/advent-calendar/2023/mierune)の18日目の記事です。昨日は、@Takayuki_Kawajiri さんによる[Cesiumでカメラ移動毎に標高値取得したら、とてつもなく重かった話](https://qiita.com/Takayuki_Kawajiri/items/f8e5e3ff59a36186ba13)でした。
:::

先日、こんな記事を書きました！
[今すぐに！FastAPIとOpenSearchで開発を始めるためのテンプレート！](https://qiita.com/nokonoko_1203/items/6400ed9e318fff81e5f9)

このテンプレートを利用するとFastAPIですぐに開発を始めることができますが、OpenSearchの実行環境としても活用できるので、今回は適当なデータを投入して検索・地図上で確認してみましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/c8054892-850d-5409-56cf-921fdce8112c.png)

## データ投入

今回は「全国の道の駅」のデータを投入してみましょう！

データは国土数値情報というところから入手できます。
https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-P35.html

上記URLの最下部にダウンロード用のボタンがあるので、そこからzipファイルをダウンロードしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/6e4eb81f-a23c-fd0d-11d3-9132ed719a62.png)

## 作業環境構築

[今すぐに！FastAPIとOpenSearchで開発を始めるためのテンプレート！](https://qiita.com/nokonoko_1203/items/6400ed9e318fff81e5f9)

に従ってテンプレートをダウンロードし、OpenSearchを実行できるようにしておきましょう！

さらに、`api/src/data`フォルダを作成し、解凍したzipファイルの中身を入れましょう。
（dataフォルダには`geojson`ファイルだけ入っていればいいので、`api/src/data/P35-18_Roadside_Station.geojson`のようなフォルダ構成になるようにしましょう。）

## データ投入プログラムを書く

Elasticsearch用のPythonライブラリの情報はたくさんありますが、OpenSearchだと微妙に書き方が違ったりするので注意しましょう！
また、OpenSearchは`https`で通信します。その辺りも微妙に注意しましょう。

まずはファイルを作成し…

```bash
mkdir api/src/utils
touch api/src/utils/register.py
```

プログラムを書いていきます。

```python
import ujson as json
from opensearchpy import OpenSearch
from opensearchpy.helpers import bulk, parallel_bulk

opensearch_url = "https://localhost:9200"
opensearch_master_user = "admin"
opensearch_master_password = "admin"

auth = (opensearch_master_user, opensearch_master_password)
opensearch = OpenSearch(
    opensearch_url,
    http_auth=auth,
    use_ssl=True,
    verify_certs=False,
    ssl_assert_hostname=False,
    ssl_show_warn=False,
)

index_name = "road_station"

if opensearch.indices.exists(index=index_name):
    opensearch.indices.delete(index=index_name)

res = opensearch.indices.create(
    index=index_name,
    body={
        "settings": {
            "number_of_shards": 3,
            "number_of_replicas": 1,
        },
        "mappings": {
            "dynamic": "strict",
            "_source": {"excludes": ["geometry"]},
            "properties": {
                "P35_001": {"type": "double"},
                "P35_002": {"type": "double"},
                "P35_003": {"type": "text"},
                "P35_004": {"type": "text"},
                "P35_005": {"type": "keyword"},
                "P35_006": {"type": "text"},
                "P35_007": {"type": "text"},
                "P35_008": {"type": "text"},
                "P35_009": {"type": "text"},
                "P35_010": {"type": "text"},
                "P35_011": {"type": "integer"},
                "P35_012": {"type": "integer"},
                "P35_013": {"type": "integer"},
                "P35_014": {"type": "integer"},
                "P35_015": {"type": "integer"},
                "P35_016": {"type": "integer"},
                "P35_017": {"type": "integer"},
                "P35_018": {"type": "integer"},
                "P35_019": {"type": "integer"},
                "P35_020": {"type": "integer"},
                "P35_021": {"type": "integer"},
                "P35_022": {"type": "integer"},
                "P35_023": {"type": "integer"},
                "P35_024": {"type": "integer"},
                "P35_025": {"type": "integer"},
                "P35_026": {"type": "integer"},
                "P35_027": {"type": "integer"},
                "P35_028": {"type": "integer"},
                "location": {"type": "geo_point"},
            },
        },
    },
)


def iter_docs():
    filepath = "src/data/P35-18_Roadside_Station.geojson"
    with open(
        filepath,
        encoding="utf-8",
    ) as f:
        data = json.load(f)
        features = data["features"]

        for d in features:
            geom = d["geometry"]["coordinates"]
            d["properties"]["location"] = [geom[0], geom[1]]

            yield {
                "_index": index_name,
                "_id": f"{d['properties']['P35_001'],d['properties']['P35_002']}",
                "_source": d["properties"],
            }


for _res in parallel_bulk(
    opensearch,
    iter_docs(),
    index=index_name,
    chunk_size=100,
    raise_on_error=True,
    request_timeout=60,
):
    print(res)
```

以下のように実行します。

```bash
cd api
poetry run python ./src/utils/register.py
```

うまく実行されるとこんな感じになると思います。

```bash
...
{'acknowledged': True, 'shards_acknowledged': True, 'index': 'road_station'}
{'acknowledged': True, 'shards_acknowledged': True, 'index': 'road_station'}
```

## データを見てみる

OpenSearch　dashboardでデータを見てみましょう！

`http://localhost:5601/`に接続してください。

サイドメニューを開き`Map`をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/b80ff6bf-1c26-ca29-3087-419268b8726b.png)

次のように遷移するので、`Create index pattern`をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/a81f72f6-3490-29b2-92a6-a51d348b87ee.png)

今回作った`road_station`インデックスを指定し、`Next step`をクリックし次の画面ではそのまま`Create index pattern`もクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/6aa91f28-e78e-f839-78d3-ef3ea47199da.png)

再度サイドバーから`Maps`をクリックすると、先ほどとは別の画面になっているはずなので、`Create map`をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/11d9725e-2439-be8c-3960-34481b5c8c6a.png)

すると地図画面が出てきましたね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/b4ea83cb-00af-f18a-0cab-b1a932ed58a8.png)

`Add layer`をクリックし、`Documents`を選択しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/edd3418a-5b7d-4502-27e3-7c391f890d34.png)

以下のように設定し、`Update`を押すと…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/0da62676-192b-17c8-bcea-c69f56bcba29.png)

道の駅が出てきました！結構多いですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/c8054892-850d-5409-56cf-921fdce8112c.png)

ラベルなどを表示したい場合は、レイヤーを選んでStyleなどを変更することで可視化できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/5c3b5089-97bf-9591-6ee8-7665280637b0.png)

地図上で矩形を描いて…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/0d654db9-04d7-1549-b330-b6105925f7f1.png)

その範囲外のデータをフィルタリングする、なども可能です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/57397d1e-8811-d4d9-013f-1915ce8faf94.png)

## 終わりに

超便利ですね！

Elasticsearch同様、OpenSearchでも地図上に位置情報を可視化させることが可能です！

kibanaとOpenSearch Dachboardsではちょっと使い勝手が異なったりしますが、基本はほぼ変わらず利用できるかと思いますし、AWSは最近、地理空間情報にも力を入れているので、強力なアップデートが突如きたりするかも！？

すでに便利ですが、もっと便利に使えると嬉しいですね！