searchkick 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2b43a0209f8d9f4e32f3c21c190ea82d5f2a0b4b
4
- data.tar.gz: 016d4568bfd949ba047e6ce59024f795861f24d6
3
+ metadata.gz: 9afc0023095cdc587ed6df4faedb9c6ba0927b61
4
+ data.tar.gz: ff38cab705eb7d2692e81510ff237707820f1bda
5
5
  SHA512:
6
- metadata.gz: 76307b462984c715a68d9e0e71edd0139811a3274ad9682ad97034ac2ceb9f9b74b5094750a53ac8c449db5f1b4c5d6be6f5cdb22e1aa19a8b21fd7230f31798
7
- data.tar.gz: 236fbf2c4e83877a03eed6728263b6af62094ee1139e88fd3ec86a4fd6da8269fcd2972f29f5bf2bafd3e667f7ee5ce40945509ea56c24f4d3746e4fed688be6
6
+ metadata.gz: af68e2a80fc2f8cead9075b8e2e2928ab81dcf98952949f2901d24f151de235b5e648316dce80c84b9197d6b549efc43fb9ed1f04e8f79a8a834e671e68171aa
7
+ data.tar.gz: b0a23d33695f2b82d84eb2152b07ca43d610ff1ba2200b917baaba61c3a7e8e7292161108270800afba3673484fefc8f19a7757ff2cbac0d131e7c36cccd3a5c
data/README.md CHANGED
@@ -16,6 +16,9 @@ Plus:
16
16
 
17
17
  - query like SQL - no need to learn a new query language
18
18
  - reindex without downtime
19
+ - easily personalize results for each user
20
+ - autocomplete
21
+ - “Did you mean” suggestions
19
22
 
20
23
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com)
21
24
 
@@ -129,6 +132,24 @@ To change this, use:
129
132
  Product.search "fresh honey", partial: true # fresh OR honey
130
133
  ```
131
134
 
135
+ ### Autocomplete
136
+
137
+ ![Autocomplete](http://ankane.github.io/searchkick/autocomplete.png)
138
+
139
+ You must specify which fields use this feature since this can increase the index size significantly. Don’t worry - this gives you blazing faster queries.
140
+
141
+ ```ruby
142
+ class Website < ActiveRecord::Base
143
+ searchkick autocomplete: ["title"]
144
+ end
145
+ ```
146
+
147
+ Reindex and search with:
148
+
149
+ ```ruby
150
+ Website.search "where", autocomplete: true
151
+ ```
152
+
132
153
  ### Synonyms
133
154
 
134
155
  ```ruby
@@ -137,11 +158,11 @@ class Product < ActiveRecord::Base
137
158
  end
138
159
  ```
139
160
 
140
- You must call `Product.reindex` after changing synonyms.
161
+ Call `Product.reindex` after changing synonyms.
141
162
 
142
163
  ### Indexing
143
164
 
144
- Control what data is indexed with the `search_data` method.
165
+ Control what data is indexed with the `search_data` method. Call `Product.reindex` after changing this method.
145
166
 
146
167
  ```ruby
147
168
  class Product < ActiveRecord::Base
@@ -182,14 +203,19 @@ end
182
203
 
183
204
  Add conversions to the index.
184
205
 
206
+ **Note**: You must specify the conversions field as of version `0.2.0`.
207
+
185
208
  ```ruby
186
209
  class Product < ActiveRecord::Base
187
210
  has_many :searches
188
211
 
212
+ searchkick conversions: "conversions" # name of field
213
+
189
214
  def search_data
190
215
  {
191
216
  name: name,
192
217
  conversions: searches.group("query").count
218
+ # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
193
219
  }
194
220
  end
195
221
  end
@@ -201,11 +227,56 @@ Reindex and set up a cron job to add new conversions daily.
201
227
  rake searchkick:reindex CLASS=Product
202
228
  ```
203
229
 
230
+ ### Personalized Results
231
+
232
+ Order results differently for each user. For example, show a user’s previously purchased products before other results.
233
+
234
+ ```ruby
235
+ class Product < ActiveRecord::Base
236
+ searchkick personalize: "user_ids"
237
+
238
+ def search_data
239
+ {
240
+ name: name,
241
+ user_ids: orders.pluck(:user_id) # boost this product for these users
242
+ # [4, 8, 15, 16, 23, 42]
243
+ }
244
+ end
245
+ end
246
+ ```
247
+
248
+ Reindex and search with:
249
+
250
+ ```ruby
251
+ Product.search "milk", user_id: 8
252
+ ```
253
+
254
+ ### Suggestions
255
+
256
+ ![Suggest](http://ankane.github.io/searchkick/recursion.png)
257
+
258
+ ```ruby
259
+ class Product < ActiveRecord::Base
260
+ searchkick suggest: ["name"] # fields to generate suggestions
261
+ end
262
+ ```
263
+
264
+ Reindex and search with:
265
+
266
+ ```ruby
267
+ products = Product.search "peantu butta", suggest: true
268
+ products.suggestions # ["peanut butter"]
269
+ ```
270
+
204
271
  ### Facets
205
272
 
273
+ [Facets](http://www.elasticsearch.org/guide/reference/api/search/facets/) provide aggregated search data.
274
+
275
+ ![Facets](http://ankane.github.io/searchkick/facets.png)
276
+
206
277
  ```ruby
207
- search = Product.search "2% Milk", facets: [:store_id, :aisle_id]
208
- p search.facets
278
+ products = Product.search "chuck taylor", facets: [:product_type, :gender, :brand]
279
+ p products.facets
209
280
  ```
210
281
 
211
282
  Advanced
@@ -323,13 +394,11 @@ end
323
394
 
324
395
  ## Thanks
325
396
 
326
- Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire) and Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884).
397
+ Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire), Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884), and Alex Leschenko for [Elasticsearch autocomplete](https://github.com/leschenko/elasticsearch_autocomplete).
327
398
 
328
399
  ## TODO
329
400
 
330
- - Custom results for each user
331
401
  - Make Searchkick work with any language
332
- - Built-in synonyms from WordNet
333
402
 
334
403
  ## Contributing
335
404
 
data/lib/searchkick.rb CHANGED
@@ -1,9 +1,10 @@
1
+ require "tire"
1
2
  require "searchkick/version"
2
3
  require "searchkick/reindex"
4
+ require "searchkick/results"
3
5
  require "searchkick/search"
4
6
  require "searchkick/model"
5
7
  require "searchkick/tasks"
6
- require "tire"
7
8
 
8
9
  # TODO find better ActiveModel hook
9
10
  ActiveModel::AttributeMethods::ClassMethods.send(:include, Searchkick::Model)
@@ -3,7 +3,6 @@ module Searchkick
3
3
 
4
4
  def searchkick(options = {})
5
5
  @searchkick_options = options.dup
6
- @searchkick_options[:conversions] = true if options[:conversions].nil?
7
6
 
8
7
  class_eval do
9
8
  extend Searchkick::Search
@@ -24,9 +23,23 @@ module Searchkick
24
23
 
25
24
  def to_indexed_json
26
25
  source = search_data
27
- if self.class.instance_variable_get("@searchkick_options")[:conversions] and source[:conversions]
28
- source[:conversions] = source[:conversions].map{|k, v| {query: k, count: v} }
26
+
27
+ # stringify fields
28
+ source = source.inject({}){|memo,(k,v)| memo[k.to_s] = v; memo}
29
+
30
+ options = self.class.instance_variable_get("@searchkick_options")
31
+
32
+ # conversions
33
+ conversions_field = options[:conversions]
34
+ if conversions_field and source[conversions_field]
35
+ source[conversions_field] = source[conversions_field].map{|k, v| {query: k, count: v} }
29
36
  end
37
+
38
+ # hack to prevent generator field doesn't exist error
39
+ (options[:suggest] || []).map(&:to_s).each do |field|
40
+ source[field] = "a" if !source[field]
41
+ end
42
+
30
43
  source.to_json
31
44
  end
32
45
  end
@@ -7,7 +7,8 @@ module Searchkick
7
7
  new_index = alias_name + "_" + Time.now.strftime("%Y%m%d%H%M%S")
8
8
  index = Tire::Index.new(new_index)
9
9
 
10
- index.create searchkick_index_options
10
+ success = index.create searchkick_index_options
11
+ raise index.response.to_s if !success
11
12
 
12
13
  # use scope for import
13
14
  scope = respond_to?(:search_import) ? search_import : self
@@ -67,6 +68,22 @@ module Searchkick
67
68
  type: "custom",
68
69
  tokenizer: "standard",
69
70
  filter: ["standard", "lowercase", "asciifolding", "stop", "snowball"]
71
+ },
72
+ # https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
73
+ searchkick_autocomplete_index: {
74
+ type: "custom",
75
+ tokenizer: "searchkick_autocomplete_ngram",
76
+ filter: ["lowercase", "asciifolding"]
77
+ },
78
+ searchkick_autocomplete_search: {
79
+ type: "custom",
80
+ tokenizer: "keyword",
81
+ filter: ["lowercase", "asciifolding"]
82
+ },
83
+ searchkick_suggest_index: {
84
+ type: "custom",
85
+ tokenizer: "standard",
86
+ filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
70
87
  }
71
88
  },
72
89
  filter: {
@@ -80,28 +97,45 @@ module Searchkick
80
97
  token_separator: "",
81
98
  output_unigrams: false,
82
99
  output_unigrams_if_no_shingles: true
100
+ },
101
+ searchkick_suggest_shingle: {
102
+ type: "shingle",
103
+ max_shingle_size: 5
104
+ }
105
+ },
106
+ tokenizer: {
107
+ searchkick_autocomplete_ngram: {
108
+ type: "edgeNGram",
109
+ min_gram: 1,
110
+ max_gram: 50
83
111
  }
84
112
  }
85
113
  }
86
114
  }.merge(options[:settings] || {})
115
+
116
+ # synonyms
87
117
  synonyms = options[:synonyms] || []
88
118
  if synonyms.any?
89
119
  settings[:analysis][:filter][:searchkick_synonym] = {
90
120
  type: "synonym",
91
- ignore_case: true,
92
- synonyms: synonyms.select{|s| s.size > 1 }.map{|s| "#{s[0..-2].join(",")} => #{s[-1]}" }
121
+ synonyms: synonyms.select{|s| s.size > 1 }.map{|s| s.join(",") }
93
122
  }
94
123
  # choosing a place for the synonym filter when stemming is not easy
95
124
  # https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
96
125
  # TODO use a snowball stemmer on synonyms when creating the token filter
97
- settings[:analysis][:analyzer][:default_index][:filter].insert(-4, "searchkick_synonym")
126
+
127
+ # http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
128
+ # I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
129
+ # - Only apply the synonym expansion at index time
130
+ # - Don't have the synonym filter applied search
131
+ # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
132
+ settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_synonym")
98
133
  settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_synonym"
99
- settings[:analysis][:analyzer][:searchkick_search][:filter].insert(-4, "searchkick_synonym")
100
- settings[:analysis][:analyzer][:searchkick_search][:filter] << "searchkick_synonym"
101
- settings[:analysis][:analyzer][:searchkick_search2][:filter] << "searchkick_synonym"
102
134
  end
103
135
 
104
136
  mapping = {}
137
+
138
+ # conversions
105
139
  if options[:conversions]
106
140
  mapping[:conversions] = {
107
141
  type: "nested",
@@ -112,6 +146,26 @@ module Searchkick
112
146
  }
113
147
  end
114
148
 
149
+ # autocomplete and suggest
150
+ autocomplete = (options[:autocomplete] || []).map(&:to_s)
151
+ suggest = (options[:suggest] || []).map(&:to_s)
152
+ (autocomplete + suggest).uniq.each do |field|
153
+ field_mapping = {
154
+ type: "multi_field",
155
+ fields: {
156
+ field => {type: "string", index: "not_analyzed"},
157
+ "analyzed" => {type: "string", index: "analyzed"}
158
+ }
159
+ }
160
+ if autocomplete.include?(field)
161
+ field_mapping[:fields]["autocomplete"] = {type: "string", index: "analyzed", analyzer: "searchkick_autocomplete_index"}
162
+ end
163
+ if suggest.include?(field)
164
+ field_mapping[:fields]["suggest"] = {type: "string", index: "analyzed", analyzer: "searchkick_suggest_index"}
165
+ end
166
+ mapping[field] = field_mapping
167
+ end
168
+
115
169
  mappings = {
116
170
  document_type.to_sym => {
117
171
  properties: mapping,
@@ -0,0 +1,17 @@
1
+ module Searchkick
2
+ class Results < Tire::Results::Collection
3
+
4
+ def suggestions
5
+ if @response["suggest"]
6
+ @response["suggest"].values.flat_map{|v| v.first["options"] }.sort_by{|o| -o["score"] }.map{|o| o["text"] }.uniq
7
+ else
8
+ raise "Pass `suggest: true` to the search method for suggestions"
9
+ end
10
+ end
11
+
12
+ # fixes deprecation warning
13
+ def __find_records_by_ids(klass, ids)
14
+ @options[:load] === true ? klass.find(ids) : klass.includes(@options[:load][:include]).find(ids)
15
+ end
16
+ end
17
+ end
@@ -3,56 +3,93 @@ module Searchkick
3
3
 
4
4
  def search(term, options = {})
5
5
  term = term.to_s
6
- fields = options[:fields] ? options[:fields].map{|f| "#{f}.analyzed" } : ["_all"]
6
+ fields =
7
+ if options[:fields]
8
+ if options[:autocomplete]
9
+ options[:fields].map{|f| "#{f}.autocomplete" }
10
+ else
11
+ options[:fields].map{|f| "#{f}.analyzed" }
12
+ end
13
+ else
14
+ if options[:autocomplete]
15
+ (@searchkick_options[:autocomplete] || []).map{|f| "#{f}.autocomplete" }
16
+ else
17
+ ["_all"]
18
+ end
19
+ end
20
+
7
21
  operator = options[:partial] ? "or" : "and"
22
+
23
+ # model and eagar loading
8
24
  load = options[:load].nil? ? true : options[:load]
9
25
  load = (options[:include] ? {include: options[:include]} : true) if load
26
+
27
+ # pagination
10
28
  page = options.has_key?(:page) ? [options[:page].to_i, 1].max : nil
11
- tire_options = {
12
- load: load,
13
- page: page,
14
- per_page: options[:limit] || options[:per_page] || 100000 # return all
15
- }
16
- tire_options[:index] = options[:index_name] if options[:index_name]
17
-
18
- collection =
19
- tire.search tire_options do
29
+ per_page = options[:limit] || options[:per_page] || 100000
30
+ offset = options[:offset] || (page && (page - 1) * per_page)
31
+ index_name = options[:index_name] || index.name
32
+
33
+ conversions_field = @searchkick_options[:conversions]
34
+ personalize_field = @searchkick_options[:personalize]
35
+
36
+ # TODO lose Tire DSL for more flexibility
37
+ s =
38
+ Tire::Search::Search.new do
20
39
  query do
21
- boolean do
22
- must do
23
- # TODO escape boost field
24
- score_script = options[:boost] ? "_score * log(doc['#{options[:boost]}'].value + 2.718281828)" : "_score"
25
- custom_score script: score_script do
26
- dis_max do
27
- query do
28
- match fields, term, boost: 10, operator: operator, analyzer: "searchkick_search"
29
- end
30
- query do
31
- match fields, term, boost: 10, operator: operator, analyzer: "searchkick_search2"
32
- end
33
- query do
34
- match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search"
35
- end
36
- query do
37
- match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search2"
40
+ custom_filters_score do
41
+ query do
42
+ boolean do
43
+ must do
44
+ if options[:autocomplete]
45
+ match fields, term, analyzer: "searchkick_autocomplete_search"
46
+ else
47
+ dis_max do
48
+ query do
49
+ match fields, term, use_dis_max: false, boost: 10, operator: operator, analyzer: "searchkick_search"
50
+ end
51
+ query do
52
+ match fields, term, use_dis_max: false, boost: 10, operator: operator, analyzer: "searchkick_search2"
53
+ end
54
+ query do
55
+ match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search"
56
+ end
57
+ query do
58
+ match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search2"
59
+ end
60
+ end
38
61
  end
39
62
  end
40
- end
41
- end
42
- unless options[:conversions] == false
43
- should do
44
- nested path: "conversions", score_mode: "total" do
45
- query do
46
- custom_score script: "doc['count'].value" do
47
- match "query", term
63
+ if conversions_field and options[:conversions] != false
64
+ should do
65
+ nested path: conversions_field, score_mode: "total" do
66
+ query do
67
+ custom_score script: "doc['count'].value" do
68
+ match "query", term
69
+ end
70
+ end
48
71
  end
49
72
  end
50
73
  end
51
74
  end
52
75
  end
76
+ if options[:boost]
77
+ filter do
78
+ filter :exists, field: options[:boost]
79
+ script "log(doc['#{options[:boost]}'].value + 2.718281828)"
80
+ end
81
+ end
82
+ if options[:user_id] and personalize_field
83
+ filter do
84
+ filter :term, personalize_field => options[:user_id]
85
+ boost 100
86
+ end
87
+ end
88
+ score_mode "total"
53
89
  end
54
90
  end
55
- from options[:offset] if options[:offset]
91
+ size per_page
92
+ from offset if offset
56
93
  explain options[:explain] if options[:explain]
57
94
 
58
95
  # order
@@ -143,7 +180,27 @@ module Searchkick
143
180
  end
144
181
  end
145
182
 
146
- collection
183
+ payload = s.to_hash
184
+
185
+ # suggestions
186
+ if options[:suggest]
187
+ suggest_fields = (@searchkick_options[:suggest] || []).map(&:to_s)
188
+ # intersection
189
+ suggest_fields = suggest_fields & options[:fields].map(&:to_s) if options[:fields]
190
+ if suggest_fields.any?
191
+ payload[:suggest] = {text: term}
192
+ suggest_fields.each do |field|
193
+ payload[:suggest][field] = {
194
+ phrase: {
195
+ field: "#{field}.suggest"
196
+ }
197
+ }
198
+ end
199
+ end
200
+ end
201
+
202
+ search = Tire::Search::Search.new(index_name, load: load, payload: payload)
203
+ Searchkick::Results.new(search.json, search.options.merge(term: term))
147
204
  end
148
205
 
149
206
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/boost_test.rb CHANGED
@@ -46,4 +46,14 @@ class TestBoost < Minitest::Unit::TestCase
46
46
  assert_order "product", ["Product Conversions", "Product Boost"], boost: "orders_count"
47
47
  end
48
48
 
49
+ def test_user_id
50
+ store [
51
+ {name: "Tomato A"},
52
+ {name: "Tomato B", user_ids: [1, 2, 3]},
53
+ {name: "Tomato C"},
54
+ {name: "Tomato D"}
55
+ ]
56
+ assert_first "tomato", "Tomato B", user_id: 2
57
+ end
58
+
49
59
  end
data/test/match_test.rb CHANGED
@@ -60,14 +60,21 @@ class TestMatch < Minitest::Unit::TestCase
60
60
  assert_search "fin", ["Finn"]
61
61
  end
62
62
 
63
- def test_edit_distance
63
+ def test_edit_distance_two
64
64
  store_names ["Bingo"]
65
65
  assert_search "bin", []
66
+ assert_search "bingooo", []
67
+ assert_search "mango", []
68
+ end
69
+
70
+ def test_edit_distance_one
71
+ store_names ["Bingo"]
66
72
  assert_search "bing", ["Bingo"]
67
73
  assert_search "bingoo", ["Bingo"]
68
- assert_search "bingooo", []
69
74
  assert_search "ringo", ["Bingo"]
70
- assert_search "mango", []
75
+ end
76
+
77
+ def test_edit_distance_long_word
71
78
  store_names ["thisisareallylongword"]
72
79
  assert_search "thisisareallylongwor", ["thisisareallylongword"] # missing letter
73
80
  assert_search "thisisareelylongword", [] # edit distance = 2
@@ -110,4 +117,21 @@ class TestMatch < Minitest::Unit::TestCase
110
117
  assert_search "almondmilks", ["Almond Milk"]
111
118
  end
112
119
 
120
+ # autocomplete
121
+
122
+ def test_autocomplete
123
+ store_names ["Hummus"]
124
+ assert_search "hum", ["Hummus"], autocomplete: true
125
+ end
126
+
127
+ def test_autocomplete_two_words
128
+ store_names ["Organic Hummus"]
129
+ assert_search "hum", [], autocomplete: true
130
+ end
131
+
132
+ def test_autocomplete_fields
133
+ store_names ["Hummus"]
134
+ assert_search "hum", ["Hummus"], autocomplete: true, fields: [:name]
135
+ end
136
+
113
137
  end
data/test/sql_test.rb CHANGED
@@ -7,6 +7,12 @@ class TestSql < Minitest::Unit::TestCase
7
7
  assert_order "product", ["Product A", "Product B"], order: {name: :asc}, limit: 2
8
8
  end
9
9
 
10
+ def test_no_limit
11
+ names = 20.times.map{|i| "Product #{i}" }
12
+ store_names names
13
+ assert_search "product", names
14
+ end
15
+
10
16
  def test_offset
11
17
  store_names ["Product A", "Product B", "Product C", "Product D"]
12
18
  assert_order "product", ["Product C", "Product D"], order: {name: :asc}, offset: 2
@@ -81,6 +87,19 @@ class TestSql < Minitest::Unit::TestCase
81
87
  assert_search "blue", ["red"], fields: ["color"]
82
88
  end
83
89
 
90
+ def test_non_existent_field
91
+ store_names ["Milk"]
92
+ assert_search "milk", [], fields: ["not_here"]
93
+ end
94
+
95
+ def test_fields_both_match
96
+ store [
97
+ {name: "Blue A", color: "red"},
98
+ {name: "Blue B", color: "light blue"}
99
+ ]
100
+ assert_first "blue", "Blue B", fields: [:name, :color]
101
+ end
102
+
84
103
  # load
85
104
 
86
105
  def test_load_default
@@ -0,0 +1,70 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestSuggest < Minitest::Unit::TestCase
4
+
5
+ def test_basic
6
+ store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
7
+ assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark"
8
+ end
9
+
10
+ def test_perfect
11
+ store_names ["Tiger Shark", "Great White Shark"]
12
+ assert_suggest "Tiger Shark", nil # no correction
13
+ end
14
+
15
+ def test_phrase
16
+ store_names ["Big Tiger Shark", "Tiger Sharp Teeth", "Tiger Sharp Mind"]
17
+ assert_suggest "How to catch a big tiger shar", "how to catch a big tiger shark"
18
+ end
19
+
20
+ def test_without_option
21
+ assert_raises(RuntimeError){ Product.search("hi").suggestions }
22
+ end
23
+
24
+ def test_multiple_fields
25
+ store [
26
+ {name: "Shark", color: "Sharp"}
27
+ ]
28
+ assert_suggest_all "shar", ["shark", "sharp"]
29
+ end
30
+
31
+ def test_multiple_fields_highest_score_first
32
+ store [
33
+ {name: "Tiger Shark", color: "Sharp"}
34
+ ]
35
+ assert_suggest "tiger shar", "tiger shark"
36
+ end
37
+
38
+ def test_multiple_fields_same_value
39
+ store [
40
+ {name: "Shark", color: "Shark"}
41
+ ]
42
+ assert_suggest_all "shar", ["shark"]
43
+ end
44
+
45
+ def test_fields_option
46
+ store [
47
+ {name: "Shark", color: "Sharp"}
48
+ ]
49
+ assert_suggest_all "shar", ["shark"], fields: [:name]
50
+ end
51
+
52
+ def test_fields_option_multiple
53
+ store [
54
+ {name: "Shark"}
55
+ ]
56
+ assert_suggest "shar", "shark", fields: [:name, :unknown]
57
+ end
58
+
59
+ protected
60
+
61
+ def assert_suggest(term, expected, options = {})
62
+ assert_equal expected, Product.search(term, options.merge(suggest: true)).suggestions.first
63
+ end
64
+
65
+ # any order
66
+ def assert_suggest_all(term, expected, options = {})
67
+ assert_equal expected.sort, Product.search(term, options.merge(suggest: true)).suggestions.sort
68
+ end
69
+
70
+ end
data/test/test_helper.rb CHANGED
@@ -44,15 +44,19 @@ class Product < ActiveRecord::Base
44
44
  ["clorox", "bleach"],
45
45
  ["scallion", "greenonion"],
46
46
  ["saranwrap", "plasticwrap"],
47
- ["qtip", "cotton swab"],
47
+ ["qtip", "cottonswab"],
48
48
  ["burger", "hamburger"],
49
49
  ["bandaid", "bandag"]
50
- ]
50
+ ],
51
+ autocomplete: [:name],
52
+ suggest: [:name, :color],
53
+ conversions: "conversions",
54
+ personalize: "user_ids"
51
55
 
52
- attr_accessor :conversions
56
+ attr_accessor :conversions, :user_ids
53
57
 
54
58
  def search_data
55
- as_json.merge conversions: conversions
59
+ as_json.merge conversions: conversions, user_ids: user_ids
56
60
  end
57
61
  end
58
62
 
@@ -70,7 +74,7 @@ class MiniTest::Unit::TestCase
70
74
  protected
71
75
 
72
76
  def store(documents)
73
- documents.each do |document|
77
+ documents.shuffle.each do |document|
74
78
  Product.create!(document)
75
79
  end
76
80
  Product.index.refresh
@@ -89,4 +93,8 @@ class MiniTest::Unit::TestCase
89
93
  assert_equal expected, Product.search(term, options).map(&:name)
90
94
  end
91
95
 
96
+ def assert_first(term, expected, options = {})
97
+ assert_equal expected, Product.search(term, options).map(&:name).first
98
+ end
99
+
92
100
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-04 00:00:00.000000000 Z
11
+ date: 2013-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tire
@@ -109,6 +109,7 @@ files:
109
109
  - lib/searchkick.rb
110
110
  - lib/searchkick/model.rb
111
111
  - lib/searchkick/reindex.rb
112
+ - lib/searchkick/results.rb
112
113
  - lib/searchkick/search.rb
113
114
  - lib/searchkick/tasks.rb
114
115
  - lib/searchkick/version.rb
@@ -117,6 +118,7 @@ files:
117
118
  - test/facets_test.rb
118
119
  - test/match_test.rb
119
120
  - test/sql_test.rb
121
+ - test/suggest_test.rb
120
122
  - test/synonyms_test.rb
121
123
  - test/test_helper.rb
122
124
  homepage: https://github.com/ankane/searchkick
@@ -148,5 +150,6 @@ test_files:
148
150
  - test/facets_test.rb
149
151
  - test/match_test.rb
150
152
  - test/sql_test.rb
153
+ - test/suggest_test.rb
151
154
  - test/synonyms_test.rb
152
155
  - test/test_helper.rb