searchkick 0.9.0 → 0.9.1

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: 086a7f84d9047b486d127d05cc3d20b7b6d66b04
4
- data.tar.gz: f0896d8115143f72cf088f69f82c923c6e4f67cf
3
+ metadata.gz: 0227952cfe570c64d03eacd4db89689ec4a0b2d5
4
+ data.tar.gz: 14d2061a47667de91432894f8ef220b1a73bec3c
5
5
  SHA512:
6
- metadata.gz: f084aa390d924d64c610ea86845433bb521b8ad319e50350936f56d57f3db770e01e87c34c55d38f88b58599b03d6176a8af6e4fee8793fc5f1b8fb01cfeea6e
7
- data.tar.gz: e26a6809754329de5cf67a473f3df2d5b8b19b1981bdb22c041421b4916a1efcf5d4f78501cd9656ba297ccdcb0e3fe1742d0d57ffee1e0bf8475104045367fb
6
+ metadata.gz: d9e12c3156a856a53d7b5157f40b167dce9ff05949f5312f34100222b6769801ab602baba3ca10aec2e80db21e2ae0410229120d865f0682a7167827b4bb7b82
7
+ data.tar.gz: cbfbbaca6c443d5f319a97342d1969f85d94749b5662f86b3da557d310a9bd6ed02fc1d7af54e8f2ca0be703bf35c8bb49164bf4d13195e348c221708e8e9793
data/.travis.yml CHANGED
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
- rvm:
3
- - 2.1
2
+ rvm: 2.2
4
3
  services:
5
4
  - elasticsearch
6
5
  - mongodb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.9.1
2
+
3
+ - `and` now matches `&`
4
+ - Added `transpositions` option to misspellings
5
+ - Added `boost_mode` and `log` options to `boost_by`
6
+ - Added `prefix_length` option to `misspellings`
7
+ - Added ability to set env
8
+
1
9
  ## 0.9.0
2
10
 
3
11
  - Much better performance for where queries if no facets
data/README.md CHANGED
@@ -64,7 +64,7 @@ Product.reindex
64
64
  And to query, use:
65
65
 
66
66
  ```ruby
67
- products = Product.search "2% Milk"
67
+ products = Product.search "apples"
68
68
  products.each do |product|
69
69
  puts product.name
70
70
  end
@@ -77,7 +77,7 @@ Searchkick supports the complete [Elasticsearch Search API](http://www.elasticse
77
77
  Query like SQL
78
78
 
79
79
  ```ruby
80
- Product.search "2% Milk", where: {in_stock: true}, limit: 10, offset: 50
80
+ Product.search "apples", where: {in_stock: true}, limit: 10, offset: 50
81
81
  ```
82
82
 
83
83
  Search specific fields
@@ -96,6 +96,7 @@ where: {
96
96
  store_id: {not: 2}, # not
97
97
  aisle_id: {not: [25, 30]}, # not in
98
98
  user_ids: {all: [1, 3]}, # all elements in array
99
+ category: /frozen .+/, # regexp
99
100
  or: [
100
101
  [{in_stock: true}, {backordered: true}]
101
102
  ]
@@ -267,18 +268,28 @@ end
267
268
 
268
269
  ### Misspellings
269
270
 
270
- By default, Searchkick handles misspelled queries by returning results with an [edit distance](http://en.wikipedia.org/wiki/Levenshtein_distance) of one. To turn off this feature, use:
271
+ By default, Searchkick handles misspelled queries by returning results with an [edit distance](http://en.wikipedia.org/wiki/Levenshtein_distance) of one.
272
+
273
+ You can change this with:
274
+
275
+ ```ruby
276
+ Product.search "zucini", misspellings: {edit_distance: 2} # zucchini
277
+ ```
278
+
279
+ Or turn off misspellings with:
271
280
 
272
281
  ```ruby
273
282
  Product.search "zuchini", misspellings: false # no zucchini
274
283
  ```
275
284
 
276
- You can also change the edit distance with:
285
+ Swapping two letters counts as two edits. To count the [transposition of two adjacent characters as a single edit](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance), use:
277
286
 
278
287
  ```ruby
279
- Product.search "zucini", misspellings: {edit_distance: 2} # zucchini
288
+ Product.search "mikl", misspellings: {transpositions: true} # milk
280
289
  ```
281
290
 
291
+ This is planned to be the default in Searchkick 1.0.
292
+
282
293
  ### Indexing
283
294
 
284
295
  Control what data is indexed with the `search_data` method. Call `Product.reindex` after changing this method.
@@ -538,7 +549,7 @@ Product.search "wingtips", facets: {size: {where: {color: "brandy"}}}
538
549
  Limit
539
550
 
540
551
  ```ruby
541
- Product.search "2% Milk", facets: {store_id: {limit: 10}}
552
+ Product.search "apples", facets: {store_id: {limit: 10}}
542
553
  ```
543
554
 
544
555
  Ranges
@@ -849,7 +860,7 @@ To modify the query generated by Searchkick, use:
849
860
 
850
861
  ```ruby
851
862
  products =
852
- Product.search "2% Milk" do |body|
863
+ Product.search "apples" do |body|
853
864
  body[:query] = {match_all: {}}
854
865
  end
855
866
  ```
@@ -898,6 +909,14 @@ class Product < ActiveRecord::Base
898
909
  end
899
910
  ```
900
911
 
912
+ Use a dynamic index name
913
+
914
+ ```ruby
915
+ class Product < ActiveRecord::Base
916
+ searchkick index_name: -> { "#{name.tableize}-#{I18n.locale}" }
917
+ end
918
+ ```
919
+
901
920
  Prefix the index name
902
921
 
903
922
  ```ruby
@@ -990,6 +1009,18 @@ Reindex all models - Rails only
990
1009
  rake searchkick:reindex:all
991
1010
  ```
992
1011
 
1012
+ Turn on misspellings after a certain number of characters
1013
+
1014
+ ```ruby
1015
+ Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
1016
+ ```
1017
+
1018
+ **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off
1019
+
1020
+ ```ruby
1021
+ Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
1022
+ ```
1023
+
993
1024
  ## Large Data Sets
994
1025
 
995
1026
  For large data sets, check out [Keeping Elasticsearch in Sync](https://www.found.no/foundation/keeping-elasticsearch-in-sync/). Searchkick will make this easy in the future.
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in searchkick.gemspec
4
4
  gemspec path: "../"
5
5
 
6
- gem "nobrainer", "0.22.0"
6
+ gem "nobrainer", "0.27.0"
data/lib/searchkick.rb CHANGED
@@ -28,6 +28,7 @@ module Searchkick
28
28
  attr_accessor :wordnet_path
29
29
  attr_accessor :timeout
30
30
  attr_accessor :models
31
+ attr_writer :env
31
32
  end
32
33
  self.search_method_name = :search
33
34
  self.wordnet_path = "/var/lib/wn_s.pl"
@@ -130,7 +130,8 @@ module Searchkick
130
130
 
131
131
  # reindex
132
132
 
133
- def create_index
133
+ def create_index(options = {})
134
+ index_options = options[:index_options] || self.index_options
134
135
  index = Searchkick::Index.new("#{name}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}", @options)
135
136
  index.create(index_options)
136
137
  index
@@ -153,7 +154,7 @@ module Searchkick
153
154
 
154
155
  clean_indices
155
156
 
156
- index = create_index
157
+ index = create_index(index_options: scope.searchkick_index_options)
157
158
 
158
159
  # check if alias exists
159
160
  if alias_exists?
@@ -217,6 +218,9 @@ module Searchkick
217
218
  },
218
219
  default_index: {
219
220
  type: "custom",
221
+ # character filters -> tokenizer -> token filters
222
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
223
+ char_filter: ["ampersand"],
220
224
  tokenizer: "standard",
221
225
  # synonym should come last, after stemming and shingle
222
226
  # shingle must come before searchkick_stemmer
@@ -224,11 +228,13 @@ module Searchkick
224
228
  },
225
229
  searchkick_search: {
226
230
  type: "custom",
231
+ char_filter: ["ampersand"],
227
232
  tokenizer: "standard",
228
233
  filter: ["standard", "lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
229
234
  },
230
235
  searchkick_search2: {
231
236
  type: "custom",
237
+ char_filter: ["ampersand"],
232
238
  tokenizer: "standard",
233
239
  filter: ["standard", "lowercase", "asciifolding", "searchkick_stemmer"]
234
240
  },
@@ -311,10 +317,20 @@ module Searchkick
311
317
  max_gram: 50
312
318
  },
313
319
  searchkick_stemmer: {
314
- type: "snowball",
320
+ # use stemmer if language is lowercase, snowball otherwise
321
+ # TODO deprecate language option in favor of stemmer
322
+ type: options[:language] == options[:language].to_s.downcase ? "stemmer" : "snowball",
315
323
  language: options[:language] || "English"
316
324
  }
317
325
  },
326
+ char_filter: {
327
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
328
+ # &_to_and
329
+ ampersand: {
330
+ type: "mapping",
331
+ mappings: ["&=> and "]
332
+ }
333
+ },
318
334
  tokenizer: {
319
335
  searchkick_autocomplete_ngram: {
320
336
  type: "edgeNGram",
@@ -15,9 +15,6 @@ module Searchkick
15
15
  @term = term
16
16
  @options = options
17
17
 
18
- below12 = Gem::Version.new(Searchkick.server_version) < Gem::Version.new("1.2.0")
19
- below14 = Gem::Version.new(Searchkick.server_version) < Gem::Version.new("1.4.0")
20
-
21
18
  boost_fields = {}
22
19
  fields =
23
20
  if options[:fields]
@@ -99,19 +96,21 @@ module Searchkick
99
96
  }
100
97
 
101
98
  if field == "_all" || field.end_with?(".analyzed")
102
- shared_options[:cutoff_frequency] = 0.001 unless operator == "and"
103
- qs.concat [
104
- shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"),
105
- shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2")
106
- ]
107
99
  misspellings = options.key?(:misspellings) ? options[:misspellings] : options[:mispellings] # why not?
108
100
  if misspellings != false
109
101
  edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
102
+ transpositions = (misspellings.is_a?(Hash) && misspellings[:transpositions] == true) ? {fuzzy_transpositions: true} : {}
103
+ prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
110
104
  qs.concat [
111
- shared_options.merge(fuzziness: edit_distance, max_expansions: 3, analyzer: "searchkick_search"),
112
- shared_options.merge(fuzziness: edit_distance, max_expansions: 3, analyzer: "searchkick_search2")
105
+ shared_options.merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: 3, analyzer: "searchkick_search").merge(transpositions),
106
+ shared_options.merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: 3, analyzer: "searchkick_search2").merge(transpositions)
113
107
  ]
114
108
  end
109
+ shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false
110
+ qs.concat [
111
+ shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"),
112
+ shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2")
113
+ ]
115
114
  elsif field.end_with?(".exact")
116
115
  f = field.split(".")[0..-2].join(".")
117
116
  queries << {match: {f => shared_options.merge(analyzer: "keyword")}}
@@ -133,7 +132,7 @@ module Searchkick
133
132
  if conversions_field && options[:conversions] != false
134
133
  # wrap payload in a bool query
135
134
  script_score =
136
- if below12
135
+ if below12?
137
136
  {script_score: {script: "doc['count'].value"}}
138
137
  else
139
138
  {field_value_factor: {field: "count"}}
@@ -164,31 +163,21 @@ module Searchkick
164
163
  end
165
164
 
166
165
  custom_filters = []
166
+ multiply_filters = []
167
167
 
168
168
  boost_by = options[:boost_by] || {}
169
+
169
170
  if boost_by.is_a?(Array)
170
171
  boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }]
172
+ elsif boost_by.is_a?(Hash)
173
+ multiply_by, boost_by = boost_by.partition { |k,v| v[:boost_mode] == "multiply" }.map{ |i| Hash[i] }
171
174
  end
172
175
  if options[:boost]
173
176
  boost_by[options[:boost]] = {factor: 1}
174
177
  end
175
178
 
176
- boost_by.each do |field, value|
177
- script_score =
178
- if below12
179
- {script_score: {script: "#{value[:factor].to_f} * log(doc['#{field}'].value + 2.718281828)"}}
180
- else
181
- {field_value_factor: {field: field, factor: value[:factor].to_f, modifier: "ln2p"}}
182
- end
183
-
184
- custom_filters << {
185
- filter: {
186
- exists: {
187
- field: field
188
- }
189
- }
190
- }.merge(script_score)
191
- end
179
+ custom_filters.concat boost_filters(boost_by, log: true)
180
+ multiply_filters.concat boost_filters(multiply_by || {})
192
181
 
193
182
  boost_where = options[:boost_where] || {}
194
183
  if options[:user_id] && personalize_field
@@ -237,6 +226,16 @@ module Searchkick
237
226
  }
238
227
  end
239
228
 
229
+ if multiply_filters.any?
230
+ payload = {
231
+ function_score: {
232
+ functions: multiply_filters,
233
+ query: payload,
234
+ score_mode: "multiply"
235
+ }
236
+ }
237
+ end
238
+
240
239
  payload = {
241
240
  query: payload,
242
241
  size: per_page,
@@ -294,7 +293,7 @@ module Searchkick
294
293
  payload[:facets][field] = {
295
294
  terms_stats: {
296
295
  key_field: field,
297
- value_script: below14 ? "doc.score" : "_score",
296
+ value_script: below14? ? "doc.score" : "_score",
298
297
  size: size
299
298
  }
300
299
  }
@@ -504,6 +503,8 @@ module Searchkick
504
503
  }
505
504
  }
506
505
  }
506
+ when :regexp # support for regexp queries without using a regexp ruby object
507
+ filters << {regexp: {field => {value: op_value}}}
507
508
  when :not # not equal
508
509
  filters << {not: term_filters(field, op_value)}
509
510
  when :all
@@ -558,10 +559,45 @@ module Searchkick
558
559
 
559
560
  def custom_filter(field, value, factor)
560
561
  {
561
- filter: term_filters(field, value),
562
+ filter: {
563
+ and: where_filters({field => value})
564
+ },
562
565
  boost_factor: factor
563
566
  }
564
567
  end
565
568
 
569
+ def boost_filters(boost_by, options = {})
570
+ boost_by.map do |field, value|
571
+ log = value.key?(:log) ? value[:log] : options[:log]
572
+ value[:factor] ||= 1
573
+ script_score =
574
+ if below12?
575
+ script = log ? "log(doc['#{field}'].value + 2.718281828)" : "doc['#{field}'].value"
576
+ {script_score: {script: "#{value[:factor].to_f} * #{script}"}}
577
+ else
578
+ {field_value_factor: {field: field, factor: value[:factor].to_f, modifier: log ? "ln2p" : nil}}
579
+ end
580
+
581
+ {
582
+ filter: {
583
+ exists: {
584
+ field: field
585
+ }
586
+ }
587
+ }.merge(script_score)
588
+ end
589
+ end
590
+
591
+ def below12?
592
+ below_version?("1.2.0")
593
+ end
594
+
595
+ def below14?
596
+ below_version?("1.4.0")
597
+ end
598
+
599
+ def below_version?(version)
600
+ Gem::Version.new(Searchkick.server_version) < Gem::Version.new(version)
601
+ end
566
602
  end
567
603
  end
@@ -15,6 +15,11 @@ module Searchkick
15
15
  @options = options
16
16
  end
17
17
 
18
+ # experimental: may not make next release
19
+ def records
20
+ @records ||= results_query(klass, hits)
21
+ end
22
+
18
23
  def results
19
24
  @results ||= begin
20
25
  if options[:load]
@@ -22,20 +27,12 @@ module Searchkick
22
27
  results = {}
23
28
 
24
29
  hits.group_by { |hit, i| hit["_type"] }.each do |type, grouped_hits|
25
- records = type.camelize.constantize
26
- if options[:includes]
27
- if defined?(NoBrainer::Document) && records < NoBrainer::Document
28
- records = records.preload(options[:includes])
29
- else
30
- records = records.includes(options[:includes])
31
- end
32
- end
33
- results[type] = results_query(records, grouped_hits)
30
+ results[type] = results_query(type.camelize.constantize, grouped_hits).to_a.index_by { |r| r.id.to_s }
34
31
  end
35
32
 
36
33
  # sort
37
34
  hits.map do |hit|
38
- results[hit["_type"]].find { |r| r.id.to_s == hit["_id"].to_s }
35
+ results[hit["_type"]][hit["_id"].to_s]
39
36
  end.compact
40
37
  else
41
38
  hits.map do |hit|
@@ -131,25 +128,40 @@ module Searchkick
131
128
  next_page.nil?
132
129
  end
133
130
 
131
+ def out_of_range?
132
+ current_page > total_pages
133
+ end
134
+
134
135
  def hits
135
136
  @response["hits"]["hits"]
136
137
  end
137
138
 
138
139
  private
139
140
 
140
- def results_query(records, grouped_hits)
141
+ def results_query(records, hits)
142
+ ids = hits.map { |hit| hit["_id"] }
143
+
144
+ if options[:includes]
145
+ records =
146
+ if defined?(NoBrainer::Document) && records < NoBrainer::Document
147
+ records.preload(options[:includes])
148
+ else
149
+ records.includes(options[:includes])
150
+ end
151
+ end
152
+
141
153
  if records.respond_to?(:primary_key) && records.primary_key
142
154
  # ActiveRecord
143
- records.where(records.primary_key => grouped_hits.map { |hit| hit["_id"] }).to_a
155
+ records.where(records.primary_key => ids)
144
156
  elsif records.respond_to?(:all) && records.all.respond_to?(:for_ids)
145
157
  # Mongoid 2
146
- records.all.for_ids(grouped_hits.map { |hit| hit["_id"] }).to_a
158
+ records.all.for_ids(ids)
147
159
  elsif records.respond_to?(:queryable)
148
160
  # Mongoid 3+
149
- records.queryable.for_ids(grouped_hits.map { |hit| hit["_id"] }).to_a
161
+ records.queryable.for_ids(ids)
150
162
  elsif records.respond_to?(:unscoped) && records.all.respond_to?(:preload)
151
163
  # Nobrainer
152
- records.unscoped.where(:id.in => grouped_hits.map { |hit| hit["_id"] }).to_a
164
+ records.unscoped.where(:id.in => ids)
153
165
  else
154
166
  raise "Not sure how to load records"
155
167
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "0.9.0"
2
+ VERSION = "0.9.1"
3
3
  end
data/test/boost_test.rb CHANGED
@@ -101,6 +101,16 @@ class TestBoost < Minitest::Test
101
101
  assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"], boost_by: {orders_count: {factor: 10}}
102
102
  end
103
103
 
104
+ def test_boost_by_boost_mode_multiply
105
+ store [
106
+ {name: "Tomato A", found_rate: 0.9},
107
+ {name: "Tomato B"},
108
+ {name: "Tomato C", found_rate: 0.5}
109
+ ]
110
+
111
+ assert_order "tomato", ["Tomato B", "Tomato A", "Tomato C"], boost_by: {found_rate: {boost_mode: "multiply"}}
112
+ end
113
+
104
114
  def test_boost_where
105
115
  store [
106
116
  {name: "Tomato A"},
@@ -108,6 +118,7 @@ class TestBoost < Minitest::Test
108
118
  {name: "Tomato C", user_ids: [3]}
109
119
  ]
110
120
  assert_first "tomato", "Tomato B", boost_where: {user_ids: 2}
121
+ assert_first "tomato", "Tomato B", boost_where: {user_ids: 1..2}
111
122
  assert_first "tomato", "Tomato B", boost_where: {user_ids: [1, 4]}
112
123
  assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: 2, factor: 10}}
113
124
  assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: [1, 4], factor: 10}}
data/test/facets_test.rb CHANGED
@@ -76,6 +76,7 @@ class TestFacets < Minitest::Test
76
76
  end
77
77
 
78
78
  def test_stats_facets
79
+ skip if Gem::Version.new(Searchkick.server_version) >= Gem::Version.new("1.4.0")
79
80
  options = {where: {store_id: 2}, facets: {store_id: {stats: true}}}
80
81
  facets = Product.search("Product", options).facets["store_id"]["terms"]
81
82
  expected_facets_keys = %w[term count total_count min max total mean]
data/test/match_test.rb CHANGED
@@ -112,6 +112,27 @@ class TestMatch < Minitest::Test
112
112
  assert_search "zip lock", ["Ziploc"]
113
113
  end
114
114
 
115
+ def test_misspelling_zucchini_transposition
116
+ store_names ["zucchini"]
117
+ assert_search "zuccihni", [] # doesn't work without transpositions:true option
118
+ assert_search "zuccihni", ["zucchini"], misspellings: {transpositions: true}
119
+ end
120
+
121
+ def test_misspelling_lasagna
122
+ store_names ["lasagna"]
123
+ assert_search "lasanga", ["lasagna"], misspellings: {transpositions: true}
124
+ assert_search "lasgana", ["lasagna"], misspellings: {transpositions: true}
125
+ assert_search "lasaang", [], misspellings: {transpositions: true} # triple transposition, shouldn't work
126
+ assert_search "lsagana", [], misspellings: {transpositions: true} # triple transposition, shouldn't work
127
+ end
128
+
129
+ def test_misspelling_lasagna_pasta
130
+ store_names ["lasagna pasta"]
131
+ assert_search "lasanga", ["lasagna pasta"], misspellings: {transpositions: true}
132
+ assert_search "lasanga pasta", ["lasagna pasta"], misspellings: {transpositions: true}
133
+ assert_search "lasanga pasat", ["lasagna pasta"], misspellings: {transpositions: true} # both words misspelled with a transposition should still work
134
+ end
135
+
115
136
  # spaces
116
137
 
117
138
  def test_spaces_in_field
@@ -153,6 +174,21 @@ class TestMatch < Minitest::Test
153
174
  assert_search "to be", ["to be or not to be"]
154
175
  end
155
176
 
177
+ def test_apostrophe
178
+ store_names ["Ben and Jerry's"]
179
+ assert_search "ben and jerrys", ["Ben and Jerry's"]
180
+ end
181
+
182
+ def test_ampersand_index
183
+ store_names ["Ben & Jerry's"]
184
+ assert_search "ben and jerrys", ["Ben & Jerry's"]
185
+ end
186
+
187
+ def test_ampersand_search
188
+ store_names ["Ben and Jerry's"]
189
+ assert_search "ben & jerrys", ["Ben and Jerry's"]
190
+ end
191
+
156
192
  def test_unsearchable
157
193
  store [
158
194
  {name: "Unsearchable", description: "Almond"}
@@ -0,0 +1,10 @@
1
+ require_relative "test_helper"
2
+
3
+ class RecordsTest < Minitest::Test
4
+
5
+ def test_records
6
+ store_names ["Milk", "Apple"]
7
+ assert_equal Product.search("milk").records.where(name: "Milk").count, 1
8
+ end
9
+
10
+ end
data/test/sql_test.rb CHANGED
@@ -40,6 +40,7 @@ class TestSql < Minitest::Test
40
40
  assert !products.first_page?
41
41
  assert !products.last_page?
42
42
  assert !products.empty?
43
+ assert !products.out_of_range?
43
44
  assert products.any?
44
45
  end
45
46
 
@@ -98,6 +99,11 @@ class TestSql < Minitest::Test
98
99
  assert_search "*", ["Product A"], where: {name: /Pro.+/}
99
100
  end
100
101
 
102
+ def test_alternate_regexp
103
+ store_names ["Product A", "Item B"]
104
+ assert_search "*", ["Product A"], where: {name: {regexp: "Pro.+"}}
105
+ end
106
+
101
107
  def test_where_string
102
108
  store [
103
109
  {name: "Product A", color: "RED"}
@@ -238,6 +244,40 @@ class TestSql < Minitest::Test
238
244
  assert_search "aaaa", ["aabb"], misspellings: {distance: 2}
239
245
  end
240
246
 
247
+ def test_misspellings_prefix_length
248
+ store_names ["ap", "api", "apt", "any", "nap", "ah", "ahi"]
249
+ assert_search "ap", ["ap"], misspellings: {prefix_length: 2}
250
+ assert_search "api", ["ap", "api", "apt"], misspellings: {prefix_length: 2}
251
+ end
252
+
253
+ def test_misspellings_prefix_length_operator
254
+ store_names ["ap", "api", "apt", "any", "nap", "ah", "aha"]
255
+ assert_search "ap ah", ["ap", "ah"], operator: "or", misspellings: {prefix_length: 2}
256
+ assert_search "api ahi", ["ap", "api", "apt", "ah", "aha"], operator: "or", misspellings: {prefix_length: 2}
257
+ end
258
+
259
+ def test_misspellings_fields_operator
260
+ store [
261
+ {name: "red", color: "red"},
262
+ {name: "blue", color: "blue"},
263
+ {name: "cyan", color: "blue green"},
264
+ {name: "magenta", color: "red blue"},
265
+ {name: "green", color: "green"}
266
+ ]
267
+ assert_search "red blue", ["red", "blue", "cyan", "magenta"], operator: "or", fields: ["color"], misspellings: false
268
+ end
269
+
270
+ def test_fields_operator
271
+ store [
272
+ {name: "red", color: "red"},
273
+ {name: "blue", color: "blue"},
274
+ {name: "cyan", color: "blue green"},
275
+ {name: "magenta", color: "red blue"},
276
+ {name: "green", color: "green"}
277
+ ]
278
+ assert_search "red blue", ["red", "blue", "cyan", "magenta"], operator: "or", fields: ["color"]
279
+ end
280
+
241
281
  def test_fields
242
282
  store [
243
283
  {name: "red", color: "light blue"},
data/test/test_helper.rb CHANGED
@@ -51,6 +51,7 @@ if defined?(Mongoid)
51
51
  field :in_stock, type: Boolean
52
52
  field :backordered, type: Boolean
53
53
  field :orders_count, type: Integer
54
+ field :found_rate, type: BigDecimal
54
55
  field :price, type: Integer
55
56
  field :color
56
57
  field :latitude, type: BigDecimal
@@ -85,27 +86,32 @@ elsif defined?(NoBrainer)
85
86
  include NoBrainer::Document
86
87
  include NoBrainer::Document::Timestamps
87
88
 
89
+ field :id, type: Object
88
90
  field :name, type: String
89
- field :store_id, type: Integer
90
91
  field :in_stock, type: Boolean
91
92
  field :backordered, type: Boolean
92
93
  field :orders_count, type: Integer
94
+ field :found_rate
93
95
  field :price, type: Integer
94
96
  field :color, type: String
95
97
  field :latitude
96
98
  field :longitude
97
99
  field :description, type: String
100
+
101
+ belongs_to :store, validates: false
98
102
  end
99
103
 
100
104
  class Store
101
105
  include NoBrainer::Document
102
106
 
107
+ field :id, type: Object
103
108
  field :name, type: String
104
109
  end
105
110
 
106
111
  class Animal
107
112
  include NoBrainer::Document
108
113
 
114
+ field :id, type: Object
109
115
  field :name, type: String
110
116
  end
111
117
 
@@ -135,6 +141,7 @@ else
135
141
  t.boolean :in_stock
136
142
  t.boolean :backordered
137
143
  t.integer :orders_count
144
+ t.decimal :found_rate
138
145
  t.integer :price
139
146
  t.string :color
140
147
  t.decimal :latitude, precision: 10, scale: 7
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.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-07 00:00:00.000000000 Z
11
+ date: 2015-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -137,6 +137,7 @@ files:
137
137
  - test/match_test.rb
138
138
  - test/model_test.rb
139
139
  - test/query_test.rb
140
+ - test/records_test.rb
140
141
  - test/reindex_job_test.rb
141
142
  - test/reindex_v2_job_test.rb
142
143
  - test/routing_test.rb
@@ -182,6 +183,7 @@ test_files:
182
183
  - test/match_test.rb
183
184
  - test/model_test.rb
184
185
  - test/query_test.rb
186
+ - test/records_test.rb
185
187
  - test/reindex_job_test.rb
186
188
  - test/reindex_v2_job_test.rb
187
189
  - test/routing_test.rb