Elasticsearch でスキーマを指定して空間検索をする

Elasticsearch でスキーマを定義してデータを投入する方法を試してみます。
Elasticsearch のインストールについては[コチラ](http://qiita.com/mserizawa/items/c1c04fa9b2f85c437334)、
kuromoji のインストーラについては[コチラ](http://qiita.com/mserizawa/items/8335d39cacb87f12b678)を参考にしてください。

## Elasticsearch のスキーマについて

Elasticsearch ではスキーマを **マッピング**、型を **タイプ** という概念で扱います。
マッピングを定義していない場合は、投入されたデータの値からタイプを自動で判定して、マッピングされます。
これはとても便利な機能である反面、意図しない・不必要なインデクシングが発生してしまう可能性もあります。
安定した運用が求められるケースでは、しっかりとスキーマを定義したほうが良いですね。

## マッピングの指定

今回はランドマークの「*名前*、*緯度軽度*、*説明*」を投入してみます。

| 項目 | プロパティ名 | 説明 |
|:----|:-----------|:----|
| 名前 | name       | ランドマークの名前。**全文検索対象外**。
| 緯度軽度 | coord | ランドマークの緯度軽度。**空間検索の対象**。
| 説明 | description | ランドマークの説明。**全文検索の対象** |

これをマッピング指定に用いる JSON で表現すると以下のようになります。

```json:mapping.json
{
    "mappings": {
        "sample": {
            "properties": {
                "name": {
                    "type": "string",
                    "index": "not_analyzed"
                },
                "coord": {
                    "type": "geo_point"
                },
                "description": {
                    "type": "string",
                    "analyzer": "kuromoji"
                }
            }
        }
    }
}
```

この JSON を使って `landmark` というインデックスを作成します。

```bash
$ curl -X PUT http://localhost:9200/landmark --data-binary @mapping.json
```

意図通り定義されたかを確認してみます。

```bash
$ curl -X GET http://localhost:9200/landmark
```

```json
{
    "landmark": {
        "aliases": {},
        "mappings": {
            "sample": {
                "properties": {
                    "coord": {
                        "type": "geo_point"
                    },
                    "description": {
                        "type": "string",
                        "analyzer": "kuromoji"
                    },
                    "name": {
                        "type": "string",
                        "index": "not_analyzed"
                    }
                }
            }
        },
        ...
    }
}
```

いい感じですね。

## データの投入

前回同様、[Bulk API](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html) を使ってデータを投入します。
以下の JSON を使います。

```json:landmark.json
{ "index" : {} }
{ "name": "スカイツリー", "coord": { "lat": "35.710063", "lon": "139.8107"}, "description": "東京スカイツリー（とうきょうスカイツリー、英: TOKYO SKYTREE）は東京都墨田区押上一丁目にある電波塔（送信所）である。"}
{ "index" : {} }
{ "name": "BLUE NOTE TOKYO", "coord": { "lat": "35.661198", "lon": "139.716207"}, "description": "N.Y.の「Blue Note」を本店に持つジャズクラブとして、南青山にオープンしたブルーノート東京。ジャズをはじめとする多様な音楽ジャンルのトップアーティストたちが、連夜渾身のプレイを繰り広げている。"}
{ "index" : {} }
{ "name": "東京タワー", "coord": { "lat": "35.65858", "lon": "139.745433"}, "description": "東京タワー（とうきょうタワー、英: Tokyo Tower）は、東京都港区芝公園にある総合電波塔とその愛称である。正式名称は日本電波塔（にっぽんでんぱとう）。"}
```

API にデータを食わせます。

```bash
$ curl -X POST http://localhost:9200/landmark/sample/_bulk --data-binary @landmark.json
```

## データの検索

「ジャズ」で検索してみます。

```bash
$ curl -X GET http://localhost:9200/landmark/sample/_search -d '{"query":{"match":{"description":"ジャズ"}}}'
```

```json
{
    ...
    "hits": {
        "total": 1,
        "max_score": 0.081366636,
        "hits": [
            {
                "_index": "landmark",
                "_type": "sample",
                "_id": "AU071ik4RTPkdK_mzpPE",
                "_score": 0.081366636,
                "_source": {
                    "name": "BLUE NOTE TOKYO",
                    "coord": {
                        "lat": "35.661198",
                        "lon": "139.716207"
                    },
                    "description": "N.Y.の「Blue Note」を本店に持つジャズクラブとして、南青山にオープンしたブルーノート東京。ジャズをはじめとする多様な音楽ジャンルのトップアーティストたちが、連夜渾身のプレイを繰り広げている。"
                }
            }
        ]
    }
}
```

ちゃんと検索されました。

次に空間検索をしてみます。
「渋谷駅から近い順」に検索してみましょう。
（緯度経度は GoogleMap などから取得すると良いです）

```json:query.json
{
    "sort": [
        {
            "_geo_distance": {
                "coord": {
                    "lat": 35.658517,
                    "lon": 139.701334
                },
                "order": "asc"
            }
        }
    ],
    "query": {
        "filtered": {
            "query": {
                "match_all": {}
            }
        }
    }
}
```

```bash
$ curl -X GET http://localhost:9200/landmark/sample/_search --data-binary @query.json
```

レスポンスはこんな感じです。

```json
{
    ...
    "hits": {
        "total": 3,
        "max_score": null,
        "hits": [
            {
                ...
                "_source": {
                    "name": "BLUE NOTE TOKYO",
                    "coord": {
                        "lat": "35.661198",
                        "lon": "139.716207"
                    },
                    ...
                },
                "sort": [
                    1376.353778271321
                ]
            },
            {
                ...
                "_source": {
                    "name": "東京タワー",
                    "coord": {
                        "lat": "35.65858",
                        "lon": "139.745433"
                    },
                    ...
                },
                "sort": [
                    3984.1379076620046
                ]
            },
            {
                ...
                "_source": {
                    "name": "スカイツリー",
                    "coord": {
                        "lat": "35.710063",
                        "lon": "139.8107"
                    },
                    ...
                "sort": [
                    11419.980751959736
                ]
            }
        ]
    }
}
```

地点間の直線距離が計算され、それの照準で返却されています。
もちろんワード指定の検索と組み合わせることも可能ですので、色々と試してみてください。