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.
data/lib/searchkick.rb CHANGED
@@ -1,29 +1,43 @@
1
1
  # dependencies
2
2
  require "active_support"
3
3
  require "active_support/core_ext/hash/deep_merge"
4
- require "elasticsearch"
4
+ require "active_support/core_ext/module/attr_internal"
5
+ require "active_support/core_ext/module/delegation"
6
+ require "active_support/notifications"
5
7
  require "hashie"
6
8
 
9
+ # stdlib
10
+ require "forwardable"
11
+
7
12
  # modules
8
- require "searchkick/bulk_indexer"
9
- require "searchkick/index"
10
- require "searchkick/indexer"
11
- require "searchkick/hash_wrapper"
12
- require "searchkick/middleware"
13
- require "searchkick/model"
14
- require "searchkick/multi_search"
15
- require "searchkick/query"
16
- require "searchkick/reindex_queue"
17
- require "searchkick/record_data"
18
- require "searchkick/record_indexer"
19
- require "searchkick/results"
20
- require "searchkick/version"
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/reranking"
29
+ require_relative "searchkick/results"
30
+ require_relative "searchkick/script"
31
+ require_relative "searchkick/version"
32
+ require_relative "searchkick/where"
21
33
 
22
34
  # integrations
23
- require "searchkick/railtie" if defined?(Rails)
24
- require "searchkick/logging" if defined?(ActiveSupport::Notifications)
35
+ require_relative "searchkick/railtie" if defined?(Rails)
25
36
 
26
37
  module Searchkick
38
+ # requires faraday
39
+ autoload :Middleware, "searchkick/middleware"
40
+
27
41
  # background jobs
28
42
  autoload :BulkReindexJob, "searchkick/bulk_reindex_job"
29
43
  autoload :ProcessBatchJob, "searchkick/process_batch_job"
@@ -33,19 +47,21 @@ module Searchkick
33
47
  # errors
34
48
  class Error < StandardError; end
35
49
  class MissingIndexError < Error; end
36
- class UnsupportedVersionError < Error; end
37
- # TODO switch to Error
38
- class InvalidQueryError < Elasticsearch::Transport::Transport::Errors::BadRequest; end
50
+ class UnsupportedVersionError < Error
51
+ def message
52
+ "This version of Searchkick requires Elasticsearch 7+ or OpenSearch 1+"
53
+ end
54
+ end
55
+ class InvalidQueryError < Error; end
39
56
  class DangerousOperation < Error; end
40
57
  class ImportError < Error; end
41
58
 
42
59
  class << self
43
- attr_accessor :search_method_name, :wordnet_path, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options
60
+ attr_accessor :search_method_name, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options, :client_type
44
61
  attr_writer :client, :env, :search_timeout
45
62
  attr_reader :aws_credentials
46
63
  end
47
64
  self.search_method_name = :search
48
- self.wordnet_path = "/var/lib/wn_s.pl"
49
65
  self.timeout = 10
50
66
  self.models = []
51
67
  self.client_options = {}
@@ -54,15 +70,52 @@ module Searchkick
54
70
 
55
71
  def self.client
56
72
  @client ||= begin
57
- require "typhoeus/adapters/faraday" if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
58
-
59
- Elasticsearch::Client.new({
60
- url: ENV["ELASTICSEARCH_URL"] || ENV["OPENSEARCH_URL"],
61
- transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
62
- retry_on_failure: 2
63
- }.deep_merge(client_options)) do |f|
64
- f.use Searchkick::Middleware
65
- f.request signer_middleware_key, signer_middleware_aws_params if aws_credentials
73
+ client_type =
74
+ if self.client_type
75
+ self.client_type
76
+ elsif defined?(OpenSearch::Client) && defined?(Elasticsearch::Client)
77
+ raise Error, "Multiple clients found - set Searchkick.client_type = :elasticsearch or :opensearch"
78
+ elsif defined?(OpenSearch::Client)
79
+ :opensearch
80
+ elsif defined?(Elasticsearch::Client)
81
+ :elasticsearch
82
+ else
83
+ raise Error, "No client found - install the `elasticsearch` or `opensearch-ruby` gem"
84
+ end
85
+
86
+ # check after client to ensure faraday is installed
87
+ # TODO remove in Searchkick 6
88
+ if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
89
+ require "typhoeus/adapters/faraday"
90
+ end
91
+
92
+ if client_type == :opensearch
93
+ OpenSearch::Client.new({
94
+ url: ENV["OPENSEARCH_URL"],
95
+ # TODO remove headers in Searchkick 6
96
+ transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
97
+ retry_on_failure: 2
98
+ }.deep_merge(client_options)) do |f|
99
+ f.use Searchkick::Middleware
100
+ f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
101
+ end
102
+ else
103
+ raise Error, "The `elasticsearch` gem must be 7+" if Elasticsearch::VERSION.to_i < 7
104
+
105
+ transport_options = {request: {timeout: timeout}}
106
+ # TODO remove headers in Searchkick 6
107
+ if Elasticsearch::VERSION.to_i < 9
108
+ transport_options[:headers] = {content_type: "application/json"}
109
+ end
110
+
111
+ Elasticsearch::Client.new({
112
+ url: ENV["ELASTICSEARCH_URL"],
113
+ transport_options: transport_options,
114
+ retry_on_failure: 2
115
+ }.deep_merge(client_options)) do |f|
116
+ f.use Searchkick::Middleware
117
+ f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
118
+ end
66
119
  end
67
120
  end
68
121
  end
@@ -91,17 +144,19 @@ module Searchkick
91
144
  @opensearch
92
145
  end
93
146
 
94
- def self.server_below?(version)
95
- server_version = opensearch? ? "7.10.2" : self.server_version
147
+ # TODO always check true version in Searchkick 6
148
+ def self.server_below?(version, true_version = false)
149
+ server_version = !true_version && opensearch? ? "7.10.2" : self.server_version
96
150
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
97
151
  end
98
152
 
99
- # memoize for performance
100
- def self.server_below7?
101
- unless defined?(@server_below7)
102
- @server_below7 = server_below?("7.0.0")
153
+ # private
154
+ def self.knn_support?
155
+ if opensearch?
156
+ !server_below?("2.4.0", true)
157
+ else
158
+ !server_below?("8.6.0")
103
159
  end
104
- @server_below7
105
160
  end
106
161
 
107
162
  def self.search(term = "*", model: nil, **options, &block)
@@ -129,17 +184,34 @@ module Searchkick
129
184
  end
130
185
  end
131
186
 
132
- options = options.merge(block: block) if block
133
- query = Searchkick::Query.new(klass, term, **options)
187
+ # TODO remove in Searchkick 6
134
188
  if options[:execute] == false
135
- query
136
- else
137
- query.execute
189
+ Searchkick.warn("The execute option is no longer needed")
190
+ options.delete(:execute)
138
191
  end
192
+
193
+ options = options.merge(block: block) if block
194
+ Relation.new(klass, term, **options)
139
195
  end
140
196
 
141
197
  def self.multi_search(queries)
142
- Searchkick::MultiSearch.new(queries).perform
198
+ return if queries.empty?
199
+
200
+ queries = queries.map { |q| q.send(:query) }
201
+ event = {
202
+ name: "Multi Search",
203
+ body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
204
+ }
205
+ ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
206
+ MultiSearch.new(queries).perform
207
+ end
208
+ end
209
+
210
+ # script
211
+
212
+ # experimental
213
+ def self.script(source, **options)
214
+ Script.new(source, **options)
143
215
  end
144
216
 
145
217
  # callbacks
@@ -160,13 +232,25 @@ module Searchkick
160
232
  end
161
233
  end
162
234
 
163
- def self.callbacks(value = nil)
235
+ # message is private
236
+ def self.callbacks(value = nil, message: nil)
164
237
  if block_given?
165
238
  previous_value = callbacks_value
166
239
  begin
167
240
  self.callbacks_value = value
168
241
  result = yield
169
- indexer.perform if callbacks_value == :bulk
242
+ if callbacks_value == :bulk && indexer.queued_items.any?
243
+ event = {}
244
+ if message
245
+ message.call(event)
246
+ else
247
+ event[:name] = "Bulk"
248
+ event[:count] = indexer.queued_items.size
249
+ end
250
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
251
+ indexer.perform
252
+ end
253
+ end
170
254
  result
171
255
  ensure
172
256
  self.callbacks_value = previous_value
@@ -177,27 +261,22 @@ module Searchkick
177
261
  end
178
262
 
179
263
  def self.aws_credentials=(creds)
180
- begin
181
- # TODO remove in Searchkick 5 (just use aws_sigv4)
182
- require "faraday_middleware/aws_signers_v4"
183
- rescue LoadError
184
- require "faraday_middleware/aws_sigv4"
185
- end
264
+ require "faraday_middleware/aws_sigv4"
265
+
186
266
  @aws_credentials = creds
187
267
  @client = nil # reset client
188
268
  end
189
269
 
190
270
  def self.reindex_status(index_name)
191
- raise Searchkick::Error, "Redis not configured" unless redis
271
+ raise Error, "Redis not configured" unless redis
192
272
 
193
- batches_left = Searchkick::Index.new(index_name).batches_left
273
+ batches_left = Index.new(index_name).batches_left
194
274
  {
195
275
  completed: batches_left == 0,
196
276
  batches_left: batches_left
197
277
  }
198
278
  end
199
279
 
200
- # TODO use ConnectionPool::Wrapper when redis is set so this is no longer needed
201
280
  def self.with_redis
202
281
  if redis
203
282
  if redis.respond_to?(:with)
@@ -215,29 +294,41 @@ module Searchkick
215
294
  end
216
295
 
217
296
  # private
218
- def self.load_records(records, ids)
219
- records =
220
- if records.respond_to?(:primary_key)
221
- # ActiveRecord
222
- records.where(records.primary_key => ids) if records.primary_key
223
- elsif records.respond_to?(:queryable)
224
- # Mongoid 3+
225
- records.queryable.for_ids(ids)
226
- elsif records.respond_to?(:unscoped) && :id.respond_to?(:in)
227
- # Nobrainer
228
- records.unscoped.where(:id.in => ids)
229
- elsif records.respond_to?(:key_column_names)
230
- records.where(records.key_column_names.first => ids)
297
+ def self.load_records(relation, ids)
298
+ relation =
299
+ if relation.respond_to?(:primary_key)
300
+ primary_key = relation.primary_key
301
+ raise Error, "Need primary key to load records" if !primary_key
302
+
303
+ relation.where(primary_key => ids)
304
+ elsif relation.respond_to?(:queryable)
305
+ relation.queryable.for_ids(ids)
231
306
  end
232
307
 
233
- raise Searchkick::Error, "Not sure how to load records" if !records
308
+ raise Error, "Not sure how to load records" if !relation
309
+
310
+ relation
311
+ end
234
312
 
235
- records
313
+ # public (for reindexing conversions)
314
+ def self.load_model(class_name, allow_child: false)
315
+ model = class_name.safe_constantize
316
+ raise Error, "Could not find class: #{class_name}" unless model
317
+ if allow_child
318
+ unless model.respond_to?(:searchkick_klass)
319
+ raise Error, "#{class_name} is not a searchkick model"
320
+ end
321
+ else
322
+ unless Searchkick.models.include?(model)
323
+ raise Error, "#{class_name} is not a searchkick model"
324
+ end
325
+ end
326
+ model
236
327
  end
237
328
 
238
329
  # private
239
330
  def self.indexer
240
- Thread.current[:searchkick_indexer] ||= Searchkick::Indexer.new
331
+ Thread.current[:searchkick_indexer] ||= Indexer.new
241
332
  end
242
333
 
243
334
  # private
@@ -250,22 +341,9 @@ module Searchkick
250
341
  Thread.current[:searchkick_callbacks_enabled] = value
251
342
  end
252
343
 
253
- # private
254
- def self.signer_middleware_key
255
- defined?(FaradayMiddleware::AwsSignersV4) ? :aws_signers_v4 : :aws_sigv4
256
- end
257
-
258
344
  # private
259
345
  def self.signer_middleware_aws_params
260
- if signer_middleware_key == :aws_sigv4
261
- {service: "es", region: "us-east-1"}.merge(aws_credentials)
262
- else
263
- {
264
- credentials: aws_credentials[:credentials] || Aws::Credentials.new(aws_credentials[:access_key_id], aws_credentials[:secret_access_key]),
265
- service_name: "es",
266
- region: aws_credentials[:region] || "us-east-1"
267
- }
268
- end
346
+ {service: "es", region: "us-east-1"}.merge(aws_credentials)
269
347
  end
270
348
 
271
349
  # private
@@ -275,31 +353,55 @@ module Searchkick
275
353
  def self.relation?(klass)
276
354
  if klass.respond_to?(:current_scope)
277
355
  !klass.current_scope.nil?
278
- elsif defined?(Mongoid::Threaded)
279
- !Mongoid::Threaded.current_scope(klass).nil?
356
+ else
357
+ klass.is_a?(Mongoid::Criteria) || !Mongoid::Threaded.current_scope(klass).nil?
358
+ end
359
+ end
360
+
361
+ # private
362
+ def self.scope(model)
363
+ # safety check to make sure used properly in code
364
+ raise Error, "Cannot scope relation" if relation?(model)
365
+
366
+ if model.searchkick_options[:unscope]
367
+ model.unscoped
368
+ else
369
+ model
280
370
  end
281
371
  end
282
372
 
283
373
  # private
284
374
  def self.not_found_error?(e)
285
- (defined?(Elasticsearch) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) ||
375
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::NotFound)) ||
376
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) ||
286
377
  (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::NotFound))
287
378
  end
288
379
 
289
380
  # private
290
381
  def self.transport_error?(e)
291
- (defined?(Elasticsearch) && e.is_a?(Elasticsearch::Transport::Transport::Error)) ||
382
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Error)) ||
383
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Error)) ||
292
384
  (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Error))
293
385
  end
294
- end
295
386
 
296
- require "active_model/callbacks"
297
- ActiveModel::Callbacks.include(Searchkick::Model)
298
- # TODO use
299
- # ActiveSupport.on_load(:mongoid) do
300
- # Mongoid::Document::ClassMethods.include Searchkick::Model
301
- # end
387
+ # private
388
+ def self.not_allowed_error?(e)
389
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::MethodNotAllowed)) ||
390
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::MethodNotAllowed)) ||
391
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::MethodNotAllowed))
392
+ end
393
+ end
302
394
 
303
395
  ActiveSupport.on_load(:active_record) do
304
396
  extend Searchkick::Model
305
397
  end
398
+
399
+ ActiveSupport.on_load(:mongoid) do
400
+ Mongoid::Document::ClassMethods.include Searchkick::Model
401
+ end
402
+
403
+ ActiveSupport.on_load(:action_controller) do
404
+ include Searchkick::ControllerRuntime
405
+ end
406
+
407
+ Searchkick::LogSubscriber.attach_to :searchkick
@@ -4,9 +4,12 @@ namespace :searchkick do
4
4
  class_name = ENV["CLASS"]
5
5
  abort "USAGE: rake searchkick:reindex CLASS=Product" unless class_name
6
6
 
7
- model = class_name.safe_constantize
8
- abort "Could not find class: #{class_name}" unless model
9
- abort "#{class_name} is not a searchkick model" unless Searchkick.models.include?(model)
7
+ model =
8
+ begin
9
+ Searchkick.load_model(class_name)
10
+ rescue Searchkick::Error => e
11
+ abort e.message
12
+ end
10
13
 
11
14
  puts "Reindexing #{model.name}..."
12
15
  model.reindex
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.3
4
+ version: 5.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-11-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activemodel
@@ -16,34 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '5'
18
+ version: '7.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '5'
27
- - !ruby/object:Gem::Dependency
28
- name: elasticsearch
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '6'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '7.14'
37
- type: :runtime
38
- prerelease: false
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: '6'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '7.14'
25
+ version: '7.1'
47
26
  - !ruby/object:Gem::Dependency
48
27
  name: hashie
49
28
  requirement: !ruby/object:Gem::Requirement
@@ -58,7 +37,6 @@ dependencies:
58
37
  - - ">="
59
38
  - !ruby/object:Gem::Version
60
39
  version: '0'
61
- description:
62
40
  email: andrew@ankane.org
63
41
  executables: []
64
42
  extensions: []
@@ -68,13 +46,14 @@ files:
68
46
  - LICENSE.txt
69
47
  - README.md
70
48
  - lib/searchkick.rb
71
- - lib/searchkick/bulk_indexer.rb
72
49
  - lib/searchkick/bulk_reindex_job.rb
50
+ - lib/searchkick/controller_runtime.rb
73
51
  - lib/searchkick/hash_wrapper.rb
74
52
  - lib/searchkick/index.rb
53
+ - lib/searchkick/index_cache.rb
75
54
  - lib/searchkick/index_options.rb
76
55
  - lib/searchkick/indexer.rb
77
- - lib/searchkick/logging.rb
56
+ - lib/searchkick/log_subscriber.rb
78
57
  - lib/searchkick/middleware.rb
79
58
  - lib/searchkick/model.rb
80
59
  - lib/searchkick/multi_search.rb
@@ -86,14 +65,18 @@ files:
86
65
  - lib/searchkick/record_indexer.rb
87
66
  - lib/searchkick/reindex_queue.rb
88
67
  - lib/searchkick/reindex_v2_job.rb
68
+ - lib/searchkick/relation.rb
69
+ - lib/searchkick/relation_indexer.rb
70
+ - lib/searchkick/reranking.rb
89
71
  - lib/searchkick/results.rb
72
+ - lib/searchkick/script.rb
90
73
  - lib/searchkick/version.rb
74
+ - lib/searchkick/where.rb
91
75
  - lib/tasks/searchkick.rake
92
76
  homepage: https://github.com/ankane/searchkick
93
77
  licenses:
94
78
  - MIT
95
79
  metadata: {}
96
- post_install_message:
97
80
  rdoc_options: []
98
81
  require_paths:
99
82
  - lib
@@ -101,15 +84,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
84
  requirements:
102
85
  - - ">="
103
86
  - !ruby/object:Gem::Version
104
- version: '2.4'
87
+ version: '3.2'
105
88
  required_rubygems_version: !ruby/object:Gem::Requirement
106
89
  requirements:
107
90
  - - ">="
108
91
  - !ruby/object:Gem::Version
109
92
  version: '0'
110
93
  requirements: []
111
- rubygems_version: 3.2.22
112
- signing_key:
94
+ rubygems_version: 3.6.7
113
95
  specification_version: 4
114
96
  summary: Intelligent search made easy with Rails and Elasticsearch or OpenSearch
115
97
  test_files: []