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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +125 -64
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +146 -65
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +17 -67
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +1 -1
- data/lib/searchkick/model.rb +48 -49
- data/lib/searchkick/process_batch_job.rb +9 -25
- data/lib/searchkick/process_queue_job.rb +3 -2
- data/lib/searchkick/query.rb +38 -54
- data/lib/searchkick/record_data.rb +1 -1
- data/lib/searchkick/record_indexer.rb +136 -52
- data/lib/searchkick/reindex_queue.rb +26 -3
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +36 -0
- data/lib/searchkick/relation_indexer.rb +150 -0
- data/lib/searchkick/results.rb +27 -28
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick.rb +159 -84
- data/lib/tasks/searchkick.rake +6 -3
- metadata +11 -28
- data/lib/searchkick/bulk_indexer.rb +0 -173
- data/lib/searchkick/logging.rb +0 -246
@@ -5,11 +5,30 @@ module Searchkick
|
|
5
5
|
def initialize(name)
|
6
6
|
@name = name
|
7
7
|
|
8
|
-
raise
|
8
|
+
raise Error, "Searchkick.redis not set" unless Searchkick.redis
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
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(
|
13
|
-
model =
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
data/lib/searchkick/results.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
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
|
-
|
163
|
-
|
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
|
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
|
-
|
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
|
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
|
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 =
|
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
|
-
|
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
|
data/lib/searchkick/version.rb
CHANGED