searchkick 4.6.3 → 5.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,155 @@
1
+ module Searchkick
2
+ class RelationIndexer
3
+ attr_reader :index
4
+
5
+ def initialize(index)
6
+ @index = index
7
+ end
8
+
9
+ def reindex(relation, mode:, method_name: nil, full: false, resume: false, scope: nil)
10
+ # apply scopes
11
+ if scope
12
+ relation = relation.send(scope)
13
+ elsif relation.respond_to?(:search_import)
14
+ relation = relation.search_import
15
+ end
16
+
17
+ # remove unneeded loading for async and queue
18
+ if mode == :async || mode == :queue
19
+ if relation.respond_to?(:primary_key)
20
+ relation = relation.except(:includes, :preload)
21
+ unless mode == :queue && relation.klass.method_defined?(:search_routing)
22
+ relation = relation.except(:select).select(relation.primary_key)
23
+ end
24
+ elsif relation.respond_to?(:only)
25
+ unless mode == :queue && relation.klass.method_defined?(:search_routing)
26
+ relation = relation.only(:_id)
27
+ end
28
+ end
29
+ end
30
+
31
+ if mode == :async && full
32
+ return full_reindex_async(relation)
33
+ end
34
+
35
+ relation = resume_relation(relation) if resume
36
+
37
+ reindex_options = {
38
+ mode: mode,
39
+ method_name: method_name,
40
+ full: full
41
+ }
42
+ record_indexer = RecordIndexer.new(index)
43
+
44
+ in_batches(relation) do |items|
45
+ record_indexer.reindex(items, **reindex_options)
46
+ end
47
+ end
48
+
49
+ def batches_left
50
+ Searchkick.with_redis { |r| r.call("SCARD", batches_key) }
51
+ end
52
+
53
+ def batch_completed(batch_id)
54
+ Searchkick.with_redis { |r| r.call("SREM", batches_key, [batch_id]) }
55
+ end
56
+
57
+ private
58
+
59
+ def resume_relation(relation)
60
+ if relation.respond_to?(:primary_key)
61
+ # use total docs instead of max id since there's not a great way
62
+ # to get the max _id without scripting since it's a string
63
+ where = relation.arel_table[relation.primary_key].gt(index.total_docs)
64
+ relation = relation.where(where)
65
+ else
66
+ raise Error, "Resume not supported for Mongoid"
67
+ end
68
+ end
69
+
70
+ def in_batches(relation)
71
+ if relation.respond_to?(:find_in_batches)
72
+ klass = relation.klass
73
+ # remove order to prevent possible warnings
74
+ relation.except(:order).find_in_batches(batch_size: batch_size) do |batch|
75
+ # prevent scope from affecting search_data as well as inline jobs
76
+ # Active Record runs relation calls in scoping block
77
+ # https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation/delegation.rb
78
+ # note: we could probably just call klass.current_scope = nil
79
+ # anywhere in reindex method (after initial all call),
80
+ # but this is more cautious
81
+ previous_scope = klass.current_scope(true)
82
+ if previous_scope
83
+ begin
84
+ klass.current_scope = nil
85
+ yield batch
86
+ ensure
87
+ klass.current_scope = previous_scope
88
+ end
89
+ else
90
+ yield batch
91
+ end
92
+ end
93
+ else
94
+ klass = relation.klass
95
+ each_batch(relation, batch_size: batch_size) do |batch|
96
+ # prevent scope from affecting search_data as well as inline jobs
97
+ # note: Model.with_scope doesn't always restore scope, so use custom logic
98
+ previous_scope = Mongoid::Threaded.current_scope(klass)
99
+ if previous_scope
100
+ begin
101
+ Mongoid::Threaded.set_current_scope(nil, klass)
102
+ yield batch
103
+ ensure
104
+ Mongoid::Threaded.set_current_scope(previous_scope, klass)
105
+ end
106
+ else
107
+ yield batch
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def each_batch(relation, batch_size:)
114
+ # https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
115
+ # use cursor for Mongoid
116
+ items = []
117
+ relation.all.each do |item|
118
+ items << item
119
+ if items.length == batch_size
120
+ yield items
121
+ items = []
122
+ end
123
+ end
124
+ yield items if items.any?
125
+ end
126
+
127
+ def batch_size
128
+ @batch_size ||= index.options[:batch_size] || 1000
129
+ end
130
+
131
+ def full_reindex_async(relation)
132
+ batch_id = 1
133
+ class_name = relation.searchkick_options[:class_name]
134
+
135
+ in_batches(relation) do |items|
136
+ batch_job(class_name, batch_id, items.map(&:id))
137
+ batch_id += 1
138
+ end
139
+ end
140
+
141
+ def batch_job(class_name, batch_id, record_ids)
142
+ Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
143
+ Searchkick::BulkReindexJob.perform_later(
144
+ class_name: class_name,
145
+ index_name: index.name,
146
+ batch_id: batch_id,
147
+ record_ids: record_ids.map { |v| v.instance_of?(Integer) ? v : v.to_s }
148
+ )
149
+ end
150
+
151
+ def batches_key
152
+ "searchkick:reindex:#{index.name}:batches"
153
+ end
154
+ end
155
+ end
@@ -1,10 +1,9 @@
1
- require "forwardable"
2
-
3
1
  module Searchkick
4
2
  class Results
5
3
  include Enumerable
6
4
  extend Forwardable
7
5
 
6
+ # TODO remove klass and options in 6.0
8
7
  attr_reader :klass, :response, :options
9
8
 
10
9
  def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
@@ -15,17 +14,16 @@ module Searchkick
15
14
  @options = options
16
15
  end
17
16
 
17
+ # TODO make private in 6.0
18
18
  def results
19
19
  @results ||= with_hit.map(&:first)
20
20
  end
21
21
 
22
- # TODO return enumerator like with_score
23
22
  def with_hit
24
- @with_hit ||= begin
25
- if missing_records.any?
26
- Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| v[:id] }.join(", ")}")
27
- end
28
- with_hit_and_missing_records[0]
23
+ return enum_for(:with_hit) unless block_given?
24
+
25
+ build_hits.each do |result|
26
+ yield result
29
27
  end
30
28
  end
31
29
 
@@ -145,7 +143,7 @@ module Searchkick
145
143
 
146
144
  def hits
147
145
  if error
148
- raise Searchkick::Error, "Query error - use the error method to view it"
146
+ raise Error, "Query error - use the error method to view it"
149
147
  else
150
148
  @response["hits"]["hits"]
151
149
  end
@@ -157,10 +155,11 @@ module Searchkick
157
155
  end
158
156
  end
159
157
 
160
- # TODO return enumerator like with_score
161
158
  def with_highlights(multiple: false)
162
- with_hit.map do |result, hit|
163
- [result, hit_highlights(hit, multiple: multiple)]
159
+ return enum_for(:with_highlights, multiple: multiple) unless block_given?
160
+
161
+ with_hit.each do |result, hit|
162
+ yield result, hit_highlights(hit, multiple: multiple)
164
163
  end
165
164
  end
166
165
 
@@ -181,7 +180,7 @@ module Searchkick
181
180
  end
182
181
 
183
182
  def scroll
184
- raise Searchkick::Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
183
+ raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
185
184
 
186
185
  if block_given?
187
186
  records = self
@@ -194,10 +193,10 @@ module Searchkick
194
193
  else
195
194
  begin
196
195
  # TODO Active Support notifications for this scroll call
197
- Searchkick::Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
196
+ Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
198
197
  rescue => e
199
198
  if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
200
- raise Searchkick::Error, "Scroll id has expired"
199
+ raise Error, "Scroll id has expired"
201
200
  else
202
201
  raise e
203
202
  end
@@ -235,7 +234,7 @@ module Searchkick
235
234
  index_alias = index.split("_")[0..-2].join("_")
236
235
  Array((options[:index_mapping] || {})[index_alias])
237
236
  end
238
- raise Searchkick::Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
237
+ raise Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
239
238
  index_models[index] = models
240
239
  end
241
240
 
@@ -287,7 +286,7 @@ module Searchkick
287
286
  end
288
287
 
289
288
  if hit["highlight"] || options[:highlight]
290
- highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
289
+ highlight = hit["highlight"].to_a.to_h { |k, v| [base_field(k), v.first] }
291
290
  options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
292
291
  result["highlighted_#{k}"] ||= (highlight[k] || result[k])
293
292
  end
@@ -302,23 +301,25 @@ module Searchkick
302
301
  end
303
302
  end
304
303
 
304
+ def build_hits
305
+ @build_hits ||= begin
306
+ if missing_records.any?
307
+ Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| "#{v[:model].model_name} #{v[:id]}" }.join(", ")}")
308
+ end
309
+ with_hit_and_missing_records[0]
310
+ end
311
+ end
312
+
305
313
  def results_query(records, hits)
314
+ records = Searchkick.scope(records)
315
+
306
316
  ids = hits.map { |hit| hit["_id"] }
307
317
  if options[:includes] || options[:model_includes]
308
318
  included_relations = []
309
319
  combine_includes(included_relations, options[:includes])
310
320
  combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
311
321
 
312
- records =
313
- if defined?(NoBrainer::Document) && records < NoBrainer::Document
314
- if Gem.loaded_specs["nobrainer"].version >= Gem::Version.new("0.21")
315
- records.eager_load(included_relations)
316
- else
317
- records.preload(included_relations)
318
- end
319
- else
320
- records.includes(included_relations)
321
- end
322
+ records = records.includes(included_relations)
322
323
  end
323
324
 
324
325
  if options[:scope_results]
@@ -344,7 +345,7 @@ module Searchkick
344
345
 
345
346
  def hit_highlights(hit, multiple: false)
346
347
  if hit["highlight"]
347
- Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }]
348
+ hit["highlight"].to_h { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }
348
349
  else
349
350
  {}
350
351
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.6.3"
2
+ VERSION = "5.2.2"
3
3
  end
@@ -0,0 +1,11 @@
1
+ module Searchkick
2
+ class Where
3
+ def initialize(relation)
4
+ @relation = relation
5
+ end
6
+
7
+ def not(value)
8
+ @relation.where(_not: value)
9
+ end
10
+ end
11
+ end