tiny-quick-gem 0.0.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/searchkick-6.1.2/CHANGELOG.md +919 -0
  3. data/searchkick-6.1.2/LICENSE.txt +22 -0
  4. data/searchkick-6.1.2/README.md +2348 -0
  5. data/searchkick-6.1.2/lib/searchkick/bulk_reindex_job.rb +19 -0
  6. data/searchkick-6.1.2/lib/searchkick/controller_runtime.rb +40 -0
  7. data/searchkick-6.1.2/lib/searchkick/hash_wrapper.rb +41 -0
  8. data/searchkick-6.1.2/lib/searchkick/index.rb +480 -0
  9. data/searchkick-6.1.2/lib/searchkick/index_cache.rb +30 -0
  10. data/searchkick-6.1.2/lib/searchkick/index_options.rb +652 -0
  11. data/searchkick-6.1.2/lib/searchkick/indexer.rb +43 -0
  12. data/searchkick-6.1.2/lib/searchkick/log_subscriber.rb +57 -0
  13. data/searchkick-6.1.2/lib/searchkick/middleware.rb +19 -0
  14. data/searchkick-6.1.2/lib/searchkick/model.rb +120 -0
  15. data/searchkick-6.1.2/lib/searchkick/multi_search.rb +46 -0
  16. data/searchkick-6.1.2/lib/searchkick/process_batch_job.rb +20 -0
  17. data/searchkick-6.1.2/lib/searchkick/process_queue_job.rb +33 -0
  18. data/searchkick-6.1.2/lib/searchkick/query.rb +1372 -0
  19. data/searchkick-6.1.2/lib/searchkick/railtie.rb +7 -0
  20. data/searchkick-6.1.2/lib/searchkick/record_data.rb +147 -0
  21. data/searchkick-6.1.2/lib/searchkick/record_indexer.rb +174 -0
  22. data/searchkick-6.1.2/lib/searchkick/reindex_queue.rb +57 -0
  23. data/searchkick-6.1.2/lib/searchkick/reindex_v2_job.rb +17 -0
  24. data/searchkick-6.1.2/lib/searchkick/relation.rb +728 -0
  25. data/searchkick-6.1.2/lib/searchkick/relation_indexer.rb +184 -0
  26. data/searchkick-6.1.2/lib/searchkick/reranking.rb +28 -0
  27. data/searchkick-6.1.2/lib/searchkick/results.rb +359 -0
  28. data/searchkick-6.1.2/lib/searchkick/script.rb +11 -0
  29. data/searchkick-6.1.2/lib/searchkick/version.rb +3 -0
  30. data/searchkick-6.1.2/lib/searchkick/where.rb +11 -0
  31. data/searchkick-6.1.2/lib/searchkick.rb +388 -0
  32. data/searchkick-6.1.2/lib/tasks/searchkick.rake +37 -0
  33. data/tiny-quick-gem.gemspec +12 -0
  34. metadata +73 -0
@@ -0,0 +1,184 @@
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, ignore_missing: nil, full: false, resume: false, scope: nil, job_options: 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, job_options: job_options)
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
+ ignore_missing: ignore_missing,
42
+ job_options: job_options
43
+ }
44
+ record_indexer = RecordIndexer.new(index)
45
+
46
+ in_batches(relation) do |items|
47
+ record_indexer.reindex(items, **reindex_options)
48
+ end
49
+ end
50
+
51
+ def batches_left
52
+ Searchkick.with_redis { |r| r.call("SCARD", batches_key) }
53
+ end
54
+
55
+ def batch_completed(batch_id)
56
+ Searchkick.with_redis { |r| r.call("SREM", batches_key, [batch_id]) }
57
+ end
58
+
59
+ private
60
+
61
+ def resume_relation(relation)
62
+ if relation.respond_to?(:primary_key)
63
+ # use total docs instead of max id since there's not a great way
64
+ # to get the max _id without scripting since it's a string
65
+ where = relation.arel_table[relation.primary_key].gt(index.total_docs)
66
+ relation = relation.where(where)
67
+ else
68
+ raise Error, "Resume not supported for Mongoid"
69
+ end
70
+ end
71
+
72
+ def in_batches(relation)
73
+ if relation.respond_to?(:find_in_batches)
74
+ klass = relation.klass
75
+ # remove order to prevent possible warnings
76
+ relation.except(:order).find_in_batches(batch_size: batch_size) do |batch|
77
+ # prevent scope from affecting search_data as well as inline jobs
78
+ # Active Record runs relation calls in scoping block
79
+ # https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation/delegation.rb
80
+ # note: we could probably just call klass.current_scope = nil
81
+ # anywhere in reindex method (after initial all call),
82
+ # but this is more cautious
83
+ previous_scope = klass.current_scope(true)
84
+ if previous_scope
85
+ begin
86
+ klass.current_scope = nil
87
+ yield batch
88
+ ensure
89
+ klass.current_scope = previous_scope
90
+ end
91
+ else
92
+ yield batch
93
+ end
94
+ end
95
+ else
96
+ klass = relation.klass
97
+ each_batch(relation, batch_size: batch_size) do |batch|
98
+ # prevent scope from affecting search_data as well as inline jobs
99
+ # note: Model.with_scope doesn't always restore scope, so use custom logic
100
+ previous_scope = Mongoid::Threaded.current_scope(klass)
101
+ if previous_scope
102
+ begin
103
+ Mongoid::Threaded.set_current_scope(nil, klass)
104
+ yield batch
105
+ ensure
106
+ Mongoid::Threaded.set_current_scope(previous_scope, klass)
107
+ end
108
+ else
109
+ yield batch
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def each_batch(relation, batch_size:)
116
+ # https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
117
+ # use cursor for Mongoid
118
+ items = []
119
+ relation.all.each do |item|
120
+ items << item
121
+ if items.length == batch_size
122
+ yield items
123
+ items = []
124
+ end
125
+ end
126
+ yield items if items.any?
127
+ end
128
+
129
+ def batch_size
130
+ @batch_size ||= index.options[:batch_size] || 1000
131
+ end
132
+
133
+ def full_reindex_async(relation, job_options: nil)
134
+ batch_id = 1
135
+ class_name = relation.searchkick_options[:class_name]
136
+ starting_id = false
137
+
138
+ if relation.respond_to?(:primary_key)
139
+ primary_key = relation.primary_key
140
+
141
+ starting_id =
142
+ begin
143
+ relation.minimum(primary_key)
144
+ rescue ActiveRecord::StatementInvalid
145
+ false
146
+ end
147
+ end
148
+
149
+ if starting_id.nil?
150
+ # no records, do nothing
151
+ elsif starting_id.is_a?(Numeric)
152
+ max_id = relation.maximum(primary_key)
153
+ batches_count = ((max_id - starting_id + 1) / batch_size.to_f).ceil
154
+
155
+ batches_count.times do |i|
156
+ min_id = starting_id + (i * batch_size)
157
+ batch_job(class_name, batch_id, job_options, min_id: min_id, max_id: min_id + batch_size - 1)
158
+ batch_id += 1
159
+ end
160
+ else
161
+ in_batches(relation) do |items|
162
+ batch_job(class_name, batch_id, job_options, record_ids: items.map(&:id).map { |v| v.instance_of?(Integer) ? v : v.to_s })
163
+ batch_id += 1
164
+ end
165
+ end
166
+ end
167
+
168
+ def batch_job(class_name, batch_id, job_options, **options)
169
+ job_options ||= {}
170
+ # TODO expire Redis key
171
+ Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
172
+ Searchkick::BulkReindexJob.set(**job_options).perform_later(
173
+ class_name: class_name,
174
+ index_name: index.name,
175
+ batch_id: batch_id,
176
+ **options
177
+ )
178
+ end
179
+
180
+ def batches_key
181
+ "searchkick:reindex:#{index.name}:batches"
182
+ end
183
+ end
184
+ 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
@@ -0,0 +1,359 @@
1
+ module Searchkick
2
+ class Results
3
+ include Enumerable
4
+ extend Forwardable
5
+
6
+ attr_reader :response
7
+
8
+ def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
9
+
10
+ def initialize(klass, response, options = {})
11
+ @klass = klass
12
+ @response = response
13
+ @options = options
14
+ end
15
+
16
+ def with_hit
17
+ return enum_for(:with_hit) unless block_given?
18
+
19
+ build_hits.each do |result|
20
+ yield result
21
+ end
22
+ end
23
+
24
+ def missing_records
25
+ @missing_records ||= with_hit_and_missing_records[1]
26
+ end
27
+
28
+ def suggestions
29
+ if response["suggest"]
30
+ response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
31
+ elsif options[:suggest]
32
+ []
33
+ else
34
+ raise "Pass `suggest: true` to the search method for suggestions"
35
+ end
36
+ end
37
+
38
+ def aggregations
39
+ response["aggregations"]
40
+ end
41
+
42
+ def aggs
43
+ @aggs ||= begin
44
+ if aggregations
45
+ aggregations.dup.each do |field, filtered_agg|
46
+ buckets = filtered_agg[field]
47
+ # move the buckets one level above into the field hash
48
+ if buckets
49
+ filtered_agg.delete(field)
50
+ filtered_agg.merge!(buckets)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def took
58
+ response["took"]
59
+ end
60
+
61
+ def error
62
+ response["error"]
63
+ end
64
+
65
+ def model_name
66
+ if klass.nil?
67
+ ActiveModel::Name.new(self.class, nil, 'Result')
68
+ else
69
+ klass.model_name
70
+ end
71
+ end
72
+
73
+ def entry_name(options = {})
74
+ if options.empty?
75
+ # backward compatibility
76
+ model_name.human.downcase
77
+ else
78
+ default = options[:count] == 1 ? model_name.human : model_name.human.pluralize
79
+ model_name.human(options.reverse_merge(default: default))
80
+ end
81
+ end
82
+
83
+ def total_count
84
+ if options[:total_entries]
85
+ options[:total_entries]
86
+ elsif response["hits"]["total"].is_a?(Hash)
87
+ response["hits"]["total"]["value"]
88
+ else
89
+ response["hits"]["total"]
90
+ end
91
+ end
92
+ alias_method :total_entries, :total_count
93
+
94
+ def current_page
95
+ options[:page]
96
+ end
97
+
98
+ def per_page
99
+ options[:per_page]
100
+ end
101
+ alias_method :limit_value, :per_page
102
+
103
+ def padding
104
+ options[:padding]
105
+ end
106
+
107
+ def total_pages
108
+ (total_count / per_page.to_f).ceil
109
+ end
110
+ alias_method :num_pages, :total_pages
111
+
112
+ def offset_value
113
+ (current_page - 1) * per_page + padding
114
+ end
115
+ alias_method :offset, :offset_value
116
+
117
+ def previous_page
118
+ current_page > 1 ? (current_page - 1) : nil
119
+ end
120
+ alias_method :prev_page, :previous_page
121
+
122
+ def next_page
123
+ current_page < total_pages ? (current_page + 1) : nil
124
+ end
125
+
126
+ def first_page?
127
+ previous_page.nil?
128
+ end
129
+
130
+ def last_page?
131
+ next_page.nil?
132
+ end
133
+
134
+ def out_of_range?
135
+ current_page > total_pages
136
+ end
137
+
138
+ def hits
139
+ if error
140
+ raise Error, "Query error - use the error method to view it"
141
+ else
142
+ @response["hits"]["hits"]
143
+ end
144
+ end
145
+
146
+ def highlights(multiple: false)
147
+ hits.map do |hit|
148
+ hit_highlights(hit, multiple: multiple)
149
+ end
150
+ end
151
+
152
+ def with_highlights(multiple: false)
153
+ return enum_for(:with_highlights, multiple: multiple) unless block_given?
154
+
155
+ with_hit.each do |result, hit|
156
+ yield result, hit_highlights(hit, multiple: multiple)
157
+ end
158
+ end
159
+
160
+ def with_score
161
+ return enum_for(:with_score) unless block_given?
162
+
163
+ with_hit.each do |result, hit|
164
+ yield result, hit["_score"]
165
+ end
166
+ end
167
+
168
+ def misspellings?
169
+ @options[:misspellings]
170
+ end
171
+
172
+ def scroll_id
173
+ @response["_scroll_id"]
174
+ end
175
+
176
+ def scroll
177
+ raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
178
+
179
+ if block_given?
180
+ records = self
181
+ while records.any?
182
+ yield records
183
+ records = records.scroll
184
+ end
185
+
186
+ records.clear_scroll
187
+ else
188
+ begin
189
+ # TODO Active Support notifications for this scroll call
190
+ params = {
191
+ scroll: options[:scroll],
192
+ body: {scroll_id: scroll_id}
193
+ }
194
+ params[:opaque_id] = options[:opaque_id] if options[:opaque_id]
195
+ Results.new(@klass, Searchkick.client.scroll(params), @options)
196
+ rescue => e
197
+ if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
198
+ raise Error, "Scroll id has expired"
199
+ else
200
+ raise e
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ def clear_scroll
207
+ begin
208
+ # try to clear scroll
209
+ # not required as scroll will expire
210
+ # but there is a cost to open scrolls
211
+ Searchkick.client.clear_scroll({body: {scroll_id: scroll_id}})
212
+ rescue => e
213
+ raise e unless Searchkick.transport_error?(e)
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ attr_reader :klass, :options
220
+
221
+ def results
222
+ @results ||= with_hit.map(&:first)
223
+ end
224
+
225
+ def with_hit_and_missing_records
226
+ @with_hit_and_missing_records ||= begin
227
+ missing_records = []
228
+
229
+ if options[:load]
230
+ grouped_hits = hits.group_by { |hit, _| hit["_index"] }
231
+
232
+ # determine models
233
+ index_models = {}
234
+ grouped_hits.each do |index, _|
235
+ models =
236
+ if @klass
237
+ [@klass]
238
+ else
239
+ index_alias = index.split("_")[0..-2].join("_")
240
+ Array((options[:index_mapping] || {})[index_alias])
241
+ end
242
+ raise Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
243
+ index_models[index] = models
244
+ end
245
+
246
+ # fetch results
247
+ results = {}
248
+ grouped_hits.each do |index, index_hits|
249
+ results[index] = {}
250
+ index_models[index].each do |model|
251
+ results[index].merge!(results_query(model, index_hits).to_a.index_by { |r| r.id.to_s })
252
+ end
253
+ end
254
+
255
+ # sort
256
+ results =
257
+ hits.map do |hit|
258
+ result = results[hit["_index"]][hit["_id"].to_s]
259
+ if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
260
+ if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
261
+ highlights = hit_highlights(hit)
262
+ result.define_singleton_method(:search_highlights) do
263
+ highlights
264
+ end
265
+ end
266
+ end
267
+ [result, hit]
268
+ end.select do |result, hit|
269
+ unless result
270
+ models = index_models[hit["_index"]]
271
+ missing_records << {
272
+ id: hit["_id"],
273
+ # may be multiple models for inheritance with child models
274
+ # not ideal to return different types
275
+ # but this situation shouldn't be common
276
+ model: models.size == 1 ? models.first : models
277
+ }
278
+ end
279
+ result
280
+ end
281
+ else
282
+ results =
283
+ hits.map do |hit|
284
+ result =
285
+ if hit["_source"]
286
+ hit.except("_source").merge(hit["_source"])
287
+ elsif hit["fields"]
288
+ hit.except("fields").merge(hit["fields"])
289
+ else
290
+ hit
291
+ end
292
+
293
+ if hit["highlight"] || options[:highlight]
294
+ highlight = hit["highlight"].to_a.to_h { |k, v| [base_field(k), v.first] }
295
+ options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
296
+ result["highlighted_#{k}"] ||= (highlight[k] || result[k])
297
+ end
298
+ end
299
+
300
+ result["id"] ||= result["_id"] # needed for legacy reasons
301
+ [HashWrapper.new(result), hit]
302
+ end
303
+ end
304
+
305
+ [results, missing_records]
306
+ end
307
+ end
308
+
309
+ def build_hits
310
+ @build_hits ||= begin
311
+ if missing_records.any?
312
+ 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(", ")}")
313
+ end
314
+ with_hit_and_missing_records[0]
315
+ end
316
+ end
317
+
318
+ def results_query(records, hits)
319
+ records = Searchkick.scope(records)
320
+
321
+ ids = hits.map { |hit| hit["_id"] }
322
+ if options[:includes] || options[:model_includes]
323
+ included_relations = []
324
+ combine_includes(included_relations, options[:includes])
325
+ combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
326
+
327
+ records = records.includes(included_relations)
328
+ end
329
+
330
+ if options[:scope_results]
331
+ records = options[:scope_results].call(records)
332
+ end
333
+
334
+ Searchkick.load_records(records, ids)
335
+ end
336
+
337
+ def combine_includes(result, inc)
338
+ if inc
339
+ if inc.is_a?(Array)
340
+ result.concat(inc)
341
+ else
342
+ result << inc
343
+ end
344
+ end
345
+ end
346
+
347
+ def base_field(k)
348
+ k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
349
+ end
350
+
351
+ def hit_highlights(hit, multiple: false)
352
+ if hit["highlight"]
353
+ hit["highlight"].to_h { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }
354
+ else
355
+ {}
356
+ end
357
+ end
358
+ end
359
+ 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
@@ -0,0 +1,3 @@
1
+ module Searchkick
2
+ VERSION = "6.1.2"
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