searchkick 3.1.2 → 4.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -132,7 +132,7 @@ module Searchkick
132
132
  def multi_search(searches)
133
133
  event = {
134
134
  name: "Multi Search",
135
- body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
135
+ body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
136
136
  }
137
137
  ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
138
138
  super
@@ -162,12 +162,17 @@ module Searchkick
162
162
 
163
163
  payload = event.payload
164
164
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
165
- type = payload[:query][:type]
165
+
166
166
  index = payload[:query][:index].is_a?(Array) ? payload[:query][:index].join(",") : payload[:query][:index]
167
+ type = payload[:query][:type]
168
+ request_params = payload[:query].except(:index, :type, :body)
169
+
170
+ params = []
171
+ request_params.each do |k, v|
172
+ params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
173
+ end
167
174
 
168
- # no easy way to tell which host the client will use
169
- host = Searchkick.client.transport.hosts.first
170
- debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -H 'Content-Type: application/json' -d '#{payload[:query][:body].to_json}'"
175
+ debug " #{color(name, YELLOW, true)} #{index}#{type ? "/#{type.join(',')}" : ''}/_search#{params.any? ? '?' + params.join('&') : nil} #{payload[:query][:body].to_json}"
171
176
  end
172
177
 
173
178
  def request(event)
@@ -187,9 +192,7 @@ module Searchkick
187
192
  payload = event.payload
188
193
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
189
194
 
190
- # no easy way to tell which host the client will use
191
- host = Searchkick.client.transport.hosts.first
192
- debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/_msearch?pretty -H 'Content-Type: application/json' -d '#{payload[:body]}'"
195
+ debug " #{color(name, YELLOW, true)} _msearch #{payload[:body]}"
193
196
  end
194
197
  end
195
198
 
@@ -3,10 +3,10 @@ module Searchkick
3
3
  def searchkick(**options)
4
4
  options = Searchkick.model_options.merge(options)
5
5
 
6
- unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :default_fields,
6
+ unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields,
7
7
  :filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language,
8
- :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :settings, :similarity,
9
- :special_characters, :stem, :stem_conversions, :suggest, :synonyms, :text_end,
8
+ :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
9
+ :special_characters, :stem, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
10
10
  :text_middle, :text_start, :word, :wordnet, :word_end, :word_middle, :word_start]
11
11
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
12
12
 
@@ -15,6 +15,7 @@ module Searchkick
15
15
  Searchkick.models << self
16
16
 
17
17
  options[:_type] ||= -> { searchkick_index.klass_document_type(self, true) }
18
+ options[:class_name] = model_name.name
18
19
 
19
20
  callbacks = options.key?(:callbacks) ? options[:callbacks] : :inline
20
21
  unless [:inline, true, false, :async, :queue].include?(callbacks)
@@ -40,12 +41,15 @@ module Searchkick
40
41
 
41
42
  class << self
42
43
  def searchkick_search(term = "*", **options, &block)
43
- Searchkick.search(term, {model: self}.merge(options), &block)
44
+ # TODO throw error in next major version
45
+ Searchkick.warn("calling search on a relation is deprecated") if Searchkick.relation?(self)
46
+
47
+ Searchkick.search(term, model: self, **options, &block)
44
48
  end
45
49
  alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
46
50
 
47
- def searchkick_index
48
- index = class_variable_get(:@@searchkick_index)
51
+ def searchkick_index(name: nil)
52
+ index = name || class_variable_get(:@@searchkick_index)
49
53
  index = index.call if index.respond_to?(:call)
50
54
  index_cache = class_variable_get(:@@searchkick_index_cache)
51
55
  index_cache[index] ||= Searchkick::Index.new(index, searchkick_options)
@@ -53,10 +57,11 @@ module Searchkick
53
57
  alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
54
58
 
55
59
  def searchkick_reindex(method_name = nil, **options)
56
- scoped = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
60
+ # TODO relation = Searchkick.relation?(self)
61
+ relation = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
57
62
  (respond_to?(:queryable) && queryable != unscoped.with_default_scope)
58
63
 
59
- searchkick_index.reindex(searchkick_klass, method_name, scoped: scoped, **options)
64
+ searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
60
65
  end
61
66
  alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
62
67
 
@@ -78,8 +83,9 @@ module Searchkick
78
83
  RecordIndexer.new(self).reindex(method_name, **options)
79
84
  end unless method_defined?(:reindex)
80
85
 
86
+ # TODO switch to keyword arguments
81
87
  def similar(options = {})
82
- self.class.searchkick_index.similar_record(self, options)
88
+ self.class.searchkick_index.similar_record(self, **options)
83
89
  end unless method_defined?(:similar)
84
90
 
85
91
  def search_data
@@ -2,7 +2,7 @@ module Searchkick
2
2
  class ProcessBatchJob < ActiveJob::Base
3
3
  queue_as { Searchkick.queue_name }
4
4
 
5
- def perform(class_name:, record_ids:)
5
+ def perform(class_name:, record_ids:, index_name: nil)
6
6
  # separate routing from id
7
7
  routing = Hash[record_ids.map { |r| r.split(/(?<!\|)\|(?!\|)/, 2).map { |v| v.gsub("||", "|") } }]
8
8
  record_ids = routing.keys
@@ -26,7 +26,7 @@ module Searchkick
26
26
  end
27
27
 
28
28
  # bulk reindex
29
- index = klass.searchkick_index
29
+ index = klass.searchkick_index(name: index_name)
30
30
  Searchkick.callbacks(:bulk) do
31
31
  index.bulk_index(records) if records.any?
32
32
  index.bulk_delete(delete_records) if delete_records.any?
@@ -2,21 +2,29 @@ module Searchkick
2
2
  class ProcessQueueJob < ActiveJob::Base
3
3
  queue_as { Searchkick.queue_name }
4
4
 
5
- def perform(class_name:)
5
+ def perform(class_name:, index_name: nil, inline: false)
6
6
  model = class_name.constantize
7
+ limit = model.searchkick_options[:batch_size] || 1000
7
8
 
8
- limit = model.searchkick_index.options[:batch_size] || 1000
9
- record_ids = model.searchkick_index.reindex_queue.reserve(limit: limit)
10
- if record_ids.any?
11
- Searchkick::ProcessBatchJob.perform_later(
12
- class_name: model.name,
13
- record_ids: record_ids
14
- )
15
- # TODO when moving to reliable queuing, mark as complete
9
+ loop do
10
+ record_ids = model.searchkick_index(name: index_name).reindex_queue.reserve(limit: limit)
11
+ if record_ids.any?
12
+ batch_options = {
13
+ class_name: class_name,
14
+ record_ids: record_ids,
15
+ index_name: index_name
16
+ }
16
17
 
17
- if record_ids.size == limit
18
- Searchkick::ProcessQueueJob.perform_later(class_name: class_name)
18
+ if inline
19
+ # use new.perform to avoid excessive logging
20
+ Searchkick::ProcessBatchJob.new.perform(**batch_options)
21
+ else
22
+ Searchkick::ProcessBatchJob.perform_later(**batch_options)
23
+ end
24
+
25
+ # TODO when moving to reliable queuing, mark as complete
19
26
  end
27
+ break unless record_ids.size == limit
20
28
  end
21
29
  end
22
30
  end
@@ -12,14 +12,14 @@ module Searchkick
12
12
  :took, :error, :model_name, :entry_name, :total_count, :total_entries,
13
13
  :current_page, :per_page, :limit_value, :padding, :total_pages, :num_pages,
14
14
  :offset_value, :offset, :previous_page, :prev_page, :next_page, :first_page?, :last_page?,
15
- :out_of_range?, :hits, :response, :to_a, :first
15
+ :out_of_range?, :hits, :response, :to_a, :first, :scroll
16
16
 
17
17
  def initialize(klass, term = "*", **options)
18
- unknown_keywords = options.keys - [:aggs, :body, :body_options, :boost,
18
+ unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
19
19
  :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain,
20
20
  :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
21
- :match, :misspellings, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
22
- :request_params, :routing, :scope_results, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
21
+ :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
22
+ :request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
23
23
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
24
24
 
25
25
  term = term.to_s
@@ -39,6 +39,7 @@ module Searchkick
39
39
  @misspellings = false
40
40
  @misspellings_below = nil
41
41
  @highlighted_fields = nil
42
+ @index_mapping = nil
42
43
 
43
44
  prepare
44
45
  end
@@ -56,9 +57,19 @@ module Searchkick
56
57
  end
57
58
 
58
59
  def params
60
+ if options[:models]
61
+ @index_mapping = {}
62
+ Array(options[:models]).each do |model|
63
+ # there can be multiple models per index name due to inheritance - see #1259
64
+ (@index_mapping[model.searchkick_index.name] ||= []) << model
65
+ end
66
+ end
67
+
59
68
  index =
60
69
  if options[:index_name]
61
70
  Array(options[:index_name]).map { |v| v.respond_to?(:searchkick_index) ? v.searchkick_index.name : v }.join(",")
71
+ elsif options[:models]
72
+ @index_mapping.keys.join(",")
62
73
  elsif searchkick_index
63
74
  searchkick_index.name
64
75
  else
@@ -71,6 +82,7 @@ module Searchkick
71
82
  }
72
83
  params[:type] = @type if @type
73
84
  params[:routing] = @routing if @routing
85
+ params[:scroll] = @scroll if @scroll
74
86
  params.merge!(options[:request_params]) if options[:request_params]
75
87
  params
76
88
  end
@@ -94,11 +106,16 @@ module Searchkick
94
106
  query = params
95
107
  type = query[:type]
96
108
  index = query[:index].is_a?(Array) ? query[:index].join(",") : query[:index]
109
+ request_params = query.except(:index, :type, :body)
97
110
 
98
111
  # no easy way to tell which host the client will use
99
112
  host = Searchkick.client.transport.hosts.first
100
113
  credentials = host[:user] || host[:password] ? "#{host[:user]}:#{host[:password]}@" : nil
101
- "curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -H 'Content-Type: application/json' -d '#{query[:body].to_json}'"
114
+ params = ["pretty"]
115
+ request_params.each do |k, v|
116
+ params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
117
+ end
118
+ "curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?#{params.join('&')} -H 'Content-Type: application/json' -d '#{query[:body].to_json}'"
102
119
  end
103
120
 
104
121
  def handle_response(response)
@@ -116,11 +133,14 @@ module Searchkick
116
133
  misspellings: @misspellings,
117
134
  term: term,
118
135
  scope_results: options[:scope_results],
119
- index_name: options[:index_name],
120
- total_entries: options[:total_entries]
136
+ total_entries: options[:total_entries],
137
+ index_mapping: @index_mapping,
138
+ suggest: options[:suggest],
139
+ scroll: options[:scroll]
121
140
  }
122
141
 
123
142
  if options[:debug]
143
+ # can remove when minimum Ruby version is 2.5
124
144
  require "pp"
125
145
 
126
146
  puts "Searchkick Version: #{Searchkick::VERSION}"
@@ -166,7 +186,7 @@ module Searchkick
166
186
  end
167
187
 
168
188
  def retry_misspellings?(response)
169
- @misspellings_below && response["hits"]["total"] < @misspellings_below
189
+ @misspellings_below && Searchkick::Results.new(searchkick_klass, response).total_count < @misspellings_below
170
190
  end
171
191
 
172
192
  private
@@ -215,9 +235,12 @@ module Searchkick
215
235
 
216
236
  # pagination
217
237
  page = [options[:page].to_i, 1].max
218
- per_page = (options[:limit] || options[:per_page] || 10_000).to_i
238
+ # maybe use index.max_result_window in the future
239
+ default_limit = searchkick_options[:deep_paging] ? 1_000_000_000 : 10_000
240
+ per_page = (options[:limit] || options[:per_page] || default_limit).to_i
219
241
  padding = [options[:padding].to_i, 0].max
220
242
  offset = options[:offset] || (page - 1) * per_page + padding
243
+ scroll = options[:scroll]
221
244
 
222
245
  # model and eager loading
223
246
  load = options[:load].nil? ? true : options[:load]
@@ -327,7 +350,7 @@ module Searchkick
327
350
  field_misspellings = misspellings && (!misspellings_fields || misspellings_fields.include?(base_field(field)))
328
351
 
329
352
  if field == "_all" || field.end_with?(".analyzed")
330
- shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false
353
+ shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false || (!below73? && !track_total_hits?)
331
354
  qs << shared_options.merge(analyzer: "searchkick_search")
332
355
 
333
356
  # searchkick_search and searchkick_search2 are the same for ukrainian
@@ -377,7 +400,7 @@ module Searchkick
377
400
  queries_to_add.concat(q2)
378
401
  end
379
402
 
380
- queries.concat(queries_to_add)
403
+ queries << queries_to_add
381
404
 
382
405
  if options[:exclude]
383
406
  must_not.concat(set_exclude(exclude_field, exclude_analyzer))
@@ -392,9 +415,10 @@ module Searchkick
392
415
 
393
416
  should = []
394
417
  else
418
+ # higher score for matching more fields
395
419
  payload = {
396
- dis_max: {
397
- queries: queries
420
+ bool: {
421
+ should: queries.map { |qs| {dis_max: {queries: qs}} }
398
422
  }
399
423
  }
400
424
 
@@ -412,6 +436,24 @@ module Searchkick
412
436
  where[:type] = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v, true) }
413
437
  end
414
438
 
439
+ models = Array(options[:models])
440
+ if models.any? { |m| m != m.searchkick_klass }
441
+ # aliases are not supported with _index in ES below 7.5
442
+ # see https://github.com/elastic/elasticsearch/pull/46640
443
+ if below75?
444
+ Searchkick.warn("Passing child models to models option throws off hits and pagination - use type option instead")
445
+ else
446
+ index_type_or =
447
+ models.map do |m|
448
+ v = {_index: m.searchkick_index.name}
449
+ v[:type] = m.searchkick_index.klass_document_type(m, true) if m != m.searchkick_klass
450
+ v
451
+ end
452
+
453
+ where[:or] = Array(where[:or]) + [index_type_or]
454
+ end
455
+ end
456
+
415
457
  # start everything as efficient filters
416
458
  # move to post_filters as aggs demand
417
459
  filters = where_filters(where)
@@ -469,7 +511,7 @@ module Searchkick
469
511
  pagination_options = options[:page] || options[:limit] || options[:per_page] || options[:offset] || options[:padding]
470
512
  if !options[:body] || pagination_options
471
513
  payload[:size] = per_page
472
- payload[:from] = offset
514
+ payload[:from] = offset if offset > 0
473
515
  end
474
516
 
475
517
  # type
@@ -480,14 +522,28 @@ module Searchkick
480
522
  # routing
481
523
  @routing = options[:routing] if options[:routing]
482
524
 
525
+ if track_total_hits?
526
+ payload[:track_total_hits] = true
527
+ end
528
+
483
529
  # merge more body options
484
530
  payload = payload.deep_merge(options[:body_options]) if options[:body_options]
485
531
 
532
+ # run block
533
+ options[:block].call(payload) if options[:block]
534
+
535
+ # scroll optimization when interating over all docs
536
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
537
+ if options[:scroll] && payload[:query] == {match_all: {}}
538
+ payload[:sort] ||= ["_doc"]
539
+ end
540
+
486
541
  @body = payload
487
542
  @page = page
488
543
  @per_page = per_page
489
544
  @padding = padding
490
545
  @load = load
546
+ @scroll = scroll
491
547
  end
492
548
 
493
549
  def set_fields
@@ -518,7 +574,8 @@ module Searchkick
518
574
 
519
575
  def build_query(query, filters, should, must_not, custom_filters, multiply_filters)
520
576
  if filters.any? || must_not.any? || should.any?
521
- bool = {must: query}
577
+ bool = {}
578
+ bool[:must] = query if query
522
579
  bool[:filter] = filters if filters.any? # where
523
580
  bool[:must_not] = must_not if must_not.any? # exclude
524
581
  bool[:should] = should if should.any? # conversions
@@ -660,20 +717,9 @@ module Searchkick
660
717
  def set_boost_by_indices(payload)
661
718
  return unless options[:indices_boost]
662
719
 
663
- if below52?
664
- indices_boost = options[:indices_boost].each_with_object({}) do |(key, boost), memo|
665
- index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key
666
- # try to use index explicitly instead of alias: https://github.com/elasticsearch/elasticsearch/issues/4756
667
- index_by_alias = Searchkick.client.indices.get_alias(index: index).keys.first
668
- memo[index_by_alias || index] = boost
669
- end
670
- else
671
- # array format supports alias resolution
672
- # https://github.com/elastic/elasticsearch/pull/21393
673
- indices_boost = options[:indices_boost].map do |key, boost|
674
- index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key
675
- {index => boost}
676
- end
720
+ indices_boost = options[:indices_boost].map do |key, boost|
721
+ index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key
722
+ {index => boost}
677
723
  end
678
724
 
679
725
  payload[:indices_boost] = indices_boost
@@ -710,7 +756,7 @@ module Searchkick
710
756
  def set_highlights(payload, fields)
711
757
  payload[:highlight] = {
712
758
  fields: Hash[fields.map { |f| [f, {}] }],
713
- fragment_size: below60? ? 30000 : 0
759
+ fragment_size: 0
714
760
  }
715
761
 
716
762
  if options[:highlight].is_a?(Hash)
@@ -821,11 +867,16 @@ module Searchkick
821
867
  # TODO id transformation for arrays
822
868
  def set_order(payload)
823
869
  order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
824
- id_field = below60? ? :_uid : :_id
870
+ id_field = :_id
825
871
  payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
826
872
  end
827
873
 
828
874
  def where_filters(where)
875
+ # if where.respond_to?(:permitted?) && !where.permitted?
876
+ # # TODO check in more places
877
+ # Searchkick.warn("Passing unpermitted parameters will raise an exception in Searchkick 5")
878
+ # end
879
+
829
880
  filters = []
830
881
  (where || {}).each do |field, value|
831
882
  field = :_id if field.to_s == "id"
@@ -840,10 +891,17 @@ module Searchkick
840
891
  filters << {bool: {must_not: where_filters(value)}}
841
892
  elsif field == :_and
842
893
  filters << {bool: {must: value.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
894
+ # elsif field == :_script
895
+ # filters << {script: {script: {source: value, lang: "painless"}}}
843
896
  else
844
897
  # expand ranges
845
898
  if value.is_a?(Range)
846
- value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
899
+ # infinite? added in Ruby 2.4
900
+ if value.end.nil? || (value.end.respond_to?(:infinite?) && value.end.infinite?)
901
+ value = {gte: value.first}
902
+ else
903
+ value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
904
+ end
847
905
  end
848
906
 
849
907
  value = {in: value} if value.is_a?(Array)
@@ -895,6 +953,23 @@ module Searchkick
895
953
  }
896
954
  }
897
955
  }
956
+ when :like
957
+ # based on Postgres
958
+ # https://www.postgresql.org/docs/current/functions-matching.html
959
+ # % matches zero or more characters
960
+ # _ matches one character
961
+ # \ is escape character
962
+ # escape Lucene reserved characters
963
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html#regexp-optional-operators
964
+ reserved = %w(. ? + * | { } [ ] ( ) " \\)
965
+ regex = op_value.dup
966
+ reserved.each do |v|
967
+ regex.gsub!(v, "\\" + v)
968
+ end
969
+ regex = regex.gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
970
+ filters << {regexp: {field => {value: regex, flags: "NONE"}}}
971
+ when :prefix
972
+ filters << {prefix: {field => {value: op_value}}}
898
973
  when :regexp # support for regexp queries without using a regexp ruby object
899
974
  filters << {regexp: {field => {value: op_value}}}
900
975
  when :not, :_not # not equal
@@ -905,6 +980,8 @@ module Searchkick
905
980
  end
906
981
  when :in
907
982
  filters << term_filters(field, op_value)
983
+ when :exists
984
+ filters << {exists: {field: field}}
908
985
  else
909
986
  range_query =
910
987
  case op
@@ -945,9 +1022,43 @@ module Searchkick
945
1022
  elsif value.nil?
946
1023
  {bool: {must_not: {exists: {field: field}}}}
947
1024
  elsif value.is_a?(Regexp)
948
- {regexp: {field => {value: value.source, flags: "NONE"}}}
1025
+ if value.casefold?
1026
+ Searchkick.warn("Case-insensitive flag does not work with Elasticsearch")
1027
+ end
1028
+
1029
+ source = value.source
1030
+ unless source.start_with?("\\A") && source.end_with?("\\z")
1031
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html
1032
+ Searchkick.warn("Regular expressions are always anchored in Elasticsearch")
1033
+ end
1034
+
1035
+ # TODO handle other anchor characters, like ^, $, \Z
1036
+ if source.start_with?("\\A")
1037
+ source = source[2..-1]
1038
+ else
1039
+ # TODO uncomment in Searchkick 5
1040
+ # source = ".*#{source}"
1041
+ end
1042
+
1043
+ if source.end_with?("\\z")
1044
+ source = source[0..-3]
1045
+ else
1046
+ # TODO uncomment in Searchkick 5
1047
+ # source = "#{source}.*"
1048
+ end
1049
+
1050
+ {regexp: {field => {value: source, flags: "NONE"}}}
949
1051
  else
950
- {term: {field => value}}
1052
+ # TODO add this for other values
1053
+ if value.as_json.is_a?(Enumerable)
1054
+ # query will fail, but this is better
1055
+ # same message as Active Record
1056
+ # TODO make TypeError
1057
+ # raise InvalidQueryError for backward compatibility
1058
+ raise Searchkick::InvalidQueryError, "can't cast #{value.class.name}"
1059
+ end
1060
+
1061
+ {term: {field => {value: value}}}
951
1062
  end
952
1063
  end
953
1064
 
@@ -1011,16 +1122,28 @@ module Searchkick
1011
1122
  k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
1012
1123
  end
1013
1124
 
1014
- def below52?
1015
- Searchkick.server_below?("5.2.0")
1125
+ def track_total_hits?
1126
+ (searchkick_options[:deep_paging] && !below70?) || body_options[:track_total_hits]
1016
1127
  end
1017
1128
 
1018
- def below60?
1019
- Searchkick.server_below?("6.0.0")
1129
+ def body_options
1130
+ options[:body_options] || {}
1020
1131
  end
1021
1132
 
1022
1133
  def below61?
1023
1134
  Searchkick.server_below?("6.1.0")
1024
1135
  end
1136
+
1137
+ def below70?
1138
+ Searchkick.server_below?("7.0.0")
1139
+ end
1140
+
1141
+ def below73?
1142
+ Searchkick.server_below?("7.3.0")
1143
+ end
1144
+
1145
+ def below75?
1146
+ Searchkick.server_below?("7.5.0")
1147
+ end
1025
1148
  end
1026
1149
  end