searchkick 2.3.2 → 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
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