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/railtie.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, Railtie, RecordData, RecordIndexer, ReindexQueue, ReindexV2Job, Relation, RelationIndexer, Results, Script, UnsupportedVersionError, Where

Constant Summary collapse

VERSION =
"6.0.2"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.aws_credentialsObject

Returns the value of attribute aws_credentials.



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

def aws_credentials
  @aws_credentials
end

.clientObject



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
# File 'lib/searchkick.rb', line 73

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

    if client_type == :opensearch
      OpenSearch::Client.new({
        url: ENV["OPENSEARCH_URL"],
        transport_options: {request: {timeout: timeout}},
        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 8+" if Elasticsearch::VERSION.to_i < 8

      Elasticsearch::Client.new({
        url: ENV["ELASTICSEARCH_URL"],
        transport_options: {request: {timeout: timeout}},
        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.



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

def client_options
  @client_options
end

.client_typeObject

Returns the value of attribute client_type.



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

def client_type
  @client_type
end

.envObject



112
113
114
# File 'lib/searchkick.rb', line 112

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

.index_prefixObject

Returns the value of attribute index_prefix.



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

def index_prefix
  @index_prefix
end

.index_suffixObject

Returns the value of attribute index_suffix.



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

def index_suffix
  @index_suffix
end

.model_optionsObject

Returns the value of attribute model_options.



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

def model_options
  @model_options
end

.modelsObject

Returns the value of attribute models.



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

def models
  @models
end

.parent_jobObject

Returns the value of attribute parent_job.



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

def parent_job
  @parent_job
end

.queue_nameObject

Returns the value of attribute queue_name.



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

def queue_name
  @queue_name
end

.redisObject

Returns the value of attribute redis.



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

def redis
  @redis
end

.search_method_nameObject

Returns the value of attribute search_method_name.



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

def search_method_name
  @search_method_name
end

.search_timeoutObject



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

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

.timeoutObject

Returns the value of attribute timeout.



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

def timeout
  @timeout
end

Class Method Details

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

message is private



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/searchkick.rb', line 217

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)


208
209
210
211
212
213
214
# File 'lib/searchkick.rb', line 208

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

.callbacks_valueObject

private



316
317
318
# File 'lib/searchkick.rb', line 316

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

.callbacks_value=(value) ⇒ Object

private



321
322
323
# File 'lib/searchkick.rb', line 321

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

.disable_callbacksObject



204
205
206
# File 'lib/searchkick.rb', line 204

def self.disable_callbacks
  self.callbacks_value = false
end

.enable_callbacksObject

callbacks



200
201
202
# File 'lib/searchkick.rb', line 200

def self.enable_callbacks
  self.callbacks_value = nil
end

.indexerObject

private



311
312
313
# File 'lib/searchkick.rb', line 311

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

.knn_support?Boolean

private

Returns:

  • (Boolean)


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

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

.load_model(class_name, allow_child: false) ⇒ Object

public (for reindexing conversions)

Raises:



295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/searchkick.rb', line 295

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:



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/searchkick.rb', line 278

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, opaque_id: nil) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/searchkick.rb', line 178

def self.multi_search(queries, opaque_id: nil)
  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, opaque_id: opaque_id).perform
  end
end

.not_allowed_error?(e) ⇒ Boolean

private

Returns:

  • (Boolean)


369
370
371
372
373
# File 'lib/searchkick.rb', line 369

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)


355
356
357
358
359
# File 'lib/searchkick.rb', line 355

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)


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

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

.reindex_status(index_name) ⇒ Object

Raises:



251
252
253
254
255
256
257
258
259
# File 'lib/searchkick.rb', line 251

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)


334
335
336
337
338
339
340
# File 'lib/searchkick.rb', line 334

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:



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

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



194
195
196
# File 'lib/searchkick.rb', line 194

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

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



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/searchkick.rb', line 149

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

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

.server_below?(version) ⇒ Boolean

Returns:

  • (Boolean)


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

def self.server_below?(version)
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
end

.server_infoObject

private



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

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

.server_versionObject



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

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

.signer_middleware_aws_paramsObject

private



326
327
328
# File 'lib/searchkick.rb', line 326

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

.transport_error?(e) ⇒ Boolean

private

Returns:

  • (Boolean)


362
363
364
365
366
# File 'lib/searchkick.rb', line 362

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



273
274
275
# File 'lib/searchkick.rb', line 273

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

.with_redisObject



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

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