Module: Searchkick

Defined in:
lib/searchkick/indexer.rb,
lib/searchkick.rb,
lib/searchkick/index.rb,
lib/searchkick/model.rb,
lib/searchkick/query.rb,
lib/searchkick/where.rb,
lib/searchkick/script.rb,
lib/searchkick/results.rb,
lib/searchkick/version.rb,
lib/searchkick/relation.rb,
lib/searchkick/reranking.rb,
lib/searchkick/middleware.rb,
lib/searchkick/index_cache.rb,
lib/searchkick/record_data.rb,
lib/searchkick/hash_wrapper.rb,
lib/searchkick/multi_search.rb,
lib/searchkick/index_options.rb,
lib/searchkick/reindex_queue.rb,
lib/searchkick/log_subscriber.rb,
lib/searchkick/record_indexer.rb,
lib/searchkick/reindex_v2_job.rb,
lib/searchkick/bulk_reindex_job.rb,
lib/searchkick/relation_indexer.rb,
lib/searchkick/process_batch_job.rb,
lib/searchkick/process_queue_job.rb,
lib/searchkick/controller_runtime.rb

Overview

Defined Under Namespace

Modules: ControllerRuntime, Model, Reranking Classes: BulkReindexJob, DangerousOperation, Error, HashWrapper, ImportError, Index, IndexCache, IndexOptions, Indexer, InvalidQueryError, LogSubscriber, Middleware, MissingIndexError, MultiSearch, ProcessBatchJob, ProcessQueueJob, Query, RecordData, RecordIndexer, ReindexQueue, ReindexV2Job, Relation, RelationIndexer, Results, Script, UnsupportedVersionError, Where

Constant Summary collapse

VERSION =
"5.4.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.aws_credentialsObject

Returns the value of attribute aws_credentials.



62
63
64
# File 'lib/searchkick.rb', line 62

def aws_credentials
  @aws_credentials
end

.clientObject



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/searchkick.rb', line 71

def self.client
  @client ||= begin
    client_type =
      if self.client_type
        self.client_type
      elsif defined?(OpenSearch::Client) && defined?(Elasticsearch::Client)
        raise Error, "Multiple clients found - set Searchkick.client_type = :elasticsearch or :opensearch"
      elsif defined?(OpenSearch::Client)
        :opensearch
      elsif defined?(Elasticsearch::Client)
        :elasticsearch
      else
        raise Error, "No client found - install the `elasticsearch` or `opensearch-ruby` gem"
      end

    # check after client to ensure faraday is installed
    # TODO remove in Searchkick 6
    if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
      require "typhoeus/adapters/faraday"
    end

    if client_type == :opensearch
      OpenSearch::Client.new({
        url: ENV["OPENSEARCH_URL"],
        transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
        retry_on_failure: 2
      }.deep_merge(client_options)) do |f|
        f.use Searchkick::Middleware
        f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
      end
    else
      raise Error, "The `elasticsearch` gem must be 7+" if Elasticsearch::VERSION.to_i < 7

      Elasticsearch::Client.new({
        url: ENV["ELASTICSEARCH_URL"],
        transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
        retry_on_failure: 2
      }.deep_merge(client_options)) do |f|
        f.use Searchkick::Middleware
        f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
      end
    end
  end
end

.client_optionsObject

Returns the value of attribute client_options.



60
61
62
# File 'lib/searchkick.rb', line 60

def client_options
  @client_options
end

.client_typeObject

Returns the value of attribute client_type.



60
61
62
# File 'lib/searchkick.rb', line 60

def client_type
  @client_type
end

.envObject



116
117
118
# File 'lib/searchkick.rb', line 116

def self.env
  @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
end

.index_prefixObject

Returns the value of attribute index_prefix.



60
61
62
# File 'lib/searchkick.rb', line 60

def index_prefix
  @index_prefix
end

.index_suffixObject

Returns the value of attribute index_suffix.



60
61
62
# File 'lib/searchkick.rb', line 60

def index_suffix
  @index_suffix
end

.model_optionsObject

Returns the value of attribute model_options.



60
61
62
# File 'lib/searchkick.rb', line 60

def model_options
  @model_options
end

.modelsObject

Returns the value of attribute models.



60
61
62
# File 'lib/searchkick.rb', line 60

def models
  @models
end

.queue_nameObject

Returns the value of attribute queue_name.



60
61
62
# File 'lib/searchkick.rb', line 60

def queue_name
  @queue_name
end

.redisObject

Returns the value of attribute redis.



60
61
62
# File 'lib/searchkick.rb', line 60

def redis
  @redis
end

.search_method_nameObject

Returns the value of attribute search_method_name.



60
61
62
# File 'lib/searchkick.rb', line 60

def search_method_name
  @search_method_name
end

.search_timeoutObject



120
121
122
# File 'lib/searchkick.rb', line 120

def self.search_timeout
  (defined?(@search_timeout) && @search_timeout) || timeout
end

.timeoutObject

Returns the value of attribute timeout.



60
61
62
# File 'lib/searchkick.rb', line 60

def timeout
  @timeout
end

Class Method Details

.callbacks(value = nil, message: nil) ⇒ Object

message is private



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/searchkick.rb', line 229

def self.callbacks(value = nil, message: nil)
  if block_given?
    previous_value = callbacks_value
    begin
      self.callbacks_value = value
      result = yield
      if callbacks_value == :bulk && indexer.queued_items.any?
        event = {}
        if message
          message.call(event)
        else
          event[:name] = "Bulk"
          event[:count] = indexer.queued_items.size
        end
        ActiveSupport::Notifications.instrument("request.searchkick", event) do
          indexer.perform
        end
      end
      result
    ensure
      self.callbacks_value = previous_value
    end
  else
    self.callbacks_value = value
  end
end

.callbacks?(default: true) ⇒ Boolean

Returns:

  • (Boolean)


220
221
222
223
224
225
226
# File 'lib/searchkick.rb', line 220

def self.callbacks?(default: true)
  if callbacks_value.nil?
    default
  else
    callbacks_value != false
  end
end

.callbacks_valueObject

private



328
329
330
# File 'lib/searchkick.rb', line 328

def self.callbacks_value
  Thread.current[:searchkick_callbacks_enabled]
end

.callbacks_value=(value) ⇒ Object

private



333
334
335
# File 'lib/searchkick.rb', line 333

def self.callbacks_value=(value)
  Thread.current[:searchkick_callbacks_enabled] = value
end

.disable_callbacksObject



216
217
218
# File 'lib/searchkick.rb', line 216

def self.disable_callbacks
  self.callbacks_value = false
end

.enable_callbacksObject

callbacks



212
213
214
# File 'lib/searchkick.rb', line 212

def self.enable_callbacks
  self.callbacks_value = nil
end

.indexerObject

private



323
324
325
# File 'lib/searchkick.rb', line 323

def self.indexer
  Thread.current[:searchkick_indexer] ||= Indexer.new
end

.knn_support?Boolean

private

Returns:

  • (Boolean)


147
148
149
150
151
152
153
# File 'lib/searchkick.rb', line 147

def self.knn_support?
  if opensearch?
    !server_below?("2.4.0", true)
  else
    !server_below?("8.6.0")
  end
end

.load_model(class_name, allow_child: false) ⇒ Object

public (for reindexing conversions)

Raises:



307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/searchkick.rb', line 307

def self.load_model(class_name, allow_child: false)
  model = class_name.safe_constantize
  raise Error, "Could not find class: #{class_name}" unless model
  if allow_child
    unless model.respond_to?(:searchkick_klass)
      raise Error, "#{class_name} is not a searchkick model"
    end
  else
    unless Searchkick.models.include?(model)
      raise Error, "#{class_name} is not a searchkick model"
    end
  end
  model
end

.load_records(relation, ids) ⇒ Object

private

Raises:



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/searchkick.rb', line 290

def self.load_records(relation, ids)
  relation =
    if relation.respond_to?(:primary_key)
      primary_key = relation.primary_key
      raise Error, "Need primary key to load records" if !primary_key

      relation.where(primary_key => ids)
    elsif relation.respond_to?(:queryable)
      relation.queryable.for_ids(ids)
    end

  raise Error, "Not sure how to load records" if !relation

  relation
end

.multi_search(queries) ⇒ Object



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/searchkick.rb', line 190

def self.multi_search(queries)
  return if queries.empty?

  queries = queries.map { |q| q.send(:query) }
  event = {
    name: "Multi Search",
    body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
  }
  ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
    MultiSearch.new(queries).perform
  end
end

.not_allowed_error?(e) ⇒ Boolean

private

Returns:

  • (Boolean)


381
382
383
384
385
# File 'lib/searchkick.rb', line 381

def self.not_allowed_error?(e)
  (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::MethodNotAllowed)) ||
  (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::MethodNotAllowed)) ||
  (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::MethodNotAllowed))
end

.not_found_error?(e) ⇒ Boolean

private

Returns:

  • (Boolean)


367
368
369
370
371
# File 'lib/searchkick.rb', line 367

def self.not_found_error?(e)
  (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::NotFound)) ||
  (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) ||
  (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::NotFound))
end

.opensearch?Boolean

Returns:

  • (Boolean)


133
134
135
136
137
138
# File 'lib/searchkick.rb', line 133

def self.opensearch?
  unless defined?(@opensearch)
    @opensearch = server_info["version"]["distribution"] == "opensearch"
  end
  @opensearch
end

.reindex_status(index_name) ⇒ Object

Raises:



263
264
265
266
267
268
269
270
271
# File 'lib/searchkick.rb', line 263

def self.reindex_status(index_name)
  raise Error, "Redis not configured" unless redis

  batches_left = Index.new(index_name).batches_left
  {
    completed: batches_left == 0,
    batches_left: batches_left
  }
end

.relation?(klass) ⇒ Boolean

private methods are forwarded to base class this check to see if scope exists on that class it’s a bit tricky, but this seems to work

Returns:

  • (Boolean)


346
347
348
349
350
351
352
# File 'lib/searchkick.rb', line 346

def self.relation?(klass)
  if klass.respond_to?(:current_scope)
    !klass.current_scope.nil?
  else
    klass.is_a?(Mongoid::Criteria) || !Mongoid::Threaded.current_scope(klass).nil?
  end
end

.scope(model) ⇒ Object

private

Raises:



355
356
357
358
359
360
361
362
363
364
# File 'lib/searchkick.rb', line 355

def self.scope(model)
  # safety check to make sure used properly in code
  raise Error, "Cannot scope relation" if relation?(model)

  if model.searchkick_options[:unscope]
    model.unscoped
  else
    model
  end
end

.script(source, **options) ⇒ Object

experimental



206
207
208
# File 'lib/searchkick.rb', line 206

def self.script(source, **options)
  Script.new(source, **options)
end

.search(term = "*", model: nil, **options, &block) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/searchkick.rb', line 155

def self.search(term = "*", model: nil, **options, &block)
  options = options.dup
  klass = model

  # convert index_name into models if possible
  # this should allow for easier upgrade
  if options[:index_name] && !options[:models] && Array(options[:index_name]).all? { |v| v.respond_to?(:searchkick_index) }
    options[:models] = options.delete(:index_name)
  end

  # make Searchkick.search(models: [Product]) and Product.search equivalent
  unless klass
    models = Array(options[:models])
    if models.size == 1
      klass = models.first
      options.delete(:models)
    end
  end

  if klass
    if (options[:models] && Array(options[:models]) != [klass]) || Array(options[:index_name]).any? { |v| v.respond_to?(:searchkick_index) && v != klass }
      raise ArgumentError, "Use Searchkick.search to search multiple models"
    end
  end

  # TODO remove in Searchkick 6
  if options[:execute] == false
    Searchkick.warn("The execute option is no longer needed")
    options.delete(:execute)
  end

  options = options.merge(block: block) if block
  Relation.new(klass, term, **options)
end

.server_below?(version, true_version = false) ⇒ Boolean

TODO always check true version in Searchkick 6

Returns:

  • (Boolean)


141
142
143
144
# File 'lib/searchkick.rb', line 141

def self.server_below?(version, true_version = false)
  server_version = !true_version && opensearch? ? "7.10.2" : self.server_version
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
end

.server_infoObject

private



125
126
127
# File 'lib/searchkick.rb', line 125

def self.server_info
  @server_info ||= client.info
end

.server_versionObject



129
130
131
# File 'lib/searchkick.rb', line 129

def self.server_version
  @server_version ||= server_info["version"]["number"]
end

.signer_middleware_aws_paramsObject

private



338
339
340
# File 'lib/searchkick.rb', line 338

def self.signer_middleware_aws_params
  {service: "es", region: "us-east-1"}.merge(aws_credentials)
end

.transport_error?(e) ⇒ Boolean

private

Returns:

  • (Boolean)


374
375
376
377
378
# File 'lib/searchkick.rb', line 374

def self.transport_error?(e)
  (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Error)) ||
  (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Error)) ||
  (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Error))
end

.warn(message) ⇒ Object



285
286
287
# File 'lib/searchkick.rb', line 285

def self.warn(message)
  super("[searchkick] WARNING: #{message}")
end

.with_redisObject



273
274
275
276
277
278
279
280
281
282
283
# File 'lib/searchkick.rb', line 273

def self.with_redis
  if redis
    if redis.respond_to?(:with)
      redis.with do |r|
        yield r
      end
    else
      yield redis
    end
  end
end