searchkickgemをつかってライブドアのレストランデータセットを様々なユースケースで検索してみます。
レストランのデータはActiveRecordを介してすでにデータベースにロードされているとします。
Elasticsearchへのインポートと更新
RestaurantsデータをElasticsearchに入れるには以下のようにします。
Restaurant.reindex
データが更新された際のElasticsearchへの更新方法は同期、非同期と2種類あります。デフォルトは同期です。また関連しているレコードはデフォルトでは更新されません。更新する倍は、after_commitで該当の関連先インスタンスをreindexします。
複数の条件で絞り込む
食べログだと上記のような複数条件の検索です。今回は以下で絞り込みます。
- nameもしくはdescription: ラーメンを含む。ただし、nameを10ブーストする
- エリア: 新宿・代々木(area_id = 3)
- 閉店していない: closed = 0
- 夜あいている: open_late = 1
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)
=> ["どうとんぼり神座", "ラーメン二郎", "ラーメン若月", "一心ラーメン", "ラーメン大"]
特定の店のレビュー数の統計を取る
Amazonで言うと上記のようなイメージです。今回は「どうとんぼり神座(id = 9575)」の結果をとってみます。
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
検索でのオートコンプリーション
Googleサーチでも出てくる検索候補です。ここらへんはある程度データがたまってきたらユーザの検索クエリをデータベースに入れておいてそれを検索すればよさそうです。
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)
=> ["ラーメンバトルコロシアム", "ラーメンドラゴン", "ラーメンパンダ", "ラーメンショップ", "ラーメンショップ"]
もしかして検索
単語を打ち間違えたときに、あっていそうなものを出してくるGoogle Searchでもおなじみのものです。
こちら日本語検索だと、デフォルトのsearchkickのsuggestionsだとうまくいかないので、http://qiita.com/wapa5pow/items/8a19241d39579d939ade で調べてみました。
ユーザの特性に応じて独自のスコアをつける
省略。いずれ書きます。
デバッグ
# 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")
=> ["す", "き", "や", "ば", "し", "こ", "じ", "ろ", "う"]