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,19 @@
1
+ module Searchkick
2
+ class BulkReindexJob < Searchkick.parent_job.constantize
3
+ queue_as { Searchkick.queue_name }
4
+
5
+ def perform(class_name:, record_ids: nil, index_name: nil, method_name: nil, batch_id: nil, min_id: nil, max_id: nil, ignore_missing: nil)
6
+ model = Searchkick.load_model(class_name)
7
+ index = model.searchkick_index(name: index_name)
8
+
9
+ record_ids ||= min_id..max_id
10
+
11
+ relation = Searchkick.scope(model)
12
+ relation = Searchkick.load_records(relation, record_ids)
13
+ relation = relation.search_import if relation.respond_to?(:search_import)
14
+
15
+ RecordIndexer.new(index).reindex(relation, mode: :inline, method_name: method_name, ignore_missing: ignore_missing, full: false)
16
+ RelationIndexer.new(index).batch_completed(batch_id) if batch_id
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ # based on https://gist.github.com/mnutt/566725
2
+ module Searchkick
3
+ module ControllerRuntime
4
+ extend ActiveSupport::Concern
5
+
6
+ protected
7
+
8
+ attr_internal :searchkick_runtime
9
+
10
+ def process_action(action, *args)
11
+ # We also need to reset the runtime before each action
12
+ # because of queries in middleware or in cases we are streaming
13
+ # and it won't be cleaned up by the method below.
14
+ Searchkick::LogSubscriber.reset_runtime
15
+ super
16
+ end
17
+
18
+ def cleanup_view_runtime
19
+ searchkick_rt_before_render = Searchkick::LogSubscriber.reset_runtime
20
+ runtime = super
21
+ searchkick_rt_after_render = Searchkick::LogSubscriber.reset_runtime
22
+ self.searchkick_runtime = searchkick_rt_before_render + searchkick_rt_after_render
23
+ runtime - searchkick_rt_after_render
24
+ end
25
+
26
+ def append_info_to_payload(payload)
27
+ super
28
+ payload[:searchkick_runtime] = (searchkick_runtime || 0) + Searchkick::LogSubscriber.reset_runtime
29
+ end
30
+
31
+ module ClassMethods
32
+ def log_process_action(payload)
33
+ messages = super
34
+ runtime = payload[:searchkick_runtime]
35
+ messages << ("Searchkick: %.1fms" % runtime.to_f) if runtime.to_f > 0
36
+ messages
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ module Searchkick
2
+ class HashWrapper
3
+ def initialize(attributes)
4
+ @attributes = attributes
5
+ end
6
+
7
+ def [](name)
8
+ @attributes[name.to_s]
9
+ end
10
+
11
+ def to_h
12
+ @attributes
13
+ end
14
+
15
+ def as_json(...)
16
+ @attributes.as_json(...)
17
+ end
18
+
19
+ def to_json(...)
20
+ @attributes.to_json(...)
21
+ end
22
+
23
+ def method_missing(name, ...)
24
+ if @attributes.key?(name.to_s)
25
+ self[name]
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def respond_to_missing?(name, ...)
32
+ @attributes.key?(name.to_s) || super
33
+ end
34
+
35
+ def inspect
36
+ attributes = @attributes.reject { |k, v| k[0] == "_" }.map { |k, v| "#{k}: #{v.inspect}" }
37
+ attributes.unshift(attributes.pop) # move id to start
38
+ "#<#{self.class.name} #{attributes.join(", ")}>"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,480 @@
1
+ module Searchkick
2
+ class Index
3
+ attr_reader :name, :options
4
+
5
+ def initialize(name, options = {})
6
+ @name = name
7
+ @options = options
8
+ @klass_document_type = {} # cache
9
+ end
10
+
11
+ def index_options
12
+ IndexOptions.new(self).index_options
13
+ end
14
+
15
+ def create(body = {})
16
+ client.indices.create index: name, body: body
17
+ end
18
+
19
+ def delete
20
+ if alias_exists?
21
+ # can't call delete directly on aliases in ES 6
22
+ indices = client.indices.get_alias(name: name).keys
23
+ client.indices.delete index: indices
24
+ else
25
+ client.indices.delete index: name
26
+ end
27
+ end
28
+
29
+ def exists?
30
+ client.indices.exists index: name
31
+ end
32
+
33
+ def refresh
34
+ client.indices.refresh index: name
35
+ end
36
+
37
+ def alias_exists?
38
+ client.indices.exists_alias name: name
39
+ end
40
+
41
+ # call to_h for consistent results between elasticsearch gem 7 and 8
42
+ # could do for all API calls, but just do for ones where return value is focus for now
43
+ def mapping
44
+ client.indices.get_mapping(index: name).to_h
45
+ end
46
+
47
+ # call to_h for consistent results between elasticsearch gem 7 and 8
48
+ def settings
49
+ client.indices.get_settings(index: name).to_h
50
+ end
51
+
52
+ def refresh_interval
53
+ index_settings["refresh_interval"]
54
+ end
55
+
56
+ def update_settings(settings)
57
+ client.indices.put_settings index: name, body: settings
58
+ end
59
+
60
+ def tokens(text, options = {})
61
+ client.indices.analyze(body: {text: text}.merge(options), index: name)["tokens"].map { |t| t["token"] }
62
+ end
63
+
64
+ def total_docs
65
+ response =
66
+ client.search(
67
+ index: name,
68
+ body: {
69
+ query: {match_all: {}},
70
+ size: 0,
71
+ track_total_hits: true
72
+ }
73
+ )
74
+
75
+ Results.new(nil, response).total_count
76
+ end
77
+
78
+ def promote(new_name, update_refresh_interval: false)
79
+ if update_refresh_interval
80
+ new_index = Index.new(new_name, @options)
81
+ settings = options[:settings] || {}
82
+ refresh_interval = (settings[:index] && settings[:index][:refresh_interval]) || "1s"
83
+ new_index.update_settings(index: {refresh_interval: refresh_interval})
84
+ end
85
+
86
+ old_indices =
87
+ begin
88
+ client.indices.get_alias(name: name).keys
89
+ rescue => e
90
+ raise e unless Searchkick.not_found_error?(e)
91
+ {}
92
+ end
93
+ actions = old_indices.map { |old_name| {remove: {index: old_name, alias: name}} } + [{add: {index: new_name, alias: name}}]
94
+ client.indices.update_aliases body: {actions: actions}
95
+ end
96
+ alias_method :swap, :promote
97
+
98
+ def retrieve(record)
99
+ record_data = RecordData.new(self, record).record_data
100
+
101
+ # remove underscore
102
+ get_options = record_data.to_h { |k, v| [k.to_s.delete_prefix("_").to_sym, v] }
103
+
104
+ client.get(get_options)["_source"]
105
+ end
106
+
107
+ def all_indices(unaliased: false)
108
+ indices =
109
+ begin
110
+ if client.indices.respond_to?(:get_alias)
111
+ client.indices.get_alias(index: "#{name}*")
112
+ else
113
+ client.indices.get_aliases
114
+ end
115
+ rescue => e
116
+ raise e unless Searchkick.not_found_error?(e)
117
+ {}
118
+ end
119
+ indices = indices.select { |_k, v| v.empty? || v["aliases"].empty? } if unaliased
120
+ indices.select { |k, _v| k =~ /\A#{Regexp.escape(name)}_\d{14,17}\z/ }.keys
121
+ end
122
+
123
+ # remove old indices that start w/ index_name
124
+ def clean_indices
125
+ indices = all_indices(unaliased: true)
126
+ indices.each do |index|
127
+ Index.new(index).delete
128
+ end
129
+ indices
130
+ end
131
+
132
+ def store(record)
133
+ notify(record, "Store") do
134
+ queue_index([record])
135
+ end
136
+ end
137
+
138
+ def remove(record)
139
+ notify(record, "Remove") do
140
+ queue_delete([record])
141
+ end
142
+ end
143
+
144
+ def update_record(record, method_name)
145
+ notify(record, "Update") do
146
+ queue_update([record], method_name)
147
+ end
148
+ end
149
+
150
+ def bulk_delete(records)
151
+ return if records.empty?
152
+
153
+ notify_bulk(records, "Delete") do
154
+ queue_delete(records)
155
+ end
156
+ end
157
+
158
+ def bulk_index(records)
159
+ return if records.empty?
160
+
161
+ notify_bulk(records, "Import") do
162
+ queue_index(records)
163
+ end
164
+ end
165
+ alias_method :import, :bulk_index
166
+
167
+ def bulk_update(records, method_name, ignore_missing: nil)
168
+ return if records.empty?
169
+
170
+ notify_bulk(records, "Update") do
171
+ queue_update(records, method_name, ignore_missing: ignore_missing)
172
+ end
173
+ end
174
+
175
+ def search_id(record)
176
+ RecordData.new(self, record).search_id
177
+ end
178
+
179
+ def document_type(record)
180
+ RecordData.new(self, record).document_type
181
+ end
182
+
183
+ def similar_record(record, **options)
184
+ options[:per_page] ||= 10
185
+ options[:similar] = [RecordData.new(self, record).record_data]
186
+ options[:models] ||= [record.class] unless options.key?(:model)
187
+
188
+ Searchkick.search("*", **options)
189
+ end
190
+
191
+ def reload_synonyms
192
+ if Searchkick.opensearch?
193
+ client.transport.perform_request "POST", "_plugins/_refresh_search_analyzers/#{CGI.escape(name)}"
194
+ else
195
+ begin
196
+ client.transport.perform_request("GET", "#{CGI.escape(name)}/_reload_search_analyzers")
197
+ rescue => e
198
+ raise Error, "Requires non-OSS version of Elasticsearch" if Searchkick.not_allowed_error?(e)
199
+ raise e
200
+ end
201
+ end
202
+ end
203
+
204
+ # queue
205
+
206
+ def reindex_queue
207
+ ReindexQueue.new(name)
208
+ end
209
+
210
+ # reindex
211
+
212
+ # note: this is designed to be used internally
213
+ # so it does not check object matches index class
214
+ def reindex(object, method_name: nil, ignore_missing: nil, full: false, **options)
215
+ if @options[:job_options]
216
+ options[:job_options] = (@options[:job_options] || {}).merge(options[:job_options] || {})
217
+ end
218
+
219
+ if object.is_a?(Array)
220
+ # note: purposefully skip full
221
+ return reindex_records(object, method_name: method_name, ignore_missing: ignore_missing, **options)
222
+ end
223
+
224
+ if !object.respond_to?(:searchkick_klass)
225
+ raise Error, "Cannot reindex object"
226
+ end
227
+
228
+ scoped = Searchkick.relation?(object)
229
+ # call searchkick_klass for inheritance
230
+ relation = scoped ? object.all : Searchkick.scope(object.searchkick_klass).all
231
+
232
+ refresh = options.fetch(:refresh, !scoped)
233
+ options.delete(:refresh)
234
+
235
+ if method_name || (scoped && !full)
236
+ mode = options.delete(:mode) || :inline
237
+ scope = options.delete(:scope)
238
+ job_options = options.delete(:job_options)
239
+ raise ArgumentError, "unsupported keywords: #{options.keys.map(&:inspect).join(", ")}" if options.any?
240
+
241
+ # import only
242
+ import_scope(relation, method_name: method_name, mode: mode, scope: scope, ignore_missing: ignore_missing, job_options: job_options)
243
+ self.refresh if refresh
244
+ true
245
+ else
246
+ async = options.delete(:async)
247
+ if async
248
+ if async.is_a?(Hash) && async[:wait]
249
+ Searchkick.warn "async option is deprecated - use mode: :async, wait: true instead"
250
+ options[:wait] = true unless options.key?(:wait)
251
+ else
252
+ Searchkick.warn "async option is deprecated - use mode: :async instead"
253
+ end
254
+ options[:mode] ||= :async
255
+ end
256
+
257
+ full_reindex(relation, **options)
258
+ end
259
+ end
260
+
261
+ def create_index(index_options: nil)
262
+ index_options ||= self.index_options
263
+ index = Index.new("#{name}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}", @options)
264
+ index.create(index_options)
265
+ index
266
+ end
267
+
268
+ def import_scope(relation, **options)
269
+ relation_indexer.reindex(relation, **options)
270
+ end
271
+
272
+ def batches_left
273
+ relation_indexer.batches_left
274
+ end
275
+
276
+ # private
277
+ def klass_document_type(klass, ignore_type = false)
278
+ @klass_document_type[[klass, ignore_type]] ||= begin
279
+ if !ignore_type && klass.searchkick_klass.searchkick_options[:_type]
280
+ type = klass.searchkick_klass.searchkick_options[:_type]
281
+ type = type.call if type.respond_to?(:call)
282
+ type
283
+ else
284
+ klass.model_name.to_s.underscore
285
+ end
286
+ end
287
+ end
288
+
289
+ # private
290
+ def conversions_fields
291
+ @conversions_fields ||= begin
292
+ conversions = Array(options[:conversions])
293
+ conversions.map(&:to_s) + conversions.map(&:to_sym)
294
+ end
295
+ end
296
+
297
+ # private
298
+ def conversions_v2_fields
299
+ @conversions_v2_fields ||= Array(options[:conversions_v2]).map(&:to_s)
300
+ end
301
+
302
+ # private
303
+ def suggest_fields
304
+ @suggest_fields ||= Array(options[:suggest]).map(&:to_s)
305
+ end
306
+
307
+ # private
308
+ def locations_fields
309
+ @locations_fields ||= begin
310
+ locations = Array(options[:locations])
311
+ locations.map(&:to_s) + locations.map(&:to_sym)
312
+ end
313
+ end
314
+
315
+ # private
316
+ def uuid
317
+ index_settings["uuid"]
318
+ end
319
+
320
+ protected
321
+
322
+ def client
323
+ Searchkick.client
324
+ end
325
+
326
+ def queue_index(records)
327
+ Searchkick.indexer.queue(records.map { |r| RecordData.new(self, r).index_data })
328
+ end
329
+
330
+ def queue_delete(records)
331
+ Searchkick.indexer.queue(records.reject { |r| r.id.blank? }.map { |r| RecordData.new(self, r).delete_data })
332
+ end
333
+
334
+ def queue_update(records, method_name, ignore_missing:)
335
+ items = records.map { |r| RecordData.new(self, r).update_data(method_name) }
336
+ items.each { |i| i.instance_variable_set(:@ignore_missing, true) } if ignore_missing
337
+ Searchkick.indexer.queue(items)
338
+ end
339
+
340
+ def relation_indexer
341
+ @relation_indexer ||= RelationIndexer.new(self)
342
+ end
343
+
344
+ def index_settings
345
+ settings.values.first["settings"]["index"]
346
+ end
347
+
348
+ def import_before_promotion(index, relation, **import_options)
349
+ index.import_scope(relation, **import_options)
350
+ end
351
+
352
+ def reindex_records(object, mode: nil, refresh: false, **options)
353
+ mode ||= Searchkick.callbacks_value || @options[:callbacks] || :inline
354
+ mode = :inline if mode == :bulk
355
+
356
+ result = RecordIndexer.new(self).reindex(object, mode: mode, full: false, **options)
357
+ self.refresh if refresh
358
+ result
359
+ end
360
+
361
+ # https://gist.github.com/jarosan/3124884
362
+ # https://www.elastic.co/blog/changing-mapping-with-zero-downtime/
363
+ def full_reindex(relation, import: true, resume: false, retain: false, mode: nil, refresh_interval: nil, scope: nil, wait: nil, job_options: nil)
364
+ raise ArgumentError, "wait only available in :async mode" if !wait.nil? && mode != :async
365
+ raise ArgumentError, "Full reindex does not support :queue mode - use :async mode instead" if mode == :queue
366
+
367
+ if resume
368
+ index_name = all_indices.sort.last
369
+ raise Error, "No index to resume" unless index_name
370
+ index = Index.new(index_name, @options)
371
+ else
372
+ clean_indices unless retain
373
+
374
+ index_options = relation.searchkick_index_options
375
+ index_options.deep_merge!(settings: {index: {refresh_interval: refresh_interval}}) if refresh_interval
376
+ index = create_index(index_options: index_options)
377
+ end
378
+
379
+ import_options = {
380
+ mode: (mode || :inline),
381
+ full: true,
382
+ resume: resume,
383
+ scope: scope,
384
+ job_options: job_options
385
+ }
386
+
387
+ uuid = index.uuid
388
+
389
+ # check if alias exists
390
+ alias_exists = alias_exists?
391
+ if alias_exists
392
+ import_before_promotion(index, relation, **import_options) if import
393
+
394
+ # get existing indices to remove
395
+ unless mode == :async
396
+ check_uuid(uuid, index.uuid)
397
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
398
+ clean_indices unless retain
399
+ end
400
+ else
401
+ delete if exists?
402
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
403
+
404
+ # import after promotion
405
+ index.import_scope(relation, **import_options) if import
406
+ end
407
+
408
+ if mode == :async
409
+ if wait
410
+ puts "Created index: #{index.name}"
411
+ puts "Jobs queued. Waiting..."
412
+ loop do
413
+ sleep 3
414
+ status = Searchkick.reindex_status(index.name)
415
+ break if status[:completed]
416
+ puts "Batches left: #{status[:batches_left]}"
417
+ end
418
+ # already promoted if alias didn't exist
419
+ if alias_exists
420
+ puts "Jobs complete. Promoting..."
421
+ check_uuid(uuid, index.uuid)
422
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
423
+ end
424
+ clean_indices unless retain
425
+ puts "SUCCESS!"
426
+ end
427
+
428
+ {index_name: index.name}
429
+ else
430
+ index.refresh
431
+ true
432
+ end
433
+ rescue => e
434
+ if Searchkick.transport_error?(e) && (e.message.include?("No handler for type [text]") || e.message.include?("class java.util.ArrayList cannot be cast to class java.util.Map"))
435
+ raise UnsupportedVersionError
436
+ end
437
+
438
+ raise e
439
+ end
440
+
441
+ # safety check
442
+ # still a chance for race condition since its called before promotion
443
+ # ideal is for user to disable automatic index creation
444
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation
445
+ def check_uuid(old_uuid, new_uuid)
446
+ if old_uuid != new_uuid
447
+ raise Error, "Safety check failed - only run one Model.reindex per model at a time"
448
+ end
449
+ end
450
+
451
+ def notify(record, name)
452
+ if Searchkick.callbacks_value == :bulk
453
+ yield
454
+ else
455
+ name = "#{record.class.searchkick_klass.name} #{name}" if record && record.class.searchkick_klass
456
+ event = {
457
+ name: name,
458
+ id: search_id(record)
459
+ }
460
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
461
+ yield
462
+ end
463
+ end
464
+ end
465
+
466
+ def notify_bulk(records, name)
467
+ if Searchkick.callbacks_value == :bulk
468
+ yield
469
+ else
470
+ event = {
471
+ name: "#{records.first.class.searchkick_klass.name} #{name}",
472
+ count: records.size
473
+ }
474
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
475
+ yield
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
@@ -0,0 +1,30 @@
1
+ module Searchkick
2
+ class IndexCache
3
+ def initialize(max_size: 20)
4
+ @data = {}
5
+ @mutex = Mutex.new
6
+ @max_size = max_size
7
+ end
8
+
9
+ # probably a better pattern for this
10
+ # but keep it simple
11
+ def fetch(name)
12
+ # thread-safe in MRI without mutex
13
+ # due to how context switching works
14
+ @mutex.synchronize do
15
+ if @data.key?(name)
16
+ @data[name]
17
+ else
18
+ @data.clear if @data.size >= @max_size
19
+ @data[name] = yield
20
+ end
21
+ end
22
+ end
23
+
24
+ def clear
25
+ @mutex.synchronize do
26
+ @data.clear
27
+ end
28
+ end
29
+ end
30
+ end