searchkick 4.4.0 → 5.3.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.
@@ -1,5 +1,6 @@
1
1
  module Searchkick
2
2
  class Query
3
+ include Enumerable
3
4
  extend Forwardable
4
5
 
5
6
  @@metric_aggs = [:avg, :cardinality, :max, :min, :sum]
@@ -12,11 +13,12 @@ module Searchkick
12
13
  :took, :error, :model_name, :entry_name, :total_count, :total_entries,
13
14
  :current_page, :per_page, :limit_value, :padding, :total_pages, :num_pages,
14
15
  :offset_value, :offset, :previous_page, :prev_page, :next_page, :first_page?, :last_page?,
15
- :out_of_range?, :hits, :response, :to_a, :first, :scroll
16
+ :out_of_range?, :hits, :response, :to_a, :first, :scroll, :highlights, :with_highlights,
17
+ :with_score, :misspellings?, :scroll_id, :clear_scroll, :missing_records, :with_hit
16
18
 
17
19
  def initialize(klass, term = "*", **options)
18
20
  unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
19
- :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain,
21
+ :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :explain,
20
22
  :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
21
23
  :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
22
24
  :request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
@@ -25,7 +27,7 @@ module Searchkick
25
27
  term = term.to_s
26
28
 
27
29
  if options[:emoji]
28
- term = EmojiParser.parse_unicode(term) { |e| " #{e.name} " }.strip
30
+ term = EmojiParser.parse_unicode(term) { |e| " #{e.name.tr('_', ' ')} " }.strip
29
31
  end
30
32
 
31
33
  @klass = klass
@@ -73,7 +75,8 @@ module Searchkick
73
75
  elsif searchkick_index
74
76
  searchkick_index.name
75
77
  else
76
- "_all"
78
+ # fixes warning about accessing system indices
79
+ "*,-.*"
77
80
  end
78
81
 
79
82
  params = {
@@ -109,7 +112,12 @@ module Searchkick
109
112
  request_params = query.except(:index, :type, :body)
110
113
 
111
114
  # no easy way to tell which host the client will use
112
- host = Searchkick.client.transport.hosts.first
115
+ host =
116
+ if Searchkick.client.transport.respond_to?(:transport)
117
+ Searchkick.client.transport.transport.hosts.first
118
+ else
119
+ Searchkick.client.transport.hosts.first
120
+ end
113
121
  credentials = host[:user] || host[:password] ? "#{host[:user]}:#{host[:password]}@" : nil
114
122
  params = ["pretty"]
115
123
  request_params.each do |k, v|
@@ -140,9 +148,6 @@ module Searchkick
140
148
  }
141
149
 
142
150
  if options[:debug]
143
- # can remove when minimum Ruby version is 2.5
144
- require "pp"
145
-
146
151
  puts "Searchkick Version: #{Searchkick::VERSION}"
147
152
  puts "Elasticsearch Version: #{Searchkick.server_version}"
148
153
  puts
@@ -182,11 +187,11 @@ module Searchkick
182
187
  end
183
188
 
184
189
  # set execute for multi search
185
- @execute = Searchkick::Results.new(searchkick_klass, response, opts)
190
+ @execute = Results.new(searchkick_klass, response, opts)
186
191
  end
187
192
 
188
193
  def retry_misspellings?(response)
189
- @misspellings_below && Searchkick::Results.new(searchkick_klass, response).total_count < @misspellings_below
194
+ @misspellings_below && response["error"].nil? && Results.new(searchkick_klass, response).total_count < @misspellings_below
190
195
  end
191
196
 
192
197
  private
@@ -194,7 +199,11 @@ module Searchkick
194
199
  def handle_error(e)
195
200
  status_code = e.message[1..3].to_i
196
201
  if status_code == 404
197
- raise MissingIndexError, "Index missing - run #{reindex_command}"
202
+ if e.message.include?("No search context found for id")
203
+ raise MissingIndexError, "No search context found for id"
204
+ else
205
+ raise MissingIndexError, "Index missing - run #{reindex_command}"
206
+ end
198
207
  elsif status_code == 500 && (
199
208
  e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") ||
200
209
  e.message.include?("No query registered for [multi_match]") ||
@@ -202,14 +211,14 @@ module Searchkick
202
211
  e.message.include?("No query registered for [function_score]")
203
212
  )
204
213
 
205
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 5 or greater"
214
+ raise UnsupportedVersionError
206
215
  elsif status_code == 400
207
216
  if (
208
217
  e.message.include?("bool query does not support [filter]") ||
209
218
  e.message.include?("[bool] filter does not support [filter]")
210
219
  )
211
220
 
212
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 5 or greater"
221
+ raise UnsupportedVersionError
213
222
  elsif e.message =~ /analyzer \[searchkick_.+\] not found/
214
223
  raise InvalidQueryError, "Bad mapping - run #{reindex_command}"
215
224
  else
@@ -225,7 +234,14 @@ module Searchkick
225
234
  end
226
235
 
227
236
  def execute_search
228
- Searchkick.client.search(params)
237
+ name = searchkick_klass ? "#{searchkick_klass.name} Search" : "Search"
238
+ event = {
239
+ name: name,
240
+ query: params
241
+ }
242
+ ActiveSupport::Notifications.instrument("search.searchkick", event) do
243
+ Searchkick.client.search(params)
244
+ end
229
245
  end
230
246
 
231
247
  def prepare
@@ -239,9 +255,15 @@ module Searchkick
239
255
  default_limit = searchkick_options[:deep_paging] ? 1_000_000_000 : 10_000
240
256
  per_page = (options[:limit] || options[:per_page] || default_limit).to_i
241
257
  padding = [options[:padding].to_i, 0].max
242
- offset = options[:offset] || (page - 1) * per_page + padding
258
+ offset = (options[:offset] || (page - 1) * per_page + padding).to_i
243
259
  scroll = options[:scroll]
244
260
 
261
+ max_result_window = searchkick_options[:max_result_window]
262
+ if max_result_window
263
+ offset = max_result_window if offset > max_result_window
264
+ per_page = max_result_window - offset if offset + per_page > max_result_window
265
+ end
266
+
245
267
  # model and eager loading
246
268
  load = options[:load].nil? ? true : options[:load]
247
269
 
@@ -260,9 +282,10 @@ module Searchkick
260
282
  should = []
261
283
 
262
284
  if options[:similar]
285
+ like = options[:similar] == true ? term : options[:similar]
263
286
  query = {
264
287
  more_like_this: {
265
- like: term,
288
+ like: like,
266
289
  min_doc_freq: 1,
267
290
  min_term_freq: 1,
268
291
  analyzer: "searchkick_search2"
@@ -350,11 +373,11 @@ module Searchkick
350
373
  field_misspellings = misspellings && (!misspellings_fields || misspellings_fields.include?(base_field(field)))
351
374
 
352
375
  if field == "_all" || field.end_with?(".analyzed")
353
- shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false || (!below73? && !track_total_hits?)
376
+ shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false || (!below73? && !track_total_hits?) || match_type == :match_phrase || !below80? || Searchkick.opensearch?
354
377
  qs << shared_options.merge(analyzer: "searchkick_search")
355
378
 
356
- # searchkick_search and searchkick_search2 are the same for ukrainian
357
- unless %w(japanese korean polish ukrainian vietnamese).include?(searchkick_options[:language])
379
+ # searchkick_search and searchkick_search2 are the same for some languages
380
+ unless %w(japanese japanese2 korean polish ukrainian vietnamese).include?(searchkick_options[:language])
358
381
  qs << shared_options.merge(analyzer: "searchkick_search2")
359
382
  end
360
383
  exclude_analyzer = "searchkick_search2"
@@ -375,11 +398,6 @@ module Searchkick
375
398
 
376
399
  if field.start_with?("*.")
377
400
  q2 = qs.map { |q| {multi_match: q.merge(fields: [field], type: match_type == :match_phrase ? "phrase" : "best_fields")} }
378
- if below61?
379
- q2.each do |q|
380
- q[:multi_match].delete(:fuzzy_transpositions)
381
- end
382
- end
383
401
  else
384
402
  q2 = qs.map { |q| {match_type => {field => q}} }
385
403
  end
@@ -431,7 +449,7 @@ module Searchkick
431
449
  payload = {}
432
450
 
433
451
  # type when inheritance
434
- where = (options[:where] || {}).dup
452
+ where = ensure_permitted(options[:where] || {}).dup
435
453
  if searchkick_options[:inheritance] && (options[:type] || (klass != searchkick_klass && searchkick_index))
436
454
  where[:type] = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v, true) }
437
455
  end
@@ -491,7 +509,7 @@ module Searchkick
491
509
  set_highlights(payload, fields) if options[:highlight]
492
510
 
493
511
  # timeout shortly after client times out
494
- payload[:timeout] ||= "#{Searchkick.search_timeout + 1}s"
512
+ payload[:timeout] ||= "#{((Searchkick.search_timeout + 1) * 1000).round}ms"
495
513
 
496
514
  # An empty array will cause only the _id and _type for each hit to be returned
497
515
  # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
@@ -688,9 +706,9 @@ module Searchkick
688
706
  def set_boost_by(multiply_filters, custom_filters)
689
707
  boost_by = options[:boost_by] || {}
690
708
  if boost_by.is_a?(Array)
691
- boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }]
709
+ boost_by = boost_by.to_h { |f| [f, {factor: 1}] }
692
710
  elsif boost_by.is_a?(Hash)
693
- multiply_by, boost_by = boost_by.partition { |_, v| v.delete(:boost_mode) == "multiply" }.map { |i| Hash[i] }
711
+ multiply_by, boost_by = boost_by.partition { |_, v| v.delete(:boost_mode) == "multiply" }.map(&:to_h)
694
712
  end
695
713
  boost_by[options[:boost]] = {factor: 1} if options[:boost]
696
714
 
@@ -755,7 +773,7 @@ module Searchkick
755
773
 
756
774
  def set_highlights(payload, fields)
757
775
  payload[:highlight] = {
758
- fields: Hash[fields.map { |f| [f, {}] }],
776
+ fields: fields.to_h { |f| [f, {}] },
759
777
  fragment_size: 0
760
778
  }
761
779
 
@@ -789,7 +807,7 @@ module Searchkick
789
807
  aggs = options[:aggs]
790
808
  payload[:aggs] = {}
791
809
 
792
- aggs = Hash[aggs.map { |f| [f, {}] }] if aggs.is_a?(Array) # convert to more advanced syntax
810
+ aggs = aggs.to_h { |f| [f, {}] } if aggs.is_a?(Array) # convert to more advanced syntax
793
811
  aggs.each do |field, agg_options|
794
812
  size = agg_options[:limit] ? agg_options[:limit] : 1_000
795
813
  shared_agg_options = agg_options.except(:limit, :field, :ranges, :date_ranges, :where)
@@ -828,8 +846,9 @@ module Searchkick
828
846
  end
829
847
 
830
848
  where = {}
831
- where = (options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
832
- agg_filters = where_filters(where.merge(agg_options[:where] || {}))
849
+ where = ensure_permitted(options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
850
+ agg_where = ensure_permitted(agg_options[:where] || {})
851
+ agg_filters = where_filters(where.merge(agg_where))
833
852
 
834
853
  # only do one level comparison for simplicity
835
854
  filters.select! do |filter|
@@ -864,19 +883,17 @@ module Searchkick
864
883
  }
865
884
  end
866
885
 
867
- # TODO id transformation for arrays
868
886
  def set_order(payload)
869
- order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
870
- id_field = :_id
871
- payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
887
+ payload[:sort] = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
872
888
  end
873
889
 
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
890
+ # provides *very* basic protection from unfiltered parameters
891
+ # this is not meant to be comprehensive and may be expanded in the future
892
+ def ensure_permitted(obj)
893
+ obj.to_h
894
+ end
879
895
 
896
+ def where_filters(where)
880
897
  filters = []
881
898
  (where || {}).each do |field, value|
882
899
  field = :_id if field.to_s == "id"
@@ -896,12 +913,7 @@ module Searchkick
896
913
  else
897
914
  # expand ranges
898
915
  if value.is_a?(Range)
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
916
+ value = expand_range(value)
905
917
  end
906
918
 
907
919
  value = {in: value} if value.is_a?(Array)
@@ -953,7 +965,7 @@ module Searchkick
953
965
  }
954
966
  }
955
967
  }
956
- when :like
968
+ when :like, :ilike
957
969
  # based on Postgres
958
970
  # https://www.postgresql.org/docs/current/functions-matching.html
959
971
  # % matches zero or more characters
@@ -961,13 +973,22 @@ module Searchkick
961
973
  # \ is escape character
962
974
  # escape Lucene reserved characters
963
975
  # https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html#regexp-optional-operators
964
- reserved = %w(. ? + * | { } [ ] ( ) " \\)
976
+ reserved = %w(\\ . ? + * | { } [ ] ( ) ")
965
977
  regex = op_value.dup
966
978
  reserved.each do |v|
967
- regex.gsub!(v, "\\" + v)
979
+ regex.gsub!(v, "\\\\" + v)
968
980
  end
969
981
  regex = regex.gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
970
- filters << {regexp: {field => {value: regex}}}
982
+
983
+ if op == :ilike
984
+ if below710?
985
+ raise ArgumentError, "ilike requires Elasticsearch 7.10+"
986
+ else
987
+ filters << {regexp: {field => {value: regex, flags: "NONE", case_insensitive: true}}}
988
+ end
989
+ else
990
+ filters << {regexp: {field => {value: regex, flags: "NONE"}}}
991
+ end
971
992
  when :prefix
972
993
  filters << {prefix: {field => {value: op_value}}}
973
994
  when :regexp # support for regexp queries without using a regexp ruby object
@@ -994,7 +1015,7 @@ module Searchkick
994
1015
  when :lte
995
1016
  {to: op_value, include_upper: true}
996
1017
  else
997
- raise "Unknown where operator: #{op.inspect}"
1018
+ raise ArgumentError, "Unknown where operator: #{op.inspect}"
998
1019
  end
999
1020
  # issue 132
1000
1021
  if (existing = filters.find { |f| f[:range] && f[:range][field] })
@@ -1022,40 +1043,37 @@ module Searchkick
1022
1043
  elsif value.nil?
1023
1044
  {bool: {must_not: {exists: {field: field}}}}
1024
1045
  elsif value.is_a?(Regexp)
1025
- if value.casefold?
1026
- Searchkick.warn("Case-insensitive flag does not work with Elasticsearch")
1027
- end
1028
-
1029
1046
  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
1047
+
1048
+ # TODO handle other regexp options
1034
1049
 
1035
1050
  # TODO handle other anchor characters, like ^, $, \Z
1036
1051
  if source.start_with?("\\A")
1037
1052
  source = source[2..-1]
1038
1053
  else
1039
- # TODO uncomment in Searchkick 5
1040
- # source = ".*#{source}"
1054
+ source = ".*#{source}"
1041
1055
  end
1042
1056
 
1043
1057
  if source.end_with?("\\z")
1044
1058
  source = source[0..-3]
1045
1059
  else
1046
- # TODO uncomment in Searchkick 5
1047
- # source = "#{source}.*"
1060
+ source = "#{source}.*"
1048
1061
  end
1049
1062
 
1050
- {regexp: {field => {value: source, flags: "NONE"}}}
1063
+ if below710?
1064
+ if value.casefold?
1065
+ raise ArgumentError, "Case-insensitive flag does not work with Elasticsearch < 7.10"
1066
+ end
1067
+ {regexp: {field => {value: source, flags: "NONE"}}}
1068
+ else
1069
+ {regexp: {field => {value: source, flags: "NONE", case_insensitive: value.casefold?}}}
1070
+ end
1051
1071
  else
1052
1072
  # TODO add this for other values
1053
1073
  if value.as_json.is_a?(Enumerable)
1054
1074
  # query will fail, but this is better
1055
1075
  # 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}"
1076
+ raise TypeError, "can't cast #{value.class.name}"
1059
1077
  end
1060
1078
 
1061
1079
  {term: {field => {value: value}}}
@@ -1118,26 +1136,29 @@ module Searchkick
1118
1136
  end
1119
1137
  end
1120
1138
 
1139
+ def expand_range(range)
1140
+ expanded = {}
1141
+ expanded[:gte] = range.begin if range.begin
1142
+
1143
+ if range.end && !(range.end.respond_to?(:infinite?) && range.end.infinite?)
1144
+ expanded[range.exclude_end? ? :lt : :lte] = range.end
1145
+ end
1146
+
1147
+ expanded
1148
+ end
1149
+
1121
1150
  def base_field(k)
1122
1151
  k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
1123
1152
  end
1124
1153
 
1125
1154
  def track_total_hits?
1126
- (searchkick_options[:deep_paging] && !below70?) || body_options[:track_total_hits]
1155
+ searchkick_options[:deep_paging] || body_options[:track_total_hits]
1127
1156
  end
1128
1157
 
1129
1158
  def body_options
1130
1159
  options[:body_options] || {}
1131
1160
  end
1132
1161
 
1133
- def below61?
1134
- Searchkick.server_below?("6.1.0")
1135
- end
1136
-
1137
- def below70?
1138
- Searchkick.server_below?("7.0.0")
1139
- end
1140
-
1141
1162
  def below73?
1142
1163
  Searchkick.server_below?("7.3.0")
1143
1164
  end
@@ -1145,5 +1166,13 @@ module Searchkick
1145
1166
  def below75?
1146
1167
  Searchkick.server_below?("7.5.0")
1147
1168
  end
1169
+
1170
+ def below710?
1171
+ Searchkick.server_below?("7.10.0")
1172
+ end
1173
+
1174
+ def below80?
1175
+ Searchkick.server_below?("8.0.0")
1176
+ end
1148
1177
  end
1149
1178
  end
@@ -25,6 +25,7 @@ module Searchkick
25
25
  {delete: record_data}
26
26
  end
27
27
 
28
+ # custom id can be useful for load: false
28
29
  def search_id
29
30
  id = record.respond_to?(:search_document_id) ? record.search_document_id : record.id
30
31
  id.is_a?(Numeric) ? id : id.to_s
@@ -39,7 +40,6 @@ module Searchkick
39
40
  _index: index.name,
40
41
  _id: search_id
41
42
  }
42
- data[:_type] = document_type if Searchkick.server_below7?
43
43
  data[:routing] = record.search_routing if record.respond_to?(:search_routing)
44
44
  data
45
45
  end
@@ -1,78 +1,163 @@
1
1
  module Searchkick
2
2
  class RecordIndexer
3
- attr_reader :record, :index
3
+ attr_reader :index
4
4
 
5
- def initialize(record)
6
- @record = record
7
- @index = record.class.searchkick_index
5
+ def initialize(index)
6
+ @index = index
8
7
  end
9
8
 
10
- def reindex(method_name = nil, refresh: false, mode: nil)
11
- unless [:inline, true, nil, :async, :queue].include?(mode)
12
- raise ArgumentError, "Invalid value for mode"
13
- end
14
-
15
- mode ||= Searchkick.callbacks_value || index.options[:callbacks] || true
9
+ def reindex(records, mode:, method_name:, full: false, single: false)
10
+ # prevents exists? check if records is a relation
11
+ records = records.to_a
12
+ return if records.empty?
16
13
 
17
14
  case mode
15
+ when :async
16
+ unless defined?(ActiveJob)
17
+ raise Error, "Active Job not found"
18
+ end
19
+
20
+ # we could likely combine ReindexV2Job, BulkReindexJob, and ProcessBatchJob
21
+ # but keep them separate for now
22
+ if single
23
+ record = records.first
24
+
25
+ # always pass routing in case record is deleted
26
+ # before the async job runs
27
+ if record.respond_to?(:search_routing)
28
+ routing = record.search_routing
29
+ end
30
+
31
+ Searchkick::ReindexV2Job.perform_later(
32
+ record.class.name,
33
+ record.id.to_s,
34
+ method_name ? method_name.to_s : nil,
35
+ routing: routing,
36
+ index_name: index.name
37
+ )
38
+ else
39
+ Searchkick::BulkReindexJob.perform_later(
40
+ class_name: records.first.class.searchkick_options[:class_name],
41
+ record_ids: records.map { |r| r.id.to_s },
42
+ index_name: index.name,
43
+ method_name: method_name ? method_name.to_s : nil
44
+ )
45
+ end
18
46
  when :queue
19
47
  if method_name
20
- raise Searchkick::Error, "Partial reindex not supported with queue option"
48
+ raise Error, "Partial reindex not supported with queue option"
21
49
  end
22
50
 
23
- # always pass routing in case record is deleted
24
- # before the queue job runs
25
- if record.respond_to?(:search_routing)
26
- routing = record.search_routing
27
- end
51
+ index.reindex_queue.push_records(records)
52
+ when true, :inline
53
+ index_records, other_records = records.partition { |r| index_record?(r) }
54
+ import_inline(index_records, !full ? other_records : [], method_name: method_name, single: single)
55
+ else
56
+ raise ArgumentError, "Invalid value for mode"
57
+ end
28
58
 
29
- # escape pipe with double pipe
30
- value = queue_escape(record.id.to_s)
31
- value = "#{value}|#{queue_escape(routing)}" if routing
32
- index.reindex_queue.push(value)
33
- when :async
34
- unless defined?(ActiveJob)
35
- raise Searchkick::Error, "Active Job not found"
36
- end
59
+ # return true like model and relation reindex for now
60
+ true
61
+ end
37
62
 
38
- # always pass routing in case record is deleted
39
- # before the async job runs
40
- if record.respond_to?(:search_routing)
41
- routing = record.search_routing
42
- end
63
+ def reindex_items(klass, items, method_name:, single: false)
64
+ routing = items.to_h { |r| [r[:id], r[:routing]] }
65
+ record_ids = routing.keys
43
66
 
44
- Searchkick::ReindexV2Job.perform_later(
45
- record.class.name,
46
- record.id.to_s,
47
- method_name ? method_name.to_s : nil,
48
- routing: routing
49
- )
50
- else # bulk, inline/true/nil
51
- reindex_record(method_name)
67
+ relation = Searchkick.load_records(klass, record_ids)
68
+ # call search_import even for single records for nested associations
69
+ relation = relation.search_import if relation.respond_to?(:search_import)
70
+ records = relation.select(&:should_index?)
52
71
 
53
- index.refresh if refresh
54
- end
72
+ # determine which records to delete
73
+ delete_ids = record_ids - records.map { |r| r.id.to_s }
74
+ delete_records =
75
+ delete_ids.map do |id|
76
+ construct_record(klass, id, routing[id])
77
+ end
78
+
79
+ import_inline(records, delete_records, method_name: method_name, single: single)
55
80
  end
56
81
 
57
82
  private
58
83
 
59
- def queue_escape(value)
60
- value.gsub("|", "||")
84
+ def index_record?(record)
85
+ record.persisted? && !record.destroyed? && record.should_index?
61
86
  end
62
87
 
63
- def reindex_record(method_name)
64
- if record.destroyed? || !record.persisted? || !record.should_index?
65
- begin
66
- index.remove(record)
67
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
68
- # do nothing
88
+ # import in single request with retries
89
+ def import_inline(index_records, delete_records, method_name:, single:)
90
+ return if index_records.empty? && delete_records.empty?
91
+
92
+ maybe_bulk(index_records, delete_records, method_name, single) do
93
+ if index_records.any?
94
+ if method_name
95
+ index.bulk_update(index_records, method_name)
96
+ else
97
+ index.bulk_index(index_records)
98
+ end
69
99
  end
100
+
101
+ if delete_records.any?
102
+ index.bulk_delete(delete_records)
103
+ end
104
+ end
105
+ end
106
+
107
+ def maybe_bulk(index_records, delete_records, method_name, single)
108
+ if Searchkick.callbacks_value == :bulk
109
+ yield
70
110
  else
71
- if method_name
72
- index.update_record(record, method_name)
73
- else
74
- index.store(record)
111
+ # set action and data
112
+ action =
113
+ if single && index_records.empty?
114
+ "Remove"
115
+ elsif method_name
116
+ "Update"
117
+ else
118
+ single ? "Store" : "Import"
119
+ end
120
+ record = index_records.first || delete_records.first
121
+ name = record.class.searchkick_klass.name
122
+ message = lambda do |event|
123
+ event[:name] = "#{name} #{action}"
124
+ if single
125
+ event[:id] = index.search_id(record)
126
+ else
127
+ event[:count] = index_records.size + delete_records.size
128
+ end
129
+ end
130
+
131
+ with_retries do
132
+ Searchkick.callbacks(:bulk, message: message) do
133
+ yield
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def construct_record(klass, id, routing)
140
+ record = klass.new
141
+ record.id = id
142
+ if routing
143
+ record.define_singleton_method(:search_routing) do
144
+ routing
145
+ end
146
+ end
147
+ record
148
+ end
149
+
150
+ def with_retries
151
+ retries = 0
152
+
153
+ begin
154
+ yield
155
+ rescue Faraday::ClientError => e
156
+ if retries < 1
157
+ retries += 1
158
+ retry
75
159
  end
160
+ raise e
76
161
  end
77
162
  end
78
163
  end