searchkick 4.6.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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