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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +160 -3
- data/LICENSE.txt +1 -1
- data/README.md +567 -421
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +167 -74
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +465 -404
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +9 -2
- data/lib/searchkick/model.rb +50 -51
- data/lib/searchkick/process_batch_job.rb +9 -25
- data/lib/searchkick/process_queue_job.rb +4 -3
- data/lib/searchkick/query.rb +106 -77
- data/lib/searchkick/record_data.rb +1 -1
- data/lib/searchkick/record_indexer.rb +136 -51
- data/lib/searchkick/reindex_queue.rb +51 -9
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +247 -0
- data/lib/searchkick/relation_indexer.rb +155 -0
- data/lib/searchkick/results.rb +131 -96
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +202 -96
- data/lib/tasks/searchkick.rake +14 -10
- metadata +18 -85
- data/CONTRIBUTING.md +0 -53
- data/lib/searchkick/bulk_indexer.rb +0 -173
- data/lib/searchkick/logging.rb +0 -246
data/lib/searchkick/query.rb
CHANGED
@@ -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, :
|
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
|
-
|
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 =
|
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 =
|
190
|
+
@execute = Results.new(searchkick_klass, response, opts)
|
186
191
|
end
|
187
192
|
|
188
193
|
def retry_misspellings?(response)
|
189
|
-
@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
|
-
|
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
|
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
|
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
|
-
|
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:
|
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
|
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}
|
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 =
|
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
|
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:
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
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
|
-
|
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, "
|
979
|
+
regex.gsub!(v, "\\\\" + v)
|
968
980
|
end
|
969
981
|
regex = regex.gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
|
970
|
-
|
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
|
-
|
1031
|
-
|
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
|
-
|
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
|
-
|
1047
|
-
# source = "#{source}.*"
|
1060
|
+
source = "#{source}.*"
|
1048
1061
|
end
|
1049
1062
|
|
1050
|
-
|
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
|
-
|
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
|
-
|
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 :
|
3
|
+
attr_reader :index
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@index = record.class.searchkick_index
|
5
|
+
def initialize(index)
|
6
|
+
@index = index
|
8
7
|
end
|
9
8
|
|
10
|
-
def reindex(
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
48
|
+
raise Error, "Partial reindex not supported with queue option"
|
21
49
|
end
|
22
50
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
54
|
-
|
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
|
60
|
-
|
84
|
+
def index_record?(record)
|
85
|
+
record.persisted? && !record.destroyed? && record.should_index?
|
61
86
|
end
|
62
87
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|