Commit d83495919997af721f79f0ea2316897c5a56be9d

Authored by Andrew Kane
1 parent df6c952e

Added suggestions

@@ -18,6 +18,7 @@ Plus: @@ -18,6 +18,7 @@ Plus:
18 - reindex without downtime 18 - reindex without downtime
19 - easily personalize results for each user [master branch] 19 - easily personalize results for each user [master branch]
20 - autocomplete [master branch] 20 - autocomplete [master branch]
  21 +- “Did you mean” suggestions [master branch]
21 22
22 :tangerine: Battle-tested at [Instacart](https://www.instacart.com) 23 :tangerine: Battle-tested at [Instacart](https://www.instacart.com)
23 24
@@ -244,11 +245,30 @@ Reindex and search with: @@ -244,11 +245,30 @@ Reindex and search with:
244 Product.search "milk", user_id: 8 245 Product.search "milk", user_id: 8
245 ``` 246 ```
246 247
  248 +### Suggestions [master branch]
  249 +
  250 +Did you mean: :sunglasses:
  251 +
  252 +```ruby
  253 +class Product < ActiveRecord::Base
  254 + searchkick suggest: [:name] # fields to generate suggestions
  255 +end
  256 +```
  257 +
  258 +Reindex and search with:
  259 +
  260 +```ruby
  261 +products = Product.search "peantu butta", suggest: true
  262 +products.suggestion # peanut butter
  263 +```
  264 +
  265 +Returns `nil` when there are no suggestions.
  266 +
247 ### Facets 267 ### Facets
248 268
249 ```ruby 269 ```ruby
250 -search = Product.search "2% Milk", facets: [:store_id, :aisle_id]  
251 -p search.facets 270 +products = Product.search "2% Milk", facets: [:store_id, :aisle_id]
  271 +p products.facets
252 ``` 272 ```
253 273
254 Advanced 274 Advanced
@@ -371,8 +391,6 @@ Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire), Jaroslav Kali @@ -371,8 +391,6 @@ Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire), Jaroslav Kali
371 ## TODO 391 ## TODO
372 392
373 - Make Searchkick work with any language 393 - Make Searchkick work with any language
374 -- Built-in synonyms from WordNet  
375 -- [Did you mean?](http://www.elasticsearch.org/guide/reference/api/search/suggest/)  
376 394
377 ## Contributing 395 ## Contributing
378 396
lib/searchkick.rb
  1 +require "tire"
1 require "searchkick/version" 2 require "searchkick/version"
2 require "searchkick/reindex" 3 require "searchkick/reindex"
  4 +require "searchkick/results"
3 require "searchkick/search" 5 require "searchkick/search"
4 require "searchkick/model" 6 require "searchkick/model"
5 require "searchkick/tasks" 7 require "searchkick/tasks"
6 -require "tire"  
7 8
8 # TODO find better ActiveModel hook 9 # TODO find better ActiveModel hook
9 ActiveModel::AttributeMethods::ClassMethods.send(:include, Searchkick::Model) 10 ActiveModel::AttributeMethods::ClassMethods.send(:include, Searchkick::Model)
lib/searchkick/reindex.rb
@@ -137,16 +137,24 @@ module Searchkick @@ -137,16 +137,24 @@ module Searchkick
137 } 137 }
138 end 138 end
139 139
140 - # autocomplete  
141 - (options[:autocomplete] || []).each do |field|  
142 - mapping[field] = { 140 + # autocomplete and suggest
  141 + autocomplete = options[:autocomplete] || []
  142 + suggest = options[:suggest] || []
  143 + (autocomplete + suggest).uniq.each do |field|
  144 + field_mapping = {
143 type: "multi_field", 145 type: "multi_field",
144 fields: { 146 fields: {
145 field => {type: "string", index: "not_analyzed"}, 147 field => {type: "string", index: "not_analyzed"},
146 - "analyzed" => {type: "string", index: "analyzed"},  
147 - "autocomplete" => {type: "string", index: "analyzed", analyzer: "searchkick_autocomplete_index"} 148 + "analyzed" => {type: "string", index: "analyzed"}
148 } 149 }
149 } 150 }
  151 + if autocomplete.include?(field)
  152 + field_mapping[:fields]["autocomplete"] = {type: "string", index: "analyzed", analyzer: "searchkick_autocomplete_index"}
  153 + end
  154 + if suggest.include?(field)
  155 + field_mapping[:fields]["suggest"] = {type: "string", index: "analyzed", analyzer: "standard"}
  156 + end
  157 + mapping[field] = field_mapping
150 end 158 end
151 159
152 mappings = { 160 mappings = {
lib/searchkick/results.rb 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +module Searchkick
  2 + class Results < Tire::Results::Collection
  3 +
  4 + # TODO use all fields
  5 + # return nil suggestion if term does not change
  6 + def suggestion
  7 + if @response["suggest"]
  8 + original_term = options[:term].downcase
  9 + suggestion = original_term.dup
  10 + @response["suggest"].values.first.each do |s|
  11 + first_option = s["options"].first
  12 + if first_option
  13 + suggestion.sub!(s["text"], first_option["text"])
  14 + end
  15 + end
  16 + suggestion == original_term ? nil : suggestion
  17 + else
  18 + raise "Pass `suggest: true` to the search method for suggestions"
  19 + end
  20 + end
  21 +
  22 + end
  23 +end
lib/searchkick/search.rb
@@ -17,19 +17,22 @@ module Searchkick @@ -17,19 +17,22 @@ module Searchkick
17 ["_all"] 17 ["_all"]
18 end 18 end
19 end 19 end
  20 +
20 operator = options[:partial] ? "or" : "and" 21 operator = options[:partial] ? "or" : "and"
  22 +
  23 + # model and eagar loading
21 load = options[:load].nil? ? true : options[:load] 24 load = options[:load].nil? ? true : options[:load]
22 load = (options[:include] ? {include: options[:include]} : true) if load 25 load = (options[:include] ? {include: options[:include]} : true) if load
  26 +
  27 + # pagination
23 page = options.has_key?(:page) ? [options[:page].to_i, 1].max : nil 28 page = options.has_key?(:page) ? [options[:page].to_i, 1].max : nil
24 - tire_options = {  
25 - load: load,  
26 - page: page,  
27 - per_page: options[:limit] || options[:per_page] || 100000 # return all  
28 - }  
29 - tire_options[:index] = options[:index_name] if options[:index_name] 29 + per_page = options[:limit] || options[:per_page]
  30 + offset = options[:offset] || (page && per_page && (page - 1) * per_page)
  31 + index_name = options[:index_name] || index.name
30 32
31 - collection =  
32 - tire.search tire_options do 33 + # TODO lose Tire DSL for more flexibility
  34 + s =
  35 + Tire::Search::Search.new do
33 query do 36 query do
34 custom_filters_score do 37 custom_filters_score do
35 query do 38 query do
@@ -82,7 +85,8 @@ module Searchkick @@ -82,7 +85,8 @@ module Searchkick
82 score_mode "total" 85 score_mode "total"
83 end 86 end
84 end 87 end
85 - from options[:offset] if options[:offset] 88 + size per_page if per_page
  89 + from offset if offset
86 explain options[:explain] if options[:explain] 90 explain options[:explain] if options[:explain]
87 91
88 # order 92 # order
@@ -173,7 +177,24 @@ module Searchkick @@ -173,7 +177,24 @@ module Searchkick
173 end 177 end
174 end 178 end
175 179
176 - collection 180 + payload = s.to_hash
  181 +
  182 + # suggested fields
  183 + suggest_fields = options[:fields] || @searchkick_options[:suggest] || []
  184 + if options[:suggest] and suggest_fields.any?
  185 + payload[:suggest] = {text: term}
  186 + suggest_fields.each do |field|
  187 + payload[:suggest][field] = {
  188 + term: {
  189 + field: "#{field}.suggest",
  190 + suggest_mode: "popular"
  191 + }
  192 + }
  193 + end
  194 + end
  195 +
  196 + search = Tire::Search::Search.new(index_name, load: load, payload: payload)
  197 + Searchkick::Results.new(search.json, search.options.merge(term: term))
177 end 198 end
178 199
179 end 200 end
test/match_test.rb
@@ -127,4 +127,26 @@ class TestMatch &lt; Minitest::Unit::TestCase @@ -127,4 +127,26 @@ class TestMatch &lt; Minitest::Unit::TestCase
127 assert_search "hum", ["Hummus"], autocomplete: true, fields: [:name] 127 assert_search "hum", ["Hummus"], autocomplete: true, fields: [:name]
128 end 128 end
129 129
  130 + # suggest
  131 +
  132 + def test_suggest
  133 + store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
  134 + assert_suggest "How Big is a Tigre Shar?", "how big is a tiger shark?"
  135 + end
  136 +
  137 + def test_suggest_perfect
  138 + store_names ["Tiger Shark", "Great White Shark"]
  139 + assert_suggest "Tiger Shark", nil # no correction
  140 + end
  141 +
  142 + def test_suggest_without_option
  143 + assert_raises(RuntimeError){ Product.search("hi").suggestion }
  144 + end
  145 +
  146 + protected
  147 +
  148 + def assert_suggest(term, expected)
  149 + assert_equal expected, Product.search(term, suggest: true).suggestion
  150 + end
  151 +
130 end 152 end
test/test_helper.rb
@@ -48,7 +48,8 @@ class Product &lt; ActiveRecord::Base @@ -48,7 +48,8 @@ class Product &lt; ActiveRecord::Base
48 ["burger", "hamburger"], 48 ["burger", "hamburger"],
49 ["bandaid", "bandag"] 49 ["bandaid", "bandag"]
50 ], 50 ],
51 - autocomplete: [:name] 51 + autocomplete: [:name],
  52 + suggest: [:name]
52 53
53 attr_accessor :conversions, :user_ids 54 attr_accessor :conversions, :user_ids
54 55