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.
@@ -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
@@ -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
- @with_hit ||= begin
25
- if missing_records.any?
26
- Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| v[:id] }.join(", ")}")
27
- end
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 Searchkick::Error, "Query error - use the error method to view it"
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
- with_hit.map do |result, hit|
163
- [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)
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 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
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
- Searchkick::Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
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 Searchkick::Error, "Scroll id has expired"
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 Searchkick::Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
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 = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
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
- 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] }
348
349
  else
349
350
  {}
350
351
  end
@@ -0,0 +1,11 @@
1
+ module Searchkick
2
+ class Script
3
+ attr_reader :source, :lang, :params
4
+
5
+ def initialize(source, lang: "painless", params: {})
6
+ @source = source
7
+ @lang = lang
8
+ @params = params
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.6.3"
2
+ VERSION = "5.5.2"
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