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.
@@ -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
- @with_hit ||= begin
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
- results
65
- else
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.model_name
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
- with_hit.map do |result, hit|
212
- [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)
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
- 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] }
265
347
  else
266
348
  {}
267
349
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.0.0"
2
+ VERSION = "5.0.0"
3
3
  end