searchkick 4.4.0 → 5.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- @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
- 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
- result["id"] ||= result["_id"] # needed for legacy reasons
88
- [HashWrapper.new(result), hit]
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.model_name
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 Searchkick::Error, "Query error - use the error method to view it"
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
- with_hit.map do |result, hit|
216
- [result, hit_highlights(hit, multiple: multiple)]
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 Searchkick::Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
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
- Searchkick::Results.new(@klass, Searchkick.client.scroll(params), @options)
248
- rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
249
- if e.class.to_s =~ /NotFound/ && e.message =~ /search_context_missing_exception/i
250
- raise Searchkick::Error, "Scroll id has expired"
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 Elasticsearch::Transport::Transport::Error
265
- # do nothing
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
- Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }]
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
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.4.0"
2
+ VERSION = "5.3.1"
3
3
  end
@@ -0,0 +1,11 @@
1
+ module Searchkick
2
+ class Where
3
+ def initialize(relation)
4
+ @relation = relation
5
+ end
6
+
7
+ def not(value)
8
+ @relation.where(_not: value)
9
+ end
10
+ end
11
+ end