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