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/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,82 +14,23 @@ 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
22
|
def with_hit
|
23
|
-
|
24
|
-
if options[:load]
|
25
|
-
# results can have different types
|
26
|
-
results = {}
|
27
|
-
|
28
|
-
hits.group_by { |hit, _| hit["_index"] }.each do |index, grouped_hits|
|
29
|
-
klasses =
|
30
|
-
if @klass
|
31
|
-
[@klass]
|
32
|
-
else
|
33
|
-
index_alias = index.split("_")[0..-2].join("_")
|
34
|
-
Array((options[:index_mapping] || {})[index_alias])
|
35
|
-
end
|
36
|
-
raise Searchkick::Error, "Unknown model for index: #{index}" unless klasses.any?
|
37
|
-
|
38
|
-
results[index] = {}
|
39
|
-
klasses.each do |klass|
|
40
|
-
results[index].merge!(results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s })
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
missing_ids = []
|
45
|
-
|
46
|
-
# sort
|
47
|
-
results =
|
48
|
-
hits.map do |hit|
|
49
|
-
result = results[hit["_index"]][hit["_id"].to_s]
|
50
|
-
if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
|
51
|
-
if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
|
52
|
-
highlights = hit_highlights(hit)
|
53
|
-
result.define_singleton_method(:search_highlights) do
|
54
|
-
highlights
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
[result, hit]
|
59
|
-
end.select do |result, hit|
|
60
|
-
missing_ids << hit["_id"] unless result
|
61
|
-
result
|
62
|
-
end
|
63
|
-
|
64
|
-
if missing_ids.any?
|
65
|
-
Searchkick.warn("Records in search index do not exist in database: #{missing_ids.join(", ")}")
|
66
|
-
end
|
67
|
-
|
68
|
-
results
|
69
|
-
else
|
70
|
-
hits.map do |hit|
|
71
|
-
result =
|
72
|
-
if hit["_source"]
|
73
|
-
hit.except("_source").merge(hit["_source"])
|
74
|
-
elsif hit["fields"]
|
75
|
-
hit.except("fields").merge(hit["fields"])
|
76
|
-
else
|
77
|
-
hit
|
78
|
-
end
|
79
|
-
|
80
|
-
if hit["highlight"] || options[:highlight]
|
81
|
-
highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
|
82
|
-
options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
|
83
|
-
result["highlighted_#{k}"] ||= (highlight[k] || result[k])
|
84
|
-
end
|
85
|
-
end
|
23
|
+
return enum_for(:with_hit) unless block_given?
|
86
24
|
|
87
|
-
|
88
|
-
|
89
|
-
end
|
90
|
-
end
|
25
|
+
build_hits.each do |result|
|
26
|
+
yield result
|
91
27
|
end
|
92
28
|
end
|
93
29
|
|
30
|
+
def missing_records
|
31
|
+
@missing_records ||= with_hit_and_missing_records[1]
|
32
|
+
end
|
33
|
+
|
94
34
|
def suggestions
|
95
35
|
if response["suggest"]
|
96
36
|
response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
|
@@ -129,7 +69,11 @@ module Searchkick
|
|
129
69
|
end
|
130
70
|
|
131
71
|
def model_name
|
132
|
-
klass.
|
72
|
+
if klass.nil?
|
73
|
+
ActiveModel::Name.new(self.class, nil, 'Result')
|
74
|
+
else
|
75
|
+
klass.model_name
|
76
|
+
end
|
133
77
|
end
|
134
78
|
|
135
79
|
def entry_name(options = {})
|
@@ -199,7 +143,7 @@ module Searchkick
|
|
199
143
|
|
200
144
|
def hits
|
201
145
|
if error
|
202
|
-
raise
|
146
|
+
raise Error, "Query error - use the error method to view it"
|
203
147
|
else
|
204
148
|
@response["hits"]["hits"]
|
205
149
|
end
|
@@ -212,8 +156,18 @@ module Searchkick
|
|
212
156
|
end
|
213
157
|
|
214
158
|
def with_highlights(multiple: false)
|
215
|
-
|
216
|
-
|
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)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def with_score
|
167
|
+
return enum_for(:with_score) unless block_given?
|
168
|
+
|
169
|
+
with_hit.each do |result, hit|
|
170
|
+
yield result, hit["_score"]
|
217
171
|
end
|
218
172
|
end
|
219
173
|
|
@@ -226,7 +180,7 @@ module Searchkick
|
|
226
180
|
end
|
227
181
|
|
228
182
|
def scroll
|
229
|
-
raise
|
183
|
+
raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
|
230
184
|
|
231
185
|
if block_given?
|
232
186
|
records = self
|
@@ -237,17 +191,12 @@ module Searchkick
|
|
237
191
|
|
238
192
|
records.clear_scroll
|
239
193
|
else
|
240
|
-
params = {
|
241
|
-
scroll: options[:scroll],
|
242
|
-
scroll_id: scroll_id
|
243
|
-
}
|
244
|
-
|
245
194
|
begin
|
246
195
|
# TODO Active Support notifications for this scroll call
|
247
|
-
|
248
|
-
rescue
|
249
|
-
if e
|
250
|
-
raise
|
196
|
+
Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
|
197
|
+
rescue => e
|
198
|
+
if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
|
199
|
+
raise Error, "Scroll id has expired"
|
251
200
|
else
|
252
201
|
raise e
|
253
202
|
end
|
@@ -261,30 +210,116 @@ module Searchkick
|
|
261
210
|
# not required as scroll will expire
|
262
211
|
# but there is a cost to open scrolls
|
263
212
|
Searchkick.client.clear_scroll(scroll_id: scroll_id)
|
264
|
-
rescue
|
265
|
-
|
213
|
+
rescue => e
|
214
|
+
raise e unless Searchkick.transport_error?(e)
|
266
215
|
end
|
267
216
|
end
|
268
217
|
|
269
218
|
private
|
270
219
|
|
220
|
+
def with_hit_and_missing_records
|
221
|
+
@with_hit_and_missing_records ||= begin
|
222
|
+
missing_records = []
|
223
|
+
|
224
|
+
if options[:load]
|
225
|
+
grouped_hits = hits.group_by { |hit, _| hit["_index"] }
|
226
|
+
|
227
|
+
# determine models
|
228
|
+
index_models = {}
|
229
|
+
grouped_hits.each do |index, _|
|
230
|
+
models =
|
231
|
+
if @klass
|
232
|
+
[@klass]
|
233
|
+
else
|
234
|
+
index_alias = index.split("_")[0..-2].join("_")
|
235
|
+
Array((options[:index_mapping] || {})[index_alias])
|
236
|
+
end
|
237
|
+
raise Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
|
238
|
+
index_models[index] = models
|
239
|
+
end
|
240
|
+
|
241
|
+
# fetch results
|
242
|
+
results = {}
|
243
|
+
grouped_hits.each do |index, index_hits|
|
244
|
+
results[index] = {}
|
245
|
+
index_models[index].each do |model|
|
246
|
+
results[index].merge!(results_query(model, index_hits).to_a.index_by { |r| r.id.to_s })
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# sort
|
251
|
+
results =
|
252
|
+
hits.map do |hit|
|
253
|
+
result = results[hit["_index"]][hit["_id"].to_s]
|
254
|
+
if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
|
255
|
+
if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
|
256
|
+
highlights = hit_highlights(hit)
|
257
|
+
result.define_singleton_method(:search_highlights) do
|
258
|
+
highlights
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
[result, hit]
|
263
|
+
end.select do |result, hit|
|
264
|
+
unless result
|
265
|
+
models = index_models[hit["_index"]]
|
266
|
+
missing_records << {
|
267
|
+
id: hit["_id"],
|
268
|
+
# may be multiple models for inheritance with child models
|
269
|
+
# not ideal to return different types
|
270
|
+
# but this situation shouldn't be common
|
271
|
+
model: models.size == 1 ? models.first : models
|
272
|
+
}
|
273
|
+
end
|
274
|
+
result
|
275
|
+
end
|
276
|
+
else
|
277
|
+
results =
|
278
|
+
hits.map do |hit|
|
279
|
+
result =
|
280
|
+
if hit["_source"]
|
281
|
+
hit.except("_source").merge(hit["_source"])
|
282
|
+
elsif hit["fields"]
|
283
|
+
hit.except("fields").merge(hit["fields"])
|
284
|
+
else
|
285
|
+
hit
|
286
|
+
end
|
287
|
+
|
288
|
+
if hit["highlight"] || options[:highlight]
|
289
|
+
highlight = hit["highlight"].to_a.to_h { |k, v| [base_field(k), v.first] }
|
290
|
+
options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
|
291
|
+
result["highlighted_#{k}"] ||= (highlight[k] || result[k])
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
result["id"] ||= result["_id"] # needed for legacy reasons
|
296
|
+
[HashWrapper.new(result), hit]
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
[results, missing_records]
|
301
|
+
end
|
302
|
+
end
|
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
|
+
|
271
313
|
def results_query(records, hits)
|
314
|
+
records = Searchkick.scope(records)
|
315
|
+
|
272
316
|
ids = hits.map { |hit| hit["_id"] }
|
273
317
|
if options[:includes] || options[:model_includes]
|
274
318
|
included_relations = []
|
275
319
|
combine_includes(included_relations, options[:includes])
|
276
320
|
combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
|
277
321
|
|
278
|
-
records =
|
279
|
-
if defined?(NoBrainer::Document) && records < NoBrainer::Document
|
280
|
-
if Gem.loaded_specs["nobrainer"].version >= Gem::Version.new("0.21")
|
281
|
-
records.eager_load(included_relations)
|
282
|
-
else
|
283
|
-
records.preload(included_relations)
|
284
|
-
end
|
285
|
-
else
|
286
|
-
records.includes(included_relations)
|
287
|
-
end
|
322
|
+
records = records.includes(included_relations)
|
288
323
|
end
|
289
324
|
|
290
325
|
if options[:scope_results]
|
@@ -310,7 +345,7 @@ module Searchkick
|
|
310
345
|
|
311
346
|
def hit_highlights(hit, multiple: false)
|
312
347
|
if hit["highlight"]
|
313
|
-
|
348
|
+
hit["highlight"].to_h { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }
|
314
349
|
else
|
315
350
|
{}
|
316
351
|
end
|
data/lib/searchkick/version.rb
CHANGED