searchkick 1.5.1 → 2.0.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.
@@ -6,19 +6,21 @@ module Searchkick
6
6
  attr_accessor :body
7
7
 
8
8
  def_delegators :execute, :map, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary,
9
- :records, :results, :suggestions, :each_with_hit, :with_details, :facets, :aggregations, :aggs,
9
+ :records, :results, :suggestions, :each_with_hit, :with_details, :aggregations, :aggs,
10
10
  :took, :error, :model_name, :entry_name, :total_count, :total_entries,
11
11
  :current_page, :per_page, :limit_value, :padding, :total_pages, :num_pages,
12
12
  :offset_value, :offset, :previous_page, :prev_page, :next_page, :first_page?, :last_page?,
13
13
  :out_of_range?, :hits, :response, :to_a, :first
14
14
 
15
- def initialize(klass, term, options = {})
16
- if term.is_a?(Hash)
17
- options = term
18
- term = "*"
19
- else
20
- term = term.to_s
21
- end
15
+ def initialize(klass, term = "*", **options)
16
+ unknown_keywords = options.keys - [:aggs, :body, :body_options, :boost,
17
+ :boost_by, :boost_by_distance, :boost_where, :conversions, :debug, :emoji, :execute, :explain,
18
+ :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
19
+ :match, :misspellings, :offset, :operator, :order, :padding, :page, :per_page, :profile,
20
+ :request_params, :routing, :select, :similar, :smart_aggs, :suggest, :track, :type, :where]
21
+ raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
22
+
23
+ term = term.to_s
22
24
 
23
25
  if options[:emoji]
24
26
  term = EmojiParser.parse_unicode(term) { |e| " #{e.name} " }.strip
@@ -97,21 +99,12 @@ module Searchkick
97
99
  end
98
100
 
99
101
  def handle_response(response)
100
- # apply facet limit in client due to
101
- # https://github.com/elasticsearch/elasticsearch/issues/1305
102
- @facet_limits.each do |field, limit|
103
- field = field.to_s
104
- facet = response["facets"][field]
105
- response["facets"][field]["terms"] = facet["terms"].first(limit)
106
- response["facets"][field]["other"] = facet["total"] - facet["terms"].sum { |term| term["count"] }
107
- end
108
-
109
102
  opts = {
110
103
  page: @page,
111
104
  per_page: @per_page,
112
105
  padding: @padding,
113
106
  load: @load,
114
- includes: options[:include] || options[:includes],
107
+ includes: options[:includes],
115
108
  json: !@json.nil?,
116
109
  match_suffix: @match_suffix,
117
110
  highlighted_fields: @highlighted_fields || []
@@ -171,13 +164,19 @@ module Searchkick
171
164
  elsif status_code == 500 && (
172
165
  e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") ||
173
166
  e.message.include?("No query registered for [multi_match]") ||
174
- e.message.include?("[match] query does not support [cutoff_frequency]]") ||
175
- e.message.include?("No query registered for [function_score]]")
167
+ e.message.include?("[match] query does not support [cutoff_frequency]") ||
168
+ e.message.include?("No query registered for [function_score]")
176
169
  )
177
170
 
178
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 1.0 or greater"
171
+ raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 2 or greater"
179
172
  elsif status_code == 400
180
- if e.message.include?("[multi_match] analyzer [searchkick_search] not found")
173
+ if (
174
+ e.message.include?("bool query does not support [filter]") ||
175
+ e.message.include?("[bool] filter does not support [filter]")
176
+ )
177
+
178
+ raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 2 or greater"
179
+ elsif e.message.include?("[multi_match] analyzer [searchkick_search] not found")
181
180
  raise InvalidQueryError, "Bad mapping - run #{reindex_command}"
182
181
  else
183
182
  raise InvalidQueryError, e.message
@@ -198,7 +197,7 @@ module Searchkick
198
197
  def prepare
199
198
  boost_fields, fields = set_fields
200
199
 
201
- operator = options[:operator] || (options[:partial] ? "or" : "and")
200
+ operator = options[:operator] || "and"
202
201
 
203
202
  # pagination
204
203
  page = [options[:page].to_i, 1].max
@@ -210,17 +209,14 @@ module Searchkick
210
209
  load = options[:load].nil? ? true : options[:load]
211
210
 
212
211
  conversions_fields = Array(options[:conversions] || searchkick_options[:conversions]).map(&:to_s)
213
- personalize_field = searchkick_options[:personalize]
214
212
 
215
213
  all = term == "*"
216
214
 
217
- @json = options[:json] || options[:body]
215
+ @json = options[:body]
218
216
  if @json
219
217
  payload = @json
220
218
  else
221
- if options[:query]
222
- payload = options[:query]
223
- elsif options[:similar]
219
+ if options[:similar]
224
220
  payload = {
225
221
  more_like_this: {
226
222
  fields: fields,
@@ -235,117 +231,98 @@ module Searchkick
235
231
  match_all: {}
236
232
  }
237
233
  else
238
- if options[:autocomplete]
239
- payload = {
240
- multi_match: {
241
- fields: fields,
242
- query: term,
243
- analyzer: "searchkick_autocomplete_search"
244
- }
245
- }
246
- else
247
- queries = []
234
+ queries = []
248
235
 
249
- misspellings =
250
- if options.key?(:misspellings)
251
- options[:misspellings]
252
- elsif options.key?(:mispellings)
253
- options[:mispellings] # why not?
254
- else
255
- true
256
- end
257
-
258
- if misspellings.is_a?(Hash) && misspellings[:below] && !@misspellings_below
259
- @misspellings_below = misspellings[:below].to_i
260
- misspellings = false
236
+ misspellings =
237
+ if options.key?(:misspellings)
238
+ options[:misspellings]
239
+ else
240
+ true
261
241
  end
262
242
 
263
- if misspellings != false
264
- edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
265
- transpositions =
266
- if misspellings.is_a?(Hash) && misspellings.key?(:transpositions)
267
- {fuzzy_transpositions: misspellings[:transpositions]}
268
- elsif below14?
269
- {}
270
- else
271
- {fuzzy_transpositions: true}
272
- end
273
- prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
274
- default_max_expansions = @misspellings_below ? 20 : 3
275
- max_expansions = (misspellings.is_a?(Hash) && misspellings[:max_expansions]) || default_max_expansions
276
- end
243
+ if misspellings.is_a?(Hash) && misspellings[:below] && !@misspellings_below
244
+ @misspellings_below = misspellings[:below].to_i
245
+ misspellings = false
246
+ end
277
247
 
278
- fields.each do |field|
279
- qs = []
248
+ if misspellings != false
249
+ edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
250
+ transpositions =
251
+ if misspellings.is_a?(Hash) && misspellings.key?(:transpositions)
252
+ {fuzzy_transpositions: misspellings[:transpositions]}
253
+ else
254
+ {fuzzy_transpositions: true}
255
+ end
256
+ prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
257
+ default_max_expansions = @misspellings_below ? 20 : 3
258
+ max_expansions = (misspellings.is_a?(Hash) && misspellings[:max_expansions]) || default_max_expansions
259
+ end
280
260
 
281
- factor = boost_fields[field] || 1
282
- shared_options = {
283
- query: term,
284
- boost: 10 * factor
285
- }
261
+ fields.each do |field|
262
+ qs = []
286
263
 
287
- match_type =
288
- if field.end_with?(".phrase")
289
- field = field.sub(/\.phrase\z/, ".analyzed")
290
- :match_phrase
291
- else
292
- :match
293
- end
264
+ factor = boost_fields[field] || 1
265
+ shared_options = {
266
+ query: term,
267
+ boost: 10 * factor
268
+ }
294
269
 
295
- shared_options[:operator] = operator if match_type == :match || below50?
296
-
297
- if field == "_all" || field.end_with?(".analyzed")
298
- shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false
299
- qs.concat [
300
- shared_options.merge(analyzer: "searchkick_search"),
301
- shared_options.merge(analyzer: "searchkick_search2")
302
- ]
303
- elsif field.end_with?(".exact")
304
- f = field.split(".")[0..-2].join(".")
305
- queries << {match: {f => shared_options.merge(analyzer: "keyword")}}
270
+ match_type =
271
+ if field.end_with?(".phrase")
272
+ field = field.sub(/\.phrase\z/, ".analyzed")
273
+ :match_phrase
306
274
  else
307
- analyzer = field =~ /\.word_(start|middle|end)\z/ ? "searchkick_word_search" : "searchkick_autocomplete_search"
308
- qs << shared_options.merge(analyzer: analyzer)
275
+ :match
309
276
  end
310
277
 
311
- if misspellings != false && (match_type == :match || below50?)
312
- qs.concat qs.map { |q| q.except(:cutoff_frequency).merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: max_expansions, boost: factor).merge(transpositions) }
313
- end
278
+ shared_options[:operator] = operator if match_type == :match
279
+
280
+ if field == "_all" || field.end_with?(".analyzed")
281
+ shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false
282
+ qs.concat [
283
+ shared_options.merge(analyzer: "searchkick_search"),
284
+ shared_options.merge(analyzer: "searchkick_search2")
285
+ ]
286
+ elsif field.end_with?(".exact")
287
+ f = field.split(".")[0..-2].join(".")
288
+ queries << {match: {f => shared_options.merge(analyzer: "keyword")}}
289
+ else
290
+ analyzer = field =~ /\.word_(start|middle|end)\z/ ? "searchkick_word_search" : "searchkick_autocomplete_search"
291
+ qs << shared_options.merge(analyzer: analyzer)
292
+ end
314
293
 
315
- # boost exact matches more
316
- if field =~ /\.word_(start|middle|end)\z/ && searchkick_options[:word] != false
317
- queries << {
318
- bool: {
319
- must: {
320
- bool: {
321
- should: qs.map { |q| {match_type => {field => q}} }
322
- }
323
- },
324
- should: {match_type => {field.sub(/\.word_(start|middle|end)\z/, ".analyzed") => qs.first}}
325
- }
326
- }
327
- else
328
- queries.concat(qs.map { |q| {match_type => {field => q}} })
329
- end
294
+ if misspellings != false && match_type == :match
295
+ qs.concat qs.map { |q| q.except(:cutoff_frequency).merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: max_expansions, boost: factor).merge(transpositions) }
330
296
  end
331
297
 
332
- payload = {
333
- dis_max: {
334
- queries: queries
298
+ # boost exact matches more
299
+ if field =~ /\.word_(start|middle|end)\z/ && searchkick_options[:word] != false
300
+ queries << {
301
+ bool: {
302
+ must: {
303
+ bool: {
304
+ should: qs.map { |q| {match_type => {field => q}} }
305
+ }
306
+ },
307
+ should: {match_type => {field.sub(/\.word_(start|middle|end)\z/, ".analyzed") => qs.first}}
308
+ }
335
309
  }
336
- }
310
+ else
311
+ queries.concat(qs.map { |q| {match_type => {field => q}} })
312
+ end
337
313
  end
338
314
 
315
+ payload = {
316
+ dis_max: {
317
+ queries: queries
318
+ }
319
+ }
320
+
339
321
  if conversions_fields.present? && options[:conversions] != false
340
322
  shoulds = []
341
323
  conversions_fields.each do |conversions_field|
342
324
  # wrap payload in a bool query
343
- script_score =
344
- if below12?
345
- {script_score: {script: "doc['count'].value"}}
346
- else
347
- {field_value_factor: {field: "#{conversions_field}.count"}}
348
- end
325
+ script_score = {field_value_factor: {field: "#{conversions_field}.count"}}
349
326
 
350
327
  shoulds << {
351
328
  nested: {
@@ -377,7 +354,7 @@ module Searchkick
377
354
  multiply_filters = []
378
355
 
379
356
  set_boost_by(multiply_filters, custom_filters)
380
- set_boost_where(custom_filters, personalize_field)
357
+ set_boost_where(custom_filters)
381
358
  set_boost_by_distance(custom_filters) if options[:boost_by_distance]
382
359
 
383
360
  if custom_filters.any?
@@ -418,9 +395,6 @@ module Searchkick
418
395
  filters = where_filters(options[:where])
419
396
  set_filters(payload, filters) if filters.any?
420
397
 
421
- # facets
422
- set_facets(payload) if options[:facets]
423
-
424
398
  # aggregations
425
399
  set_aggregations(payload) if options[:aggs]
426
400
 
@@ -437,25 +411,14 @@ module Searchkick
437
411
  # doc for :select - http://www.elasticsearch.org/guide/reference/api/search/fields/
438
412
  # doc for :select_v2 - https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
439
413
  if options[:select]
440
- payload[:fields] = options[:select] if options[:select] != true
441
- elsif options[:select_v2]
442
- if options[:select_v2] == []
414
+ if options[:select] == []
443
415
  # intuitively [] makes sense to return no fields, but ES by default returns all fields
444
- if below50?
445
- payload[:fields] = []
446
- else
447
- payload[:_source] = false
448
- end
416
+ payload[:_source] = false
449
417
  else
450
- payload[:_source] = options[:select_v2]
418
+ payload[:_source] = options[:select]
451
419
  end
452
420
  elsif load
453
- # don't need any fields since we're going to load them from the DB anyways
454
- if below50?
455
- payload[:fields] = []
456
- else
457
- payload[:_source] = false
458
- end
421
+ payload[:_source] = false
459
422
  end
460
423
 
461
424
  if options[:type] || (klass != searchkick_klass && searchkick_index)
@@ -470,7 +433,6 @@ module Searchkick
470
433
  payload = payload.deep_merge(options[:body_options]) if options[:body_options]
471
434
 
472
435
  @body = payload
473
- @facet_limits ||= {}
474
436
  @page = page
475
437
  @per_page = per_page
476
438
  @padding = padding
@@ -482,23 +444,15 @@ module Searchkick
482
444
  fields = options[:fields] || searchkick_options[:searchable]
483
445
  fields =
484
446
  if fields
485
- if options[:autocomplete]
486
- fields.map { |f| "#{f}.autocomplete" }
487
- else
488
- fields.map do |value|
489
- k, v = value.is_a?(Hash) ? value.to_a.first : [value, options[:match] || searchkick_options[:match] || :word]
490
- k2, boost = k.to_s.split("^", 2)
491
- field = "#{k2}.#{v == :word ? 'analyzed' : v}"
492
- boost_fields[field] = boost.to_f if boost
493
- field
494
- end
447
+ fields.map do |value|
448
+ k, v = value.is_a?(Hash) ? value.to_a.first : [value, options[:match] || searchkick_options[:match] || :word]
449
+ k2, boost = k.to_s.split("^", 2)
450
+ field = "#{k2}.#{v == :word ? 'analyzed' : v}"
451
+ boost_fields[field] = boost.to_f if boost
452
+ field
495
453
  end
496
454
  else
497
- if options[:autocomplete]
498
- (searchkick_options[:autocomplete] || []).map { |f| "#{f}.autocomplete" }
499
- else
500
- ["_all"]
501
- end
455
+ ["_all"]
502
456
  end
503
457
  [boost_fields, fields]
504
458
  end
@@ -531,14 +485,8 @@ module Searchkick
531
485
  multiply_filters.concat boost_filters(multiply_by || {})
532
486
  end
533
487
 
534
- def set_boost_where(custom_filters, personalize_field)
488
+ def set_boost_where(custom_filters)
535
489
  boost_where = options[:boost_where] || {}
536
- if options[:user_id] && personalize_field
537
- boost_where[personalize_field] = options[:user_id]
538
- end
539
- if options[:personalize]
540
- boost_where = boost_where.merge(options[:personalize])
541
- end
542
490
  boost_where.each do |field, value|
543
491
  if value.is_a?(Array) && value.first.is_a?(Hash)
544
492
  value.each do |value_factor|
@@ -676,91 +624,21 @@ module Searchkick
676
624
  end
677
625
  end
678
626
 
679
- def set_facets(payload)
680
- facets = options[:facets] || {}
681
- facets = Hash[facets.map { |f| [f, {}] }] if facets.is_a?(Array) # convert to more advanced syntax
682
- facet_limits = {}
683
- payload[:facets] = {}
684
-
685
- facets.each do |field, facet_options|
686
- # ask for extra facets due to
687
- # https://github.com/elasticsearch/elasticsearch/issues/1305
688
- size = facet_options[:limit] ? facet_options[:limit] + 150 : 1_000
689
-
690
- if facet_options[:ranges]
691
- payload[:facets][field] = {
692
- range: {
693
- field.to_sym => facet_options[:ranges]
694
- }
695
- }
696
- elsif facet_options[:stats]
697
- payload[:facets][field] = {
698
- terms_stats: {
699
- key_field: field,
700
- value_script: below14? ? "doc.score" : "_score",
701
- size: size
702
- }
703
- }
704
- else
705
- payload[:facets][field] = {
706
- terms: {
707
- field: facet_options[:field] || field,
708
- size: size
709
- }
710
- }
711
- end
712
-
713
- facet_limits[field] = facet_options[:limit] if facet_options[:limit]
714
-
715
- # offset is not possible
716
- # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html
717
-
718
- facet_options.deep_merge!(where: options.fetch(:where, {}).reject { |k| k == field }) if options[:smart_facets] == true
719
- facet_filters = where_filters(facet_options[:where])
720
- if facet_filters.any?
721
- payload[:facets][field][:facet_filter] = {
722
- and: {
723
- filters: facet_filters
724
- }
725
- }
726
- end
727
- end
728
-
729
- @facet_limits = facet_limits
730
- end
731
-
732
627
  def set_filters(payload, filters)
733
- if options[:facets] || options[:aggs]
734
- if below20?
735
- payload[:filter] = {
736
- and: filters
628
+ if options[:aggs]
629
+ payload[:post_filter] = {
630
+ bool: {
631
+ filter: filters
737
632
  }
738
- else
739
- payload[:post_filter] = {
740
- bool: {
741
- filter: filters
742
- }
743
- }
744
- end
633
+ }
745
634
  else
746
- # more efficient query if no facets
747
- if below20?
748
- payload[:query] = {
749
- filtered: {
750
- query: payload[:query],
751
- filter: {
752
- and: filters
753
- }
754
- }
635
+ # more efficient query if no aggs
636
+ payload[:query] = {
637
+ bool: {
638
+ must: payload[:query],
639
+ filter: filters
755
640
  }
756
- else
757
- payload[:query] = {
758
- bool: {
759
- must: payload[:query],
760
- filter: filters
761
- }
762
- }
763
- end
641
+ }
764
642
  end
765
643
  end
766
644
 
@@ -778,30 +656,14 @@ module Searchkick
778
656
 
779
657
  if field == :or
780
658
  value.each do |or_clause|
781
- if below50?
782
- filters << {or: or_clause.map { |or_statement| {and: where_filters(or_statement)} }}
783
- else
784
- filters << {bool: {should: or_clause.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
785
- end
659
+ filters << {bool: {should: or_clause.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
786
660
  end
787
661
  elsif field == :_or
788
- if below20?
789
- filters << {or: value.map { |or_statement| {and: where_filters(or_statement)} }}
790
- else
791
- filters << {bool: {should: value.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
792
- end
662
+ filters << {bool: {should: value.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
793
663
  elsif field == :_not
794
- if below20?
795
- filters << {not: {and: where_filters(value)}}
796
- else
797
- filters << {bool: {must_not: where_filters(value)}}
798
- end
664
+ filters << {bool: {must_not: where_filters(value)}}
799
665
  elsif field == :_and
800
- if below20?
801
- filters << {and: value.map { |or_statement| {and: where_filters(or_statement)} }}
802
- else
803
- filters << {bool: {must: value.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
804
- end
666
+ filters << {bool: {must: value.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
805
667
  else
806
668
  # expand ranges
807
669
  if value.is_a?(Range)
@@ -851,11 +713,7 @@ module Searchkick
851
713
  when :regexp # support for regexp queries without using a regexp ruby object
852
714
  filters << {regexp: {field => {value: op_value}}}
853
715
  when :not # not equal
854
- if below50?
855
- filters << {not: {filter: term_filters(field, op_value)}}
856
- else
857
- filters << {bool: {must_not: term_filters(field, op_value)}}
858
- end
716
+ filters << {bool: {must_not: term_filters(field, op_value)}}
859
717
  when :all
860
718
  op_value.each do |val|
861
719
  filters << term_filters(field, val)
@@ -895,20 +753,12 @@ module Searchkick
895
753
  def term_filters(field, value)
896
754
  if value.is_a?(Array) # in query
897
755
  if value.any?(&:nil?)
898
- if below50?
899
- {or: [term_filters(field, nil), term_filters(field, value.compact)]}
900
- else
901
- {bool: {should: [term_filters(field, nil), term_filters(field, value.compact)]}}
902
- end
756
+ {bool: {should: [term_filters(field, nil), term_filters(field, value.compact)]}}
903
757
  else
904
758
  {in: {field => value}}
905
759
  end
906
760
  elsif value.nil?
907
- if below50?
908
- {missing: {field: field, existence: true, null_value: true}}
909
- else
910
- {bool: {must_not: {exists: {field: field}}}}
911
- end
761
+ {bool: {must_not: {exists: {field: field}}}}
912
762
  elsif value.is_a?(Regexp)
913
763
  {regexp: {field => {value: value.source}}}
914
764
  else
@@ -936,13 +786,13 @@ module Searchkick
936
786
  boost_by.map do |field, value|
937
787
  log = value.key?(:log) ? value[:log] : options[:log]
938
788
  value[:factor] ||= 1
939
- script_score =
940
- if below12?
941
- script = log ? "log(doc['#{field}'].value + 2.718281828)" : "doc['#{field}'].value"
942
- {script_score: {script: "#{value[:factor].to_f} * #{script}"}}
943
- else
944
- {field_value_factor: {field: field, factor: value[:factor].to_f, modifier: log ? "ln2p" : nil}}
945
- end
789
+ script_score = {
790
+ field_value_factor: {
791
+ field: field,
792
+ factor: value[:factor].to_f,
793
+ modifier: log ? "ln2p" : nil
794
+ }
795
+ }
946
796
 
947
797
  {
948
798
  filter: {
@@ -961,7 +811,7 @@ module Searchkick
961
811
  if value.is_a?(Hash)
962
812
  [value[:lon], value[:lat]]
963
813
  elsif value.is_a?(Array) and !value[0].is_a?(Numeric)
964
- value.map {|a| coordinate_array(a) }
814
+ value.map { |a| coordinate_array(a) }
965
815
  else
966
816
  value
967
817
  end
@@ -975,18 +825,6 @@ module Searchkick
975
825
  end
976
826
  end
977
827
 
978
- def below12?
979
- Searchkick.server_below?("1.2.0")
980
- end
981
-
982
- def below14?
983
- Searchkick.server_below?("1.4.0")
984
- end
985
-
986
- def below20?
987
- Searchkick.server_below?("2.0.0")
988
- end
989
-
990
828
  def below50?
991
829
  Searchkick.server_below?("5.0.0-alpha1")
992
830
  end