searchkick 4.6.3 → 5.5.2
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 +142 -2
- data/LICENSE.txt +1 -1
- data/README.md +424 -228
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +152 -67
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +102 -68
- 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 +3 -2
- data/lib/searchkick/query.rb +212 -69
- data/lib/searchkick/railtie.rb +1 -1
- data/lib/searchkick/record_data.rb +1 -1
- data/lib/searchkick/record_indexer.rb +136 -52
- data/lib/searchkick/reindex_queue.rb +36 -8
- 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/reranking.rb +28 -0
- data/lib/searchkick/results.rb +29 -28
- data/lib/searchkick/script.rb +11 -0
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +197 -95
- data/lib/tasks/searchkick.rake +6 -3
- metadata +14 -32
- data/lib/searchkick/bulk_indexer.rb +0 -173
- data/lib/searchkick/logging.rb +0 -246
@@ -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
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Searchkick
|
2
|
+
module Reranking
|
3
|
+
def self.rrf(first_ranking, *rankings, k: 60)
|
4
|
+
rankings.unshift(first_ranking)
|
5
|
+
rankings.map!(&:to_ary)
|
6
|
+
|
7
|
+
ranks = []
|
8
|
+
results = []
|
9
|
+
rankings.each do |ranking|
|
10
|
+
ranks << ranking.map.with_index.to_h { |v, i| [v, i + 1] }
|
11
|
+
results.concat(ranking)
|
12
|
+
end
|
13
|
+
|
14
|
+
results =
|
15
|
+
results.uniq.map do |result|
|
16
|
+
score =
|
17
|
+
ranks.sum do |rank|
|
18
|
+
r = rank[result]
|
19
|
+
r ? 1.0 / (k + r) : 0.0
|
20
|
+
end
|
21
|
+
|
22
|
+
{result: result, score: score}
|
23
|
+
end
|
24
|
+
|
25
|
+
results.sort_by { |v| -v[:score] }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/searchkick/results.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
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
|
-
|
163
|
-
|
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
|
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
|
-
|
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
|
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
|
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 =
|
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| "#{Array(v[:model]).map(&:model_name).sort.join("/")} #{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
|
-
|
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
|
data/lib/searchkick/version.rb
CHANGED