実践的Elasticsearchクエリ

この記事は[WanoグループAdventCalendar2016](http://qiita.com/advent-calendar/2016/wano-group)の13日目の記事になります。

Elasticsearchのクエリはとても使いやすく、かつ、実開発現場で必要となる、複雑で複数の検索結果が欲しいような場面でも、保守性を損なうことのない、見やすい一つのクエリで書ききることができるような設計になっています。

ですが、実際書く段なると細部までは覚えていないので、マニュアルや実装例をWebで調べることになるのですが、ブログ記事などの実装例が非常に助けになったので、微力ながらそういったものの一つになればと思い、この記事を書くことにしました。

今回は、下記のような力士データベースを例にしてみます。

| 項目名|型|データ例|
|:---|:---:|---:|
|名前|string|稀勢の里|
|生年月日|date|1986年7月3日|
|性別|string|男|
|出身地|string|茨城県|
|格付|string|西大関|
|所属|string|田子ノ浦部屋|
|年齢|integer|30歳|
|身長|integer|188cm|
|体重|integer|171cm|
|バスト|integer|147cm|
|ウエスト|integer|203cm|
|ヒップ|integer|185cm|
|血液型|string|B型|
|趣味|string|スポーツ観戦|
|得意技|string|左四つ・寄り・突き|
|好きなタイプ|string|仲間由紀恵|
|髪型|string|大銀杏|
|今までに突き合った人数|integer|1209人|

それでは、好みの力士を検索してみます。
（説明用にコメント文を挿入していますが、使用時は削除してください）

```JSON
{
    //取得件数と開始位置
    "size": "20",
    "from": "0",
    
　  //ここからが検索条件
    "query": {
        "bool": {

            //単に結果をフィルタしたいだけの場合はfilter構文を使うといくらか高速
            "filter": {
                "bool": {

                    //mustは全部の条件が満たされたいということ
                    "must": [
                        {
                            //年齢は15歳～35歳に限定
                            "range": {
                                "年齢": {
                                    "gte": "15", 
                                    "lte": "35"
                                }
                            }
                        }, 
                        {
                            //性別は女性だけ
                            "term": {
                                "性別": "女"
                            }
                        }, 
                        {
                            //髪型はショートカットかセミロングどちらか
                            "terms": {
                                "髪型": [
                                    "ショートカット",
                                    "セミロング",
                                ]
                            }
                        }
                    ], 

                    //must_notで満たしたくない条件を指定する。
                    //男性は含まない、って上で女性指定してるからこの条件は不要ですね。
                    "must_not": {
                        "terms": {
                            "性別": ["男"]
                        }
                    }
                }
            },

            //ここからがfilterではない本来のqueryなのでスコア付けされる 
            "must": [
                {
                    "bool": {

                        //boostして各条件に重み付けをしている。
                        //よく分からない重み付けになってるけど、
                        //とにかく好きなタイプがプログラマーかSEかサーバーエンジニア希望ということ。
                        "should": [
                            {
                                "term": {
                                    "boost": 4, 
                                    "好きなタイプ": "プログラマー"
                                }
                            }, 
                            {
                                "term": {
                                    "boost": 3, 
                                    "好きなタイプ": "SE"
                                }
                            }, 
                            {
                                "term": {
                                    "boost": 2, 
                                    "好きなタイプ": "サーバーエンジニア"
                                }
                            }
                        ]
                    }
                }, 
                {
                    //趣味か得意技にプログラミングかラムダ式が含まれること希望。
                    //趣味に若干重みを付けている。
                    "multi_match": {
                        "fields": [
                            "趣味^6", 
                            "得意技^4", 
                        ], 
                        "operator": "or", 
                        "query": "プログラミング ラムダ式", 
                        "type": "most_fields"
                    }
                }
            ]
        }
    }, 

    //検索条件は終わってソート。
    //ソートしてしまったら、さっきまでの重み付けの意味が・・・。
    //指定しなかったら、スコア順になる。
    //まず、バストは大きい方が良くて、同じサイズならウエストは小さいほうが良いらしい。
    "sort": [
        {
            "バスト": {
                "order": "desc"
            }
        },
        {
            "ウエスト": {
                "order": "asc"
            }
        }
    ],

    //検索結果のついでに、全検索結果の統計情報も取り出す
    "aggs": {
        
        //最初のタイトル（ここでは「血液型グループ」）は結果の目印なので適当に。
        //血液型がB型の人数を知りたいみたい。
        "血液型グループ": {
            "filter": {
                "term": {
                    "血液型": "B"
                }
            }
        }, 

        //これは複雑ですが、今までに突き合った人数を10人単位でヒストグラム化している
        //～9人：5名
        //10～19人：3名
        //20～29人：7名
        //みたいな感じで。
        "今までに突き合った人数グループ": {
            "aggs": {
                "今までに突き合った人数ヒストグラム": {
                    "histogram": {
                        "field": "今までに突き合った人数", 
                        "interval": 10, 
                        "order": {
                            "_key": "asc"
                        }
                    }
                }
            },
 
            //ヒストグラムに、今までに突き合った人数が0人の人は含めないようにしている。
            "filter": {
                "range": {
                    "今までに突き合った人数": {
                        "gte": 1
                    }
                }
            }
        }, 

        //趣味ごとの人数を、多い順に並べている
        "趣味グループ": {
            "terms": {
                "field": "趣味", 
                "order": {
                    "_count": "desc"
                }, 
                "size": 0 //全趣味を取ってくるために0を指定
            }
        } 
    } 
}
```

こんな感じで、好みの力士を検索することができました。