searchkick 4.6.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,7 +7,7 @@ module Searchkick
7
7
  :filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language,
8
8
  :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
9
9
  :special_characters, :stem, :stemmer, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
10
- :text_middle, :text_start, :word, :wordnet, :word_end, :word_middle, :word_start]
10
+ :text_middle, :text_start, :unscope, :word, :word_end, :word_middle, :word_start]
11
11
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
12
12
 
13
13
  raise "Only call searchkick once per model" if respond_to?(:searchkick_index)
@@ -22,52 +22,76 @@ module Searchkick
22
22
  raise ArgumentError, "Invalid value for callbacks"
23
23
  end
24
24
 
25
- index_name =
26
- if options[:index_name]
27
- options[:index_name]
28
- elsif options[:index_prefix].respond_to?(:call)
29
- -> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
30
- else
31
- [options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
25
+ mod = Module.new
26
+ include(mod)
27
+ mod.module_eval do
28
+ def reindex(method_name = nil, mode: nil, refresh: false)
29
+ self.class.searchkick_index.reindex([self], method_name: method_name, mode: mode, refresh: refresh, single: true)
32
30
  end
33
31
 
32
+ def similar(**options)
33
+ self.class.searchkick_index.similar_record(self, **options)
34
+ end
35
+
36
+ def search_data
37
+ data = respond_to?(:to_hash) ? to_hash : serializable_hash
38
+ data.delete("id")
39
+ data.delete("_id")
40
+ data.delete("_type")
41
+ data
42
+ end
43
+
44
+ def should_index?
45
+ true
46
+ end
47
+ end
48
+
34
49
  class_eval do
35
- cattr_reader :searchkick_options, :searchkick_klass
50
+ cattr_reader :searchkick_options, :searchkick_klass, instance_reader: false
36
51
 
37
52
  class_variable_set :@@searchkick_options, options.dup
38
53
  class_variable_set :@@searchkick_klass, self
39
- class_variable_set :@@searchkick_index, index_name
40
- class_variable_set :@@searchkick_index_cache, {}
54
+ class_variable_set :@@searchkick_index_cache, Searchkick::IndexCache.new
41
55
 
42
56
  class << self
43
57
  def searchkick_search(term = "*", **options, &block)
44
- # TODO throw error in next major version
45
- Searchkick.warn("calling search on a relation is deprecated") if Searchkick.relation?(self)
58
+ if Searchkick.relation?(self)
59
+ raise Searchkick::Error, "search must be called on model, not relation"
60
+ end
46
61
 
47
62
  Searchkick.search(term, model: self, **options, &block)
48
63
  end
49
64
  alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
50
65
 
51
66
  def searchkick_index(name: nil)
52
- index = name || class_variable_get(:@@searchkick_index)
67
+ index = name || searchkick_index_name
53
68
  index = index.call if index.respond_to?(:call)
54
69
  index_cache = class_variable_get(:@@searchkick_index_cache)
55
- index_cache[index] ||= Searchkick::Index.new(index, searchkick_options)
70
+ index_cache.fetch(index) { Searchkick::Index.new(index, searchkick_options) }
56
71
  end
57
72
  alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
58
73
 
59
74
  def searchkick_reindex(method_name = nil, **options)
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) ||
62
- (respond_to?(:queryable) && queryable != unscoped.with_default_scope)
63
-
64
- searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
75
+ searchkick_index.reindex(self, method_name: method_name, **options)
65
76
  end
66
77
  alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
67
78
 
68
79
  def searchkick_index_options
69
80
  searchkick_index.index_options
70
81
  end
82
+
83
+ def searchkick_index_name
84
+ @searchkick_index_name ||= begin
85
+ options = class_variable_get(:@@searchkick_options)
86
+ if options[:index_name]
87
+ options[:index_name]
88
+ elsif options[:index_prefix].respond_to?(:call)
89
+ -> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
90
+ else
91
+ [options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
92
+ end
93
+ end
94
+ end
71
95
  end
72
96
 
73
97
  # always add callbacks, even when callbacks is false
@@ -78,33 +102,6 @@ module Searchkick
78
102
  after_save :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
79
103
  after_destroy :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
80
104
  end
81
-
82
- def reindex(method_name = nil, **options)
83
- RecordIndexer.new(self).reindex(method_name, **options)
84
- end unless method_defined?(:reindex)
85
-
86
- # TODO switch to keyword arguments
87
- def similar(options = {})
88
- self.class.searchkick_index.similar_record(self, **options)
89
- end unless method_defined?(:similar)
90
-
91
- def search_data
92
- data = respond_to?(:to_hash) ? to_hash : serializable_hash
93
- data.delete("id")
94
- data.delete("_id")
95
- data.delete("_type")
96
- data
97
- end unless method_defined?(:search_data)
98
-
99
- def should_index?
100
- true
101
- end unless method_defined?(:should_index?)
102
-
103
- if defined?(Cequel) && self < Cequel::Record && !method_defined?(:destroyed?)
104
- def destroyed?
105
- transient?
106
- end
107
- end
108
105
  end
109
106
  end
110
107
  end
@@ -3,34 +3,18 @@ module Searchkick
3
3
  queue_as { Searchkick.queue_name }
4
4
 
5
5
  def perform(class_name:, record_ids:, index_name: nil)
6
- # separate routing from id
7
- routing = Hash[record_ids.map { |r| r.split(/(?<!\|)\|(?!\|)/, 2).map { |v| v.gsub("||", "|") } }]
8
- record_ids = routing.keys
6
+ model = Searchkick.load_model(class_name)
7
+ index = model.searchkick_index(name: index_name)
9
8
 
10
- klass = class_name.constantize
11
- scope = Searchkick.load_records(klass, record_ids)
12
- scope = scope.search_import if scope.respond_to?(:search_import)
13
- records = scope.select(&:should_index?)
14
-
15
- # determine which records to delete
16
- delete_ids = record_ids - records.map { |r| r.id.to_s }
17
- delete_records = delete_ids.map do |id|
18
- m = klass.new
19
- m.id = id
20
- if routing[id]
21
- m.define_singleton_method(:search_routing) do
22
- routing[id]
23
- end
9
+ items =
10
+ record_ids.map do |r|
11
+ parts = r.split(/(?<!\|)\|(?!\|)/, 2)
12
+ .map { |v| v.gsub("||", "|") }
13
+ {id: parts[0], routing: parts[1]}
24
14
  end
25
- m
26
- end
27
15
 
28
- # bulk reindex
29
- index = klass.searchkick_index(name: index_name)
30
- Searchkick.callbacks(:bulk) do
31
- index.bulk_index(records) if records.any?
32
- index.bulk_delete(delete_records) if delete_records.any?
33
- end
16
+ relation = Searchkick.scope(model)
17
+ RecordIndexer.new(index).reindex_items(relation, items, method_name: nil)
34
18
  end
35
19
  end
36
20
  end
@@ -3,11 +3,12 @@ module Searchkick
3
3
  queue_as { Searchkick.queue_name }
4
4
 
5
5
  def perform(class_name:, index_name: nil, inline: false)
6
- model = class_name.constantize
6
+ model = Searchkick.load_model(class_name)
7
+ index = model.searchkick_index(name: index_name)
7
8
  limit = model.searchkick_options[:batch_size] || 1000
8
9
 
9
10
  loop do
10
- record_ids = model.searchkick_index(name: index_name).reindex_queue.reserve(limit: limit)
11
+ record_ids = index.reindex_queue.reserve(limit: limit)
11
12
  if record_ids.any?
12
13
  batch_options = {
13
14
  class_name: class_name,
@@ -18,7 +18,7 @@ module Searchkick
18
18
 
19
19
  def initialize(klass, term = "*", **options)
20
20
  unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
21
- :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,
22
22
  :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
23
23
  :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
24
24
  :request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
@@ -148,9 +148,6 @@ module Searchkick
148
148
  }
149
149
 
150
150
  if options[:debug]
151
- # can remove when minimum Ruby version is 2.5
152
- require "pp"
153
-
154
151
  puts "Searchkick Version: #{Searchkick::VERSION}"
155
152
  puts "Elasticsearch Version: #{Searchkick.server_version}"
156
153
  puts
@@ -210,14 +207,14 @@ module Searchkick
210
207
  e.message.include?("No query registered for [function_score]")
211
208
  )
212
209
 
213
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 5 or greater"
210
+ raise UnsupportedVersionError
214
211
  elsif status_code == 400
215
212
  if (
216
213
  e.message.include?("bool query does not support [filter]") ||
217
214
  e.message.include?("[bool] filter does not support [filter]")
218
215
  )
219
216
 
220
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 5 or greater"
217
+ raise UnsupportedVersionError
221
218
  elsif e.message =~ /analyzer \[searchkick_.+\] not found/
222
219
  raise InvalidQueryError, "Bad mapping - run #{reindex_command}"
223
220
  else
@@ -233,7 +230,14 @@ module Searchkick
233
230
  end
234
231
 
235
232
  def execute_search
236
- Searchkick.client.search(params)
233
+ name = searchkick_klass ? "#{searchkick_klass.name} Search" : "Search"
234
+ event = {
235
+ name: name,
236
+ query: params
237
+ }
238
+ ActiveSupport::Notifications.instrument("search.searchkick", event) do
239
+ Searchkick.client.search(params)
240
+ end
237
241
  end
238
242
 
239
243
  def prepare
@@ -268,9 +272,10 @@ module Searchkick
268
272
  should = []
269
273
 
270
274
  if options[:similar]
275
+ like = options[:similar] == true ? term : options[:similar]
271
276
  query = {
272
277
  more_like_this: {
273
- like: term,
278
+ like: like,
274
279
  min_doc_freq: 1,
275
280
  min_term_freq: 1,
276
281
  analyzer: "searchkick_search2"
@@ -383,11 +388,6 @@ module Searchkick
383
388
 
384
389
  if field.start_with?("*.")
385
390
  q2 = qs.map { |q| {multi_match: q.merge(fields: [field], type: match_type == :match_phrase ? "phrase" : "best_fields")} }
386
- if below61?
387
- q2.each do |q|
388
- q[:multi_match].delete(:fuzzy_transpositions)
389
- end
390
- end
391
391
  else
392
392
  q2 = qs.map { |q| {match_type => {field => q}} }
393
393
  end
@@ -439,7 +439,7 @@ module Searchkick
439
439
  payload = {}
440
440
 
441
441
  # type when inheritance
442
- where = (options[:where] || {}).dup
442
+ where = ensure_permitted(options[:where] || {}).dup
443
443
  if searchkick_options[:inheritance] && (options[:type] || (klass != searchkick_klass && searchkick_index))
444
444
  where[:type] = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v, true) }
445
445
  end
@@ -696,9 +696,9 @@ module Searchkick
696
696
  def set_boost_by(multiply_filters, custom_filters)
697
697
  boost_by = options[:boost_by] || {}
698
698
  if boost_by.is_a?(Array)
699
- boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }]
699
+ boost_by = boost_by.to_h { |f| [f, {factor: 1}] }
700
700
  elsif boost_by.is_a?(Hash)
701
- multiply_by, boost_by = boost_by.partition { |_, v| v.delete(:boost_mode) == "multiply" }.map { |i| Hash[i] }
701
+ multiply_by, boost_by = boost_by.partition { |_, v| v.delete(:boost_mode) == "multiply" }.map(&:to_h)
702
702
  end
703
703
  boost_by[options[:boost]] = {factor: 1} if options[:boost]
704
704
 
@@ -763,7 +763,7 @@ module Searchkick
763
763
 
764
764
  def set_highlights(payload, fields)
765
765
  payload[:highlight] = {
766
- fields: Hash[fields.map { |f| [f, {}] }],
766
+ fields: fields.to_h { |f| [f, {}] },
767
767
  fragment_size: 0
768
768
  }
769
769
 
@@ -797,7 +797,7 @@ module Searchkick
797
797
  aggs = options[:aggs]
798
798
  payload[:aggs] = {}
799
799
 
800
- aggs = Hash[aggs.map { |f| [f, {}] }] if aggs.is_a?(Array) # convert to more advanced syntax
800
+ aggs = aggs.to_h { |f| [f, {}] } if aggs.is_a?(Array) # convert to more advanced syntax
801
801
  aggs.each do |field, agg_options|
802
802
  size = agg_options[:limit] ? agg_options[:limit] : 1_000
803
803
  shared_agg_options = agg_options.except(:limit, :field, :ranges, :date_ranges, :where)
@@ -836,8 +836,9 @@ module Searchkick
836
836
  end
837
837
 
838
838
  where = {}
839
- where = (options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
840
- agg_filters = where_filters(where.merge(agg_options[:where] || {}))
839
+ where = ensure_permitted(options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
840
+ agg_where = ensure_permitted(agg_options[:where] || {})
841
+ agg_filters = where_filters(where.merge(agg_where))
841
842
 
842
843
  # only do one level comparison for simplicity
843
844
  filters.select! do |filter|
@@ -873,19 +874,16 @@ module Searchkick
873
874
  end
874
875
 
875
876
  def set_order(payload)
876
- order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
877
- id_field = :_id
878
- # TODO no longer map id to _id in Searchkick 5
879
- # since sorting on _id is deprecated in Elasticsearch
880
- payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
877
+ payload[:sort] = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
881
878
  end
882
879
 
883
- def where_filters(where)
884
- # if where.respond_to?(:permitted?) && !where.permitted?
885
- # # TODO check in more places
886
- # Searchkick.warn("Passing unpermitted parameters will raise an exception in Searchkick 5")
887
- # end
880
+ # provides *very* basic protection from unfiltered parameters
881
+ # this is not meant to be comprehensive and may be expanded in the future
882
+ def ensure_permitted(obj)
883
+ obj.to_h
884
+ end
888
885
 
886
+ def where_filters(where)
889
887
  filters = []
890
888
  (where || {}).each do |field, value|
891
889
  field = :_id if field.to_s == "id"
@@ -1007,7 +1005,7 @@ module Searchkick
1007
1005
  when :lte
1008
1006
  {to: op_value, include_upper: true}
1009
1007
  else
1010
- raise "Unknown where operator: #{op.inspect}"
1008
+ raise ArgumentError, "Unknown where operator: #{op.inspect}"
1011
1009
  end
1012
1010
  # issue 132
1013
1011
  if (existing = filters.find { |f| f[:range] && f[:range][field] })
@@ -1036,29 +1034,25 @@ module Searchkick
1036
1034
  {bool: {must_not: {exists: {field: field}}}}
1037
1035
  elsif value.is_a?(Regexp)
1038
1036
  source = value.source
1039
- unless source.start_with?("\\A") && source.end_with?("\\z")
1040
- # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html
1041
- Searchkick.warn("Regular expressions are always anchored in Elasticsearch")
1042
- end
1037
+
1038
+ # TODO handle other regexp options
1043
1039
 
1044
1040
  # TODO handle other anchor characters, like ^, $, \Z
1045
1041
  if source.start_with?("\\A")
1046
1042
  source = source[2..-1]
1047
1043
  else
1048
- # TODO uncomment in Searchkick 5
1049
- # source = ".*#{source}"
1044
+ source = ".*#{source}"
1050
1045
  end
1051
1046
 
1052
1047
  if source.end_with?("\\z")
1053
1048
  source = source[0..-3]
1054
1049
  else
1055
- # TODO uncomment in Searchkick 5
1056
- # source = "#{source}.*"
1050
+ source = "#{source}.*"
1057
1051
  end
1058
1052
 
1059
1053
  if below710?
1060
1054
  if value.casefold?
1061
- Searchkick.warn("Case-insensitive flag does not work with Elasticsearch < 7.10")
1055
+ raise ArgumentError, "Case-insensitive flag does not work with Elasticsearch < 7.10"
1062
1056
  end
1063
1057
  {regexp: {field => {value: source, flags: "NONE"}}}
1064
1058
  else
@@ -1069,9 +1063,7 @@ module Searchkick
1069
1063
  if value.as_json.is_a?(Enumerable)
1070
1064
  # query will fail, but this is better
1071
1065
  # same message as Active Record
1072
- # TODO make TypeError
1073
- # raise InvalidQueryError for backward compatibility
1074
- raise Searchkick::InvalidQueryError, "can't cast #{value.class.name}"
1066
+ raise TypeError, "can't cast #{value.class.name}"
1075
1067
  end
1076
1068
 
1077
1069
  {term: {field => {value: value}}}
@@ -1150,21 +1142,13 @@ module Searchkick
1150
1142
  end
1151
1143
 
1152
1144
  def track_total_hits?
1153
- (searchkick_options[:deep_paging] && !below70?) || body_options[:track_total_hits]
1145
+ searchkick_options[:deep_paging] || body_options[:track_total_hits]
1154
1146
  end
1155
1147
 
1156
1148
  def body_options
1157
1149
  options[:body_options] || {}
1158
1150
  end
1159
1151
 
1160
- def below61?
1161
- Searchkick.server_below?("6.1.0")
1162
- end
1163
-
1164
- def below70?
1165
- Searchkick.server_below?("7.0.0")
1166
- end
1167
-
1168
1152
  def below73?
1169
1153
  Searchkick.server_below?("7.3.0")
1170
1154
  end
@@ -39,7 +39,6 @@ module Searchkick
39
39
  _index: index.name,
40
40
  _id: search_id
41
41
  }
42
- data[:_type] = document_type if Searchkick.server_below7?
43
42
  data[:routing] = record.search_routing if record.respond_to?(:search_routing)
44
43
  data
45
44
  end
@@ -1,79 +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 Searchkick::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
48
  raise Searchkick::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 => e
68
- raise e unless Searchkick.not_found_error?(e)
69
- # do nothing if not found
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
99
+ end
100
+
101
+ if delete_records.any?
102
+ index.bulk_delete(delete_records)
70
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
71
110
  else
72
- if method_name
73
- index.update_record(record, method_name)
74
- else
75
- 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
76
159
  end
160
+ raise e
77
161
  end
78
162
  end
79
163
  end