searchkick 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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