searchkick 4.6.3 → 5.0.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.
@@ -5,11 +5,30 @@ module Searchkick
5
5
  def initialize(name)
6
6
  @name = name
7
7
 
8
- raise Searchkick::Error, "Searchkick.redis not set" unless Searchkick.redis
8
+ raise Error, "Searchkick.redis not set" unless Searchkick.redis
9
9
  end
10
10
 
11
- def push(record_id)
12
- Searchkick.with_redis { |r| r.lpush(redis_key, record_id) }
11
+ # supports single and multiple ids
12
+ def push(record_ids)
13
+ Searchkick.with_redis { |r| r.lpush(redis_key, record_ids) }
14
+ end
15
+
16
+ def push_records(records)
17
+ record_ids =
18
+ records.map do |record|
19
+ # always pass routing in case record is deleted
20
+ # before the queue job runs
21
+ if record.respond_to?(:search_routing)
22
+ routing = record.search_routing
23
+ end
24
+
25
+ # escape pipe with double pipe
26
+ value = escape(record.id.to_s)
27
+ value = "#{value}|#{escape(routing)}" if routing
28
+ value
29
+ end
30
+
31
+ push(record_ids)
13
32
  end
14
33
 
15
34
  # TODO use reliable queuing
@@ -48,5 +67,9 @@ module Searchkick
48
67
  def redis_version
49
68
  @redis_version ||= Searchkick.with_redis { |r| Gem::Version.new(r.info["redis_version"]) }
50
69
  end
70
+
71
+ def escape(value)
72
+ value.gsub("|", "||")
73
+ end
51
74
  end
52
75
  end
@@ -1,41 +1,17 @@
1
1
  module Searchkick
2
2
  class ReindexV2Job < ActiveJob::Base
3
- RECORD_NOT_FOUND_CLASSES = [
4
- "ActiveRecord::RecordNotFound",
5
- "Mongoid::Errors::DocumentNotFound",
6
- "NoBrainer::Error::DocumentNotFound",
7
- "Cequel::Record::RecordNotFound"
8
- ]
9
-
10
3
  queue_as { Searchkick.queue_name }
11
4
 
12
- def perform(klass, id, method_name = nil, routing: nil)
13
- model = klass.constantize
14
- record =
15
- begin
16
- if model.respond_to?(:unscoped)
17
- model.unscoped.find(id)
18
- else
19
- model.find(id)
20
- end
21
- rescue => e
22
- # check by name rather than rescue directly so we don't need
23
- # to determine which classes are defined
24
- raise e unless RECORD_NOT_FOUND_CLASSES.include?(e.class.name)
25
- nil
26
- end
27
-
28
- unless record
29
- record = model.new
30
- record.id = id
31
- if routing
32
- record.define_singleton_method(:search_routing) do
33
- routing
34
- end
35
- end
36
- end
37
-
38
- RecordIndexer.new(record).reindex(method_name, mode: :inline)
5
+ def perform(class_name, id, method_name = nil, routing: nil, index_name: nil)
6
+ model = Searchkick.load_model(class_name, allow_child: true)
7
+ index = model.searchkick_index(name: index_name)
8
+ # use should_index? to decide whether to index (not default scope)
9
+ # just like saving inline
10
+ # could use Searchkick.scope() in future
11
+ # but keep for now for backwards compatibility
12
+ model = model.unscoped if model.respond_to?(:unscoped)
13
+ items = [{id: id, routing: routing}]
14
+ RecordIndexer.new(index).reindex_items(model, items, method_name: method_name, single: true)
39
15
  end
40
16
  end
41
17
  end
@@ -0,0 +1,36 @@
1
+ module Searchkick
2
+ class Relation
3
+ # note: modifying body directly is not supported
4
+ # and has no impact on query after being executed
5
+ # TODO freeze body object?
6
+ delegate :body, :params, to: :@query
7
+ delegate_missing_to :private_execute
8
+
9
+ def initialize(model, term = "*", **options)
10
+ @query = Query.new(model, term, **options)
11
+ end
12
+
13
+ # same as Active Record
14
+ def inspect
15
+ entries = results.first(11).map!(&:inspect)
16
+ entries[10] = "..." if entries.size == 11
17
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
18
+ end
19
+
20
+ def execute
21
+ Searchkick.warn("The execute method is no longer needed")
22
+ private_execute
23
+ self
24
+ end
25
+
26
+ private
27
+
28
+ def private_execute
29
+ @execute ||= @query.execute
30
+ end
31
+
32
+ def query
33
+ @query
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,150 @@
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
18
+ if mode == :async
19
+ if relation.respond_to?(:primary_key)
20
+ relation = relation.select(relation.primary_key).except(:includes, :preload)
21
+ elsif relation.respond_to?(:only)
22
+ relation = relation.only(:_id)
23
+ end
24
+ end
25
+
26
+ if mode == :async && full
27
+ return full_reindex_async(relation)
28
+ end
29
+
30
+ relation = resume_relation(relation) if resume
31
+
32
+ reindex_options = {
33
+ mode: mode,
34
+ method_name: method_name,
35
+ full: full
36
+ }
37
+ record_indexer = RecordIndexer.new(index)
38
+
39
+ in_batches(relation) do |items|
40
+ record_indexer.reindex(items, **reindex_options)
41
+ end
42
+ end
43
+
44
+ def batches_left
45
+ Searchkick.with_redis { |r| r.scard(batches_key) }
46
+ end
47
+
48
+ def batch_completed(batch_id)
49
+ Searchkick.with_redis { |r| r.srem(batches_key, batch_id) }
50
+ end
51
+
52
+ private
53
+
54
+ def resume_relation(relation)
55
+ if relation.respond_to?(:primary_key)
56
+ # use total docs instead of max id since there's not a great way
57
+ # to get the max _id without scripting since it's a string
58
+ where = relation.arel_table[relation.primary_key].gt(index.total_docs)
59
+ relation = relation.where(where)
60
+ else
61
+ raise Error, "Resume not supported for Mongoid"
62
+ end
63
+ end
64
+
65
+ def in_batches(relation)
66
+ if relation.respond_to?(:find_in_batches)
67
+ klass = relation.klass
68
+ # remove order to prevent possible warnings
69
+ relation.except(:order).find_in_batches(batch_size: batch_size) do |batch|
70
+ # prevent scope from affecting search_data as well as inline jobs
71
+ # Active Record runs relation calls in scoping block
72
+ # https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation/delegation.rb
73
+ # note: we could probably just call klass.current_scope = nil
74
+ # anywhere in reindex method (after initial all call),
75
+ # but this is more cautious
76
+ previous_scope = klass.current_scope(true)
77
+ if previous_scope
78
+ begin
79
+ klass.current_scope = nil
80
+ yield batch
81
+ ensure
82
+ klass.current_scope = previous_scope
83
+ end
84
+ else
85
+ yield batch
86
+ end
87
+ end
88
+ else
89
+ klass = relation.klass
90
+ each_batch(relation, batch_size: batch_size) do |batch|
91
+ # prevent scope from affecting search_data as well as inline jobs
92
+ # note: Model.with_scope doesn't always restore scope, so use custom logic
93
+ previous_scope = Mongoid::Threaded.current_scope(klass)
94
+ if previous_scope
95
+ begin
96
+ Mongoid::Threaded.set_current_scope(nil, klass)
97
+ yield batch
98
+ ensure
99
+ Mongoid::Threaded.set_current_scope(previous_scope, klass)
100
+ end
101
+ else
102
+ yield batch
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def each_batch(relation, batch_size:)
109
+ # https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
110
+ # use cursor for Mongoid
111
+ items = []
112
+ relation.all.each do |item|
113
+ items << item
114
+ if items.length == batch_size
115
+ yield items
116
+ items = []
117
+ end
118
+ end
119
+ yield items if items.any?
120
+ end
121
+
122
+ def batch_size
123
+ @batch_size ||= index.options[:batch_size] || 1000
124
+ end
125
+
126
+ def full_reindex_async(relation)
127
+ batch_id = 1
128
+ class_name = relation.searchkick_options[:class_name]
129
+
130
+ in_batches(relation) do |items|
131
+ batch_job(class_name, batch_id, items.map(&:id))
132
+ batch_id += 1
133
+ end
134
+ end
135
+
136
+ def batch_job(class_name, batch_id, record_ids)
137
+ Searchkick.with_redis { |r| r.sadd(batches_key, batch_id) }
138
+ Searchkick::BulkReindexJob.perform_later(
139
+ class_name: class_name,
140
+ index_name: index.name,
141
+ batch_id: batch_id,
142
+ record_ids: record_ids.map { |v| v.instance_of?(Integer) ? v : v.to_s }
143
+ )
144
+ end
145
+
146
+ def batches_key
147
+ "searchkick:reindex:#{index.name}:batches"
148
+ end
149
+ end
150
+ end
@@ -1,5 +1,3 @@
1
- require "forwardable"
2
-
3
1
  module Searchkick
4
2
  class Results
5
3
  include Enumerable
@@ -19,13 +17,11 @@ module Searchkick
19
17
  @results ||= with_hit.map(&:first)
20
18
  end
21
19
 
22
- # TODO return enumerator like with_score
23
20
  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]
21
+ return enum_for(:with_hit) unless block_given?
22
+
23
+ build_hits.each do |result|
24
+ yield result
29
25
  end
30
26
  end
31
27
 
@@ -145,7 +141,7 @@ module Searchkick
145
141
 
146
142
  def hits
147
143
  if error
148
- raise Searchkick::Error, "Query error - use the error method to view it"
144
+ raise Error, "Query error - use the error method to view it"
149
145
  else
150
146
  @response["hits"]["hits"]
151
147
  end
@@ -157,10 +153,11 @@ module Searchkick
157
153
  end
158
154
  end
159
155
 
160
- # TODO return enumerator like with_score
161
156
  def with_highlights(multiple: false)
162
- with_hit.map do |result, hit|
163
- [result, hit_highlights(hit, multiple: multiple)]
157
+ return enum_for(:with_highlights, multiple: multiple) unless block_given?
158
+
159
+ with_hit.each do |result, hit|
160
+ yield result, hit_highlights(hit, multiple: multiple)
164
161
  end
165
162
  end
166
163
 
@@ -181,7 +178,7 @@ module Searchkick
181
178
  end
182
179
 
183
180
  def scroll
184
- raise Searchkick::Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
181
+ raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
185
182
 
186
183
  if block_given?
187
184
  records = self
@@ -194,10 +191,10 @@ module Searchkick
194
191
  else
195
192
  begin
196
193
  # 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)
194
+ Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
198
195
  rescue => e
199
196
  if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
200
- raise Searchkick::Error, "Scroll id has expired"
197
+ raise Error, "Scroll id has expired"
201
198
  else
202
199
  raise e
203
200
  end
@@ -235,7 +232,7 @@ module Searchkick
235
232
  index_alias = index.split("_")[0..-2].join("_")
236
233
  Array((options[:index_mapping] || {})[index_alias])
237
234
  end
238
- raise Searchkick::Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
235
+ raise Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
239
236
  index_models[index] = models
240
237
  end
241
238
 
@@ -287,7 +284,7 @@ module Searchkick
287
284
  end
288
285
 
289
286
  if hit["highlight"] || options[:highlight]
290
- highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
287
+ highlight = hit["highlight"].to_a.to_h { |k, v| [base_field(k), v.first] }
291
288
  options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
292
289
  result["highlighted_#{k}"] ||= (highlight[k] || result[k])
293
290
  end
@@ -302,23 +299,25 @@ module Searchkick
302
299
  end
303
300
  end
304
301
 
302
+ def build_hits
303
+ @build_hits ||= begin
304
+ if missing_records.any?
305
+ Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| v[:id] }.join(", ")}")
306
+ end
307
+ with_hit_and_missing_records[0]
308
+ end
309
+ end
310
+
305
311
  def results_query(records, hits)
312
+ records = Searchkick.scope(records)
313
+
306
314
  ids = hits.map { |hit| hit["_id"] }
307
315
  if options[:includes] || options[:model_includes]
308
316
  included_relations = []
309
317
  combine_includes(included_relations, options[:includes])
310
318
  combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
311
319
 
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
320
+ records = records.includes(included_relations)
322
321
  end
323
322
 
324
323
  if options[:scope_results]
@@ -344,7 +343,7 @@ module Searchkick
344
343
 
345
344
  def hit_highlights(hit, multiple: false)
346
345
  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] }]
346
+ hit["highlight"].to_h { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }
348
347
  else
349
348
  {}
350
349
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.6.3"
2
+ VERSION = "5.0.1"
3
3
  end