searchkick 1.5.1 → 2.0.0

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