Elasticsearchでsearchkick gemを使ってレストランデータを検索する


[searchkick](https://github.com/ankane/searchkick)gemをつかってライブドアのレストランデータセットを様々なユースケースで検索してみます。
レストランのデータはActiveRecordを介してすでにデータベースにロードされているとします。


# Elasticsearchへのインポートと更新

RestaurantsデータをElasticsearchに入れるには以下のようにします。

```
Restaurant.reindex
```

データが更新された際のElasticsearchへの更新方法は同期、非同期と２種類あります。デフォルトは同期です。また関連しているレコードはデフォルトでは更新されません。更新する倍は、after_commitで該当の関連先インスタンスをreindexします。

# 複数の条件で絞り込む

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/0c638aa3-3a58-2c4d-1aa7-0dc011b1866d.png)

食べログだと上記のような複数条件の検索です。今回は以下で絞り込みます。

* nameもしくはdescription: ラーメンを含む。ただし、nameを10ブーストする
* エリア: 新宿・代々木(area_id = 3)
* 閉店していない: closed = 0
* 夜あいている: open_late = 1

```rb
restaurants = Restaurant.search "ラーメン", fields: ["name^10", "description"], where: { area_id: 3, closed: 0, open_late: 1 }, limit: 5, order: { access_count: :desc }

# 結果は以下
[27] pry(main)> restaurants.map(&:name)
  Restaurant Load (2.7ms)  SELECT `restaurants`.* FROM `restaurants` WHERE `restaurants`.`id` IN (9575, 7690, 11777, 674, 393412)
=> ["どうとんぼり神座", "ラーメン二郎", "ラーメン若月", "一心ラーメン", "ラーメン大"]
```


# 特定の店のレビュー数の統計を取る

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/2cf49705-6a0d-5901-faf2-f9e1d92ec557.png)

Amazonで言うと上記のようなイメージです。今回は「どうとんぼり神座(id = 9575)」の結果をとってみます。

```rb
ratings = Rating.search "*", where: { restaurant_id: 9575 }, aggs: { total: { order: { "_term" => "asc" } } }, limit: 5

# ratingに対する評価数
[14] pry(main)> ratings.aggs["total"]["buckets"]
=> [{"key"=>0, "doc_count"=>1}, {"key"=>1, "doc_count"=>4}, {"key"=>2, "doc_count"=>17}, {"key"=>3, "doc_count"=>45}, {"key"=>4, "doc_count"=>28}, {"key"=>5, "doc_count"=>2}]

# 平均評価
[44] pry(main)> ratings.aggs["total"]["buckets"].map { |e| e["key"] * e["doc_count"] }.sum / ratings.aggs["total"]["doc_count"].to_f
=> 3.0412371134020617
```

# 検索でのオートコンプリーション

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/951c75bc-e07e-d97a-0e7f-778fdac37e05.png)

Googleサーチでも出てくる検索候補です。ここらへんはある程度データがたまってきたらユーザの検索クエリをデータベースに入れておいてそれを検索すればよさそうです。

```rb
restaurants = Restaurant.search "ラーメン", limit: 5

# 結果
[42] pry(main)> restaurants.map(&:name)
  Restaurant Load (0.4ms)  SELECT `restaurants`.* FROM `restaurants` WHERE `restaurants`.`id` IN (12742, 14108, 18345, 366402, 388797)
=> ["ラーメンバトルコロシアム", "ラーメンドラゴン", "ラーメンパンダ", "ラーメンショップ", "ラーメンショップ"]
```

# もしかして検索

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/2d0c6e27-78c0-def2-b981-babbe545d1f9.png)

単語を打ち間違えたときに、あっていそうなものを出してくるGoogle Searchでもおなじみのものです。

こちら日本語検索だと、デフォルトのsearchkickのsuggestionsだとうまくいかないので、http://qiita.com/wapa5pow/items/8a19241d39579d939ade で調べてみました。

# ユーザの特性に応じて独自のスコアをつける

省略。いずれ書きます。

# デバッグ

```rb
# resopnseでElasticsearchからのレスポンスがみれる
[57] pry(main)> Restaurant.search("すきやばしこじろう", explain: true).response
  Restaurant Search (17.5ms)  curl http://localhost:9200/restaurants_development/_search?pretty -d '{"query":{"dis_max":{"queries":[{"match":{"name.word_start":{"query":"すきやばしこじろう","boost":10,"operator":"and","analyzer":"searchkick_word_search"}}},{"match":{"name.word_start":{"query":"すきやばしこじろう","boost":1,"operator":"and","analyzer":"searchkick_word_search","fuzziness":1,"prefix_length":0,"max_expansions":3,"fuzzy_transpositions":true}}}]}},"size":1000,"from":0,"explain":true,"fields":[]}'
=> {"took"=>15, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0}, "hits"=>{"total"=>0, "max_score"=>nil, "hits"=>[]}}

# 各種アナザイザーからの返却値
[58] pry(main)> Restaurant.searchkick_index.tokens("すきやばしこじろう", analyzer: "default_index")
=> ["す", "すき", "き", "きや", "や", "やば", "ば", "ばし", "し", "しこ", "こ", "こじ", "じ", "じろ", "ろ", "ろう", "う"]
[59] pry(main)> Restaurant.searchkick_index.tokens("すきやばしこじろう", analyzer: "searchkick_search")
=> ["すき", "きや", "やば", "ばし", "しこ", "こじ", "じろ", "ろう"]
[60] pry(main)> Restaurant.searchkick_index.tokens("すきやばしこじろう", analyzer: "searchkick_search2")
=> ["す", "き", "や", "ば", "し", "こ", "じ", "ろ", "う"]
```



# 参考
* http://qiita.com/wapa5pow/items/02b6b060c4ecfae85105
* [searchkickのanalyzer一覧](https://github.com/ankane/searchkick/blob/master/lib/searchkick/index.rb#L209)