searchkick 2.3.2 → 5.2.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 (87) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +377 -84
  3. data/LICENSE.txt +1 -1
  4. data/README.md +859 -602
  5. data/lib/searchkick/bulk_reindex_job.rb +13 -9
  6. data/lib/searchkick/controller_runtime.rb +40 -0
  7. data/lib/searchkick/hash_wrapper.rb +12 -0
  8. data/lib/searchkick/index.rb +281 -356
  9. data/lib/searchkick/index_cache.rb +30 -0
  10. data/lib/searchkick/index_options.rb +487 -281
  11. data/lib/searchkick/indexer.rb +15 -8
  12. data/lib/searchkick/log_subscriber.rb +57 -0
  13. data/lib/searchkick/middleware.rb +9 -2
  14. data/lib/searchkick/model.rb +72 -118
  15. data/lib/searchkick/multi_search.rb +9 -10
  16. data/lib/searchkick/process_batch_job.rb +12 -15
  17. data/lib/searchkick/process_queue_job.rb +22 -13
  18. data/lib/searchkick/query.rb +458 -217
  19. data/lib/searchkick/railtie.rb +7 -0
  20. data/lib/searchkick/record_data.rb +128 -0
  21. data/lib/searchkick/record_indexer.rb +164 -0
  22. data/lib/searchkick/reindex_queue.rb +51 -9
  23. data/lib/searchkick/reindex_v2_job.rb +10 -32
  24. data/lib/searchkick/relation.rb +247 -0
  25. data/lib/searchkick/relation_indexer.rb +155 -0
  26. data/lib/searchkick/results.rb +201 -82
  27. data/lib/searchkick/version.rb +1 -1
  28. data/lib/searchkick/where.rb +11 -0
  29. data/lib/searchkick.rb +269 -97
  30. data/lib/tasks/searchkick.rake +37 -0
  31. metadata +24 -178
  32. data/.gitignore +0 -22
  33. data/.travis.yml +0 -39
  34. data/Gemfile +0 -16
  35. data/Rakefile +0 -20
  36. data/benchmark/Gemfile +0 -23
  37. data/benchmark/benchmark.rb +0 -97
  38. data/lib/searchkick/logging.rb +0 -242
  39. data/lib/searchkick/tasks.rb +0 -33
  40. data/searchkick.gemspec +0 -28
  41. data/test/aggs_test.rb +0 -197
  42. data/test/autocomplete_test.rb +0 -75
  43. data/test/boost_test.rb +0 -202
  44. data/test/callbacks_test.rb +0 -59
  45. data/test/ci/before_install.sh +0 -17
  46. data/test/errors_test.rb +0 -19
  47. data/test/gemfiles/activerecord31.gemfile +0 -7
  48. data/test/gemfiles/activerecord32.gemfile +0 -7
  49. data/test/gemfiles/activerecord40.gemfile +0 -8
  50. data/test/gemfiles/activerecord41.gemfile +0 -8
  51. data/test/gemfiles/activerecord42.gemfile +0 -7
  52. data/test/gemfiles/activerecord50.gemfile +0 -7
  53. data/test/gemfiles/apartment.gemfile +0 -8
  54. data/test/gemfiles/cequel.gemfile +0 -8
  55. data/test/gemfiles/mongoid2.gemfile +0 -7
  56. data/test/gemfiles/mongoid3.gemfile +0 -6
  57. data/test/gemfiles/mongoid4.gemfile +0 -7
  58. data/test/gemfiles/mongoid5.gemfile +0 -7
  59. data/test/gemfiles/mongoid6.gemfile +0 -12
  60. data/test/gemfiles/nobrainer.gemfile +0 -8
  61. data/test/gemfiles/parallel_tests.gemfile +0 -8
  62. data/test/geo_shape_test.rb +0 -175
  63. data/test/highlight_test.rb +0 -78
  64. data/test/index_test.rb +0 -166
  65. data/test/inheritance_test.rb +0 -83
  66. data/test/marshal_test.rb +0 -8
  67. data/test/match_test.rb +0 -276
  68. data/test/misspellings_test.rb +0 -56
  69. data/test/model_test.rb +0 -42
  70. data/test/multi_search_test.rb +0 -36
  71. data/test/multi_tenancy_test.rb +0 -22
  72. data/test/order_test.rb +0 -46
  73. data/test/pagination_test.rb +0 -70
  74. data/test/partial_reindex_test.rb +0 -58
  75. data/test/query_test.rb +0 -35
  76. data/test/records_test.rb +0 -10
  77. data/test/reindex_test.rb +0 -64
  78. data/test/reindex_v2_job_test.rb +0 -32
  79. data/test/routing_test.rb +0 -23
  80. data/test/should_index_test.rb +0 -32
  81. data/test/similar_test.rb +0 -28
  82. data/test/sql_test.rb +0 -214
  83. data/test/suggest_test.rb +0 -95
  84. data/test/support/kaminari.yml +0 -21
  85. data/test/synonyms_test.rb +0 -67
  86. data/test/test_helper.rb +0 -567
  87. data/test/where_test.rb +0 -223
data/lib/searchkick.rb CHANGED
@@ -1,67 +1,112 @@
1
- require "active_model"
2
- require "elasticsearch"
3
- require "hashie"
4
- require "searchkick/version"
5
- require "searchkick/index_options"
6
- require "searchkick/index"
7
- require "searchkick/indexer"
8
- require "searchkick/reindex_queue"
9
- require "searchkick/results"
10
- require "searchkick/query"
11
- require "searchkick/multi_search"
12
- require "searchkick/model"
13
- require "searchkick/tasks"
14
- require "searchkick/middleware"
15
- require "searchkick/logging" if defined?(ActiveSupport::Notifications)
1
+ # dependencies
2
+ require "active_support"
16
3
  require "active_support/core_ext/hash/deep_merge"
4
+ require "active_support/core_ext/module/attr_internal"
5
+ require "active_support/core_ext/module/delegation"
6
+ require "active_support/notifications"
7
+ require "hashie"
17
8
 
18
- # background jobs
19
- begin
20
- require "active_job"
21
- rescue LoadError
22
- # do nothing
23
- end
24
- if defined?(ActiveJob)
25
- require "searchkick/bulk_reindex_job"
26
- require "searchkick/process_queue_job"
27
- require "searchkick/process_batch_job"
28
- require "searchkick/reindex_v2_job"
29
- end
9
+ # stdlib
10
+ require "forwardable"
11
+
12
+ # modules
13
+ require_relative "searchkick/controller_runtime"
14
+ require_relative "searchkick/index"
15
+ require_relative "searchkick/index_cache"
16
+ require_relative "searchkick/index_options"
17
+ require_relative "searchkick/indexer"
18
+ require_relative "searchkick/hash_wrapper"
19
+ require_relative "searchkick/log_subscriber"
20
+ require_relative "searchkick/model"
21
+ require_relative "searchkick/multi_search"
22
+ require_relative "searchkick/query"
23
+ require_relative "searchkick/reindex_queue"
24
+ require_relative "searchkick/record_data"
25
+ require_relative "searchkick/record_indexer"
26
+ require_relative "searchkick/relation"
27
+ require_relative "searchkick/relation_indexer"
28
+ require_relative "searchkick/results"
29
+ require_relative "searchkick/version"
30
+ require_relative "searchkick/where"
31
+
32
+ # integrations
33
+ require_relative "searchkick/railtie" if defined?(Rails)
30
34
 
31
35
  module Searchkick
36
+ # requires faraday
37
+ autoload :Middleware, "searchkick/middleware"
38
+
39
+ # background jobs
40
+ autoload :BulkReindexJob, "searchkick/bulk_reindex_job"
41
+ autoload :ProcessBatchJob, "searchkick/process_batch_job"
42
+ autoload :ProcessQueueJob, "searchkick/process_queue_job"
43
+ autoload :ReindexV2Job, "searchkick/reindex_v2_job"
44
+
45
+ # errors
32
46
  class Error < StandardError; end
33
47
  class MissingIndexError < Error; end
34
- class UnsupportedVersionError < Error; end
35
- class InvalidQueryError < Elasticsearch::Transport::Transport::Errors::BadRequest; end
48
+ class UnsupportedVersionError < Error
49
+ def message
50
+ "This version of Searchkick requires Elasticsearch 7+ or OpenSearch 1+"
51
+ end
52
+ end
53
+ class InvalidQueryError < Error; end
36
54
  class DangerousOperation < Error; end
37
55
  class ImportError < Error; end
38
56
 
39
57
  class << self
40
- attr_accessor :search_method_name, :wordnet_path, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name
58
+ attr_accessor :search_method_name, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options, :client_type
41
59
  attr_writer :client, :env, :search_timeout
42
60
  attr_reader :aws_credentials
43
61
  end
44
62
  self.search_method_name = :search
45
- self.wordnet_path = "/var/lib/wn_s.pl"
46
63
  self.timeout = 10
47
64
  self.models = []
48
65
  self.client_options = {}
49
66
  self.queue_name = :searchkick
67
+ self.model_options = {}
50
68
 
51
69
  def self.client
52
70
  @client ||= begin
53
- require "typhoeus/adapters/faraday" if defined?(Typhoeus)
54
-
55
- Elasticsearch::Client.new({
56
- url: ENV["ELASTICSEARCH_URL"],
57
- transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}}
58
- }.deep_merge(client_options)) do |f|
59
- f.use Searchkick::Middleware
60
- f.request :aws_signers_v4, {
61
- credentials: Aws::Credentials.new(aws_credentials[:access_key_id], aws_credentials[:secret_access_key]),
62
- service_name: "es",
63
- region: aws_credentials[:region] || "us-east-1"
64
- } if aws_credentials
71
+ client_type =
72
+ if self.client_type
73
+ self.client_type
74
+ elsif defined?(OpenSearch::Client) && defined?(Elasticsearch::Client)
75
+ raise Error, "Multiple clients found - set Searchkick.client_type = :elasticsearch or :opensearch"
76
+ elsif defined?(OpenSearch::Client)
77
+ :opensearch
78
+ elsif defined?(Elasticsearch::Client)
79
+ :elasticsearch
80
+ else
81
+ raise Error, "No client found - install the `elasticsearch` or `opensearch-ruby` gem"
82
+ end
83
+
84
+ # check after client to ensure faraday is installed
85
+ # TODO remove in Searchkick 6
86
+ if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
87
+ require "typhoeus/adapters/faraday"
88
+ end
89
+
90
+ if client_type == :opensearch
91
+ OpenSearch::Client.new({
92
+ url: ENV["OPENSEARCH_URL"],
93
+ transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
94
+ retry_on_failure: 2
95
+ }.deep_merge(client_options)) do |f|
96
+ f.use Searchkick::Middleware
97
+ f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
98
+ end
99
+ else
100
+ raise Error, "The `elasticsearch` gem must be 7+" if Elasticsearch::VERSION.to_i < 7
101
+
102
+ Elasticsearch::Client.new({
103
+ url: ENV["ELASTICSEARCH_URL"],
104
+ transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
105
+ retry_on_failure: 2
106
+ }.deep_merge(client_options)) do |f|
107
+ f.use Searchkick::Middleware
108
+ f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
109
+ end
65
110
  end
66
111
  end
67
112
  end
@@ -71,39 +116,77 @@ module Searchkick
71
116
  end
72
117
 
73
118
  def self.search_timeout
74
- @search_timeout || timeout
119
+ (defined?(@search_timeout) && @search_timeout) || timeout
120
+ end
121
+
122
+ # private
123
+ def self.server_info
124
+ @server_info ||= client.info
75
125
  end
76
126
 
77
127
  def self.server_version
78
- @server_version ||= client.info["version"]["number"]
128
+ @server_version ||= server_info["version"]["number"]
79
129
  end
80
130
 
81
- def self.server_below?(version)
82
- Gem::Version.new(server_version.sub("-", ".")) < Gem::Version.new(version.sub("-", "."))
131
+ def self.opensearch?
132
+ unless defined?(@opensearch)
133
+ @opensearch = server_info["version"]["distribution"] == "opensearch"
134
+ end
135
+ @opensearch
136
+ end
137
+
138
+ # TODO always check true version in Searchkick 6
139
+ def self.server_below?(version, true_version = false)
140
+ server_version = !true_version && opensearch? ? "7.10.2" : self.server_version
141
+ Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
83
142
  end
84
143
 
85
- def self.search(term = "*", **options, &block)
86
- klass = options[:model]
144
+ def self.search(term = "*", model: nil, **options, &block)
145
+ options = options.dup
146
+ klass = model
147
+
148
+ # convert index_name into models if possible
149
+ # this should allow for easier upgrade
150
+ if options[:index_name] && !options[:models] && Array(options[:index_name]).all? { |v| v.respond_to?(:searchkick_index) }
151
+ options[:models] = options.delete(:index_name)
152
+ end
153
+
154
+ # make Searchkick.search(models: [Product]) and Product.search equivalent
155
+ unless klass
156
+ models = Array(options[:models])
157
+ if models.size == 1
158
+ klass = models.first
159
+ options.delete(:models)
160
+ end
161
+ end
87
162
 
88
- # TODO add in next major version
89
- # if !klass
90
- # index_name = Array(options[:index_name])
91
- # if index_name.size == 1 && index_name.first.respond_to?(:searchkick_index)
92
- # klass = index_name.first
93
- # end
94
- # end
163
+ if klass
164
+ if (options[:models] && Array(options[:models]) != [klass]) || Array(options[:index_name]).any? { |v| v.respond_to?(:searchkick_index) && v != klass }
165
+ raise ArgumentError, "Use Searchkick.search to search multiple models"
166
+ end
167
+ end
95
168
 
96
- query = Searchkick::Query.new(klass, term, options.except(:model))
97
- block.call(query.body) if block
169
+ # TODO remove in Searchkick 6
98
170
  if options[:execute] == false
99
- query
100
- else
101
- query.execute
171
+ Searchkick.warn("The execute option is no longer needed")
172
+ options.delete(:execute)
102
173
  end
174
+
175
+ options = options.merge(block: block) if block
176
+ Relation.new(klass, term, **options)
103
177
  end
104
178
 
105
- def self.multi_search(queries, retry_misspellings: false)
106
- Searchkick::MultiSearch.new(queries, retry_misspellings: retry_misspellings).perform
179
+ def self.multi_search(queries)
180
+ return if queries.empty?
181
+
182
+ queries = queries.map { |q| q.send(:query) }
183
+ event = {
184
+ name: "Multi Search",
185
+ body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
186
+ }
187
+ ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
188
+ MultiSearch.new(queries).perform
189
+ end
107
190
  end
108
191
 
109
192
  # callbacks
@@ -116,17 +199,34 @@ module Searchkick
116
199
  self.callbacks_value = false
117
200
  end
118
201
 
119
- def self.callbacks?
120
- Thread.current[:searchkick_callbacks_enabled].nil? || Thread.current[:searchkick_callbacks_enabled]
202
+ def self.callbacks?(default: true)
203
+ if callbacks_value.nil?
204
+ default
205
+ else
206
+ callbacks_value != false
207
+ end
121
208
  end
122
209
 
123
- def self.callbacks(value)
210
+ # message is private
211
+ def self.callbacks(value = nil, message: nil)
124
212
  if block_given?
125
213
  previous_value = callbacks_value
126
214
  begin
127
215
  self.callbacks_value = value
128
- yield
129
- indexer.perform if callbacks_value == :bulk
216
+ result = yield
217
+ if callbacks_value == :bulk && indexer.queued_items.any?
218
+ event = {}
219
+ if message
220
+ message.call(event)
221
+ else
222
+ event[:name] = "Bulk"
223
+ event[:count] = indexer.queued_items.size
224
+ end
225
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
226
+ indexer.perform
227
+ end
228
+ end
229
+ result
130
230
  ensure
131
231
  self.callbacks_value = previous_value
132
232
  end
@@ -136,21 +236,20 @@ module Searchkick
136
236
  end
137
237
 
138
238
  def self.aws_credentials=(creds)
139
- require "faraday_middleware/aws_signers_v4"
239
+ require "faraday_middleware/aws_sigv4"
240
+
140
241
  @aws_credentials = creds
141
242
  @client = nil # reset client
142
243
  end
143
244
 
144
245
  def self.reindex_status(index_name)
145
- if redis
146
- batches_left = Searchkick::Index.new(index_name).batches_left
147
- {
148
- completed: batches_left == 0,
149
- batches_left: batches_left
150
- }
151
- else
152
- raise Searchkick::Error, "Redis not configured"
153
- end
246
+ raise Error, "Redis not configured" unless redis
247
+
248
+ batches_left = Index.new(index_name).batches_left
249
+ {
250
+ completed: batches_left == 0,
251
+ batches_left: batches_left
252
+ }
154
253
  end
155
254
 
156
255
  def self.with_redis
@@ -165,30 +264,46 @@ module Searchkick
165
264
  end
166
265
  end
167
266
 
267
+ def self.warn(message)
268
+ super("[searchkick] WARNING: #{message}")
269
+ end
270
+
168
271
  # private
169
- def self.load_records(records, ids)
170
- records =
171
- if records.respond_to?(:primary_key)
172
- # ActiveRecord
173
- records.where(records.primary_key => ids) if records.primary_key
174
- elsif records.respond_to?(:queryable)
175
- # Mongoid 3+
176
- records.queryable.for_ids(ids)
177
- elsif records.respond_to?(:unscoped) && :id.respond_to?(:in)
178
- # Nobrainer
179
- records.unscoped.where(:id.in => ids)
180
- elsif records.respond_to?(:key_column_names)
181
- records.where(records.key_column_names.first => ids)
272
+ def self.load_records(relation, ids)
273
+ relation =
274
+ if relation.respond_to?(:primary_key)
275
+ primary_key = relation.primary_key
276
+ raise Error, "Need primary key to load records" if !primary_key
277
+
278
+ relation.where(primary_key => ids)
279
+ elsif relation.respond_to?(:queryable)
280
+ relation.queryable.for_ids(ids)
182
281
  end
183
282
 
184
- raise Searchkick::Error, "Not sure how to load records" if !records
283
+ raise Error, "Not sure how to load records" if !relation
185
284
 
186
- records
285
+ relation
286
+ end
287
+
288
+ # public (for reindexing conversions)
289
+ def self.load_model(class_name, allow_child: false)
290
+ model = class_name.safe_constantize
291
+ raise Error, "Could not find class: #{class_name}" unless model
292
+ if allow_child
293
+ unless model.respond_to?(:searchkick_klass)
294
+ raise Error, "#{class_name} is not a searchkick model"
295
+ end
296
+ else
297
+ unless Searchkick.models.include?(model)
298
+ raise Error, "#{class_name} is not a searchkick model"
299
+ end
300
+ end
301
+ model
187
302
  end
188
303
 
189
304
  # private
190
305
  def self.indexer
191
- Thread.current[:searchkick_indexer] ||= Searchkick::Indexer.new
306
+ Thread.current[:searchkick_indexer] ||= Indexer.new
192
307
  end
193
308
 
194
309
  # private
@@ -200,11 +315,68 @@ module Searchkick
200
315
  def self.callbacks_value=(value)
201
316
  Thread.current[:searchkick_callbacks_enabled] = value
202
317
  end
203
- end
204
318
 
205
- # TODO find better ActiveModel hook
206
- ActiveModel::Callbacks.send(:include, Searchkick::Model)
319
+ # private
320
+ def self.signer_middleware_aws_params
321
+ {service: "es", region: "us-east-1"}.merge(aws_credentials)
322
+ end
323
+
324
+ # private
325
+ # methods are forwarded to base class
326
+ # this check to see if scope exists on that class
327
+ # it's a bit tricky, but this seems to work
328
+ def self.relation?(klass)
329
+ if klass.respond_to?(:current_scope)
330
+ !klass.current_scope.nil?
331
+ else
332
+ klass.is_a?(Mongoid::Criteria) || !Mongoid::Threaded.current_scope(klass).nil?
333
+ end
334
+ end
335
+
336
+ # private
337
+ def self.scope(model)
338
+ # safety check to make sure used properly in code
339
+ raise Error, "Cannot scope relation" if relation?(model)
340
+
341
+ if model.searchkick_options[:unscope]
342
+ model.unscoped
343
+ else
344
+ model
345
+ end
346
+ end
347
+
348
+ # private
349
+ def self.not_found_error?(e)
350
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::NotFound)) ||
351
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) ||
352
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::NotFound))
353
+ end
354
+
355
+ # private
356
+ def self.transport_error?(e)
357
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Error)) ||
358
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Error)) ||
359
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Error))
360
+ end
361
+
362
+ # private
363
+ def self.not_allowed_error?(e)
364
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::MethodNotAllowed)) ||
365
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::MethodNotAllowed)) ||
366
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::MethodNotAllowed))
367
+ end
368
+ end
207
369
 
208
370
  ActiveSupport.on_load(:active_record) do
209
371
  extend Searchkick::Model
210
372
  end
373
+
374
+ ActiveSupport.on_load(:mongoid) do
375
+ Mongoid::Document::ClassMethods.include Searchkick::Model
376
+ end
377
+
378
+ ActiveSupport.on_load(:action_controller) do
379
+ include Searchkick::ControllerRuntime
380
+ end
381
+
382
+ Searchkick::LogSubscriber.attach_to :searchkick
@@ -0,0 +1,37 @@
1
+ namespace :searchkick do
2
+ desc "reindex a model (specify CLASS)"
3
+ task reindex: :environment do
4
+ class_name = ENV["CLASS"]
5
+ abort "USAGE: rake searchkick:reindex CLASS=Product" unless class_name
6
+
7
+ model =
8
+ begin
9
+ Searchkick.load_model(class_name)
10
+ rescue Searchkick::Error => e
11
+ abort e.message
12
+ end
13
+
14
+ puts "Reindexing #{model.name}..."
15
+ model.reindex
16
+ puts "Reindex successful"
17
+ end
18
+
19
+ namespace :reindex do
20
+ desc "reindex all models"
21
+ task all: :environment do
22
+ # eager load models to populate Searchkick.models
23
+ if Rails.respond_to?(:autoloaders) && Rails.autoloaders.zeitwerk_enabled?
24
+ # fix for https://github.com/rails/rails/issues/37006
25
+ Zeitwerk::Loader.eager_load_all
26
+ else
27
+ Rails.application.eager_load!
28
+ end
29
+
30
+ Searchkick.models.each do |model|
31
+ puts "Reindexing #{model.name}..."
32
+ model.reindex
33
+ end
34
+ puts "Reindex complete"
35
+ end
36
+ end
37
+ end