lib/searchkick.rb in searchkick-4.6.3 vs lib/searchkick.rb in searchkick-5.0.0

- old
+ new

@@ -1,71 +1,114 @@ # dependencies require "active_support" require "active_support/core_ext/hash/deep_merge" -require "elasticsearch" +require "active_support/core_ext/module/attr_internal" +require "active_support/core_ext/module/delegation" +require "active_support/notifications" require "hashie" +# stdlib +require "forwardable" + # modules -require "searchkick/bulk_indexer" +require "searchkick/controller_runtime" require "searchkick/index" +require "searchkick/index_cache" +require "searchkick/index_options" require "searchkick/indexer" require "searchkick/hash_wrapper" -require "searchkick/middleware" +require "searchkick/log_subscriber" require "searchkick/model" require "searchkick/multi_search" require "searchkick/query" require "searchkick/reindex_queue" require "searchkick/record_data" require "searchkick/record_indexer" +require "searchkick/relation" +require "searchkick/relation_indexer" require "searchkick/results" require "searchkick/version" # integrations require "searchkick/railtie" if defined?(Rails) -require "searchkick/logging" if defined?(ActiveSupport::Notifications) module Searchkick + # requires faraday + autoload :Middleware, "searchkick/middleware" + # background jobs autoload :BulkReindexJob, "searchkick/bulk_reindex_job" autoload :ProcessBatchJob, "searchkick/process_batch_job" autoload :ProcessQueueJob, "searchkick/process_queue_job" autoload :ReindexV2Job, "searchkick/reindex_v2_job" # errors class Error < StandardError; end class MissingIndexError < Error; end - class UnsupportedVersionError < Error; end - # TODO switch to Error - class InvalidQueryError < Elasticsearch::Transport::Transport::Errors::BadRequest; end + class UnsupportedVersionError < Error + def message + "This version of Searchkick requires Elasticsearch 7+ or OpenSearch 1+" + end + end + class InvalidQueryError < Error; end class DangerousOperation < Error; end class ImportError < Error; end class << self - attr_accessor :search_method_name, :wordnet_path, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options + attr_accessor :search_method_name, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options, :client_type attr_writer :client, :env, :search_timeout attr_reader :aws_credentials end self.search_method_name = :search - self.wordnet_path = "/var/lib/wn_s.pl" self.timeout = 10 self.models = [] self.client_options = {} self.queue_name = :searchkick self.model_options = {} def self.client @client ||= begin - require "typhoeus/adapters/faraday" if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0") + 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 - Elasticsearch::Client.new({ - url: ENV["ELASTICSEARCH_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 signer_middleware_key, signer_middleware_aws_params if aws_credentials + # 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 def self.env @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" @@ -94,18 +137,10 @@ def self.server_below?(version) server_version = opensearch? ? "7.10.2" : self.server_version Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0]) end - # memoize for performance - def self.server_below7? - unless defined?(@server_below7) - @server_below7 = server_below?("7.0.0") - end - @server_below7 - end - def self.search(term = "*", model: nil, **options, &block) options = options.dup klass = model # convert index_name into models if possible @@ -127,21 +162,31 @@ 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 - query = Searchkick::Query.new(klass, term, **options) + # TODO remove in Searchkick 6 if options[:execute] == false - query - else - query.execute + Searchkick.warn("The execute option is no longer needed") + options.delete(:execute) end + + options = options.merge(block: block) if block + Searchkick::Relation.new(klass, term, **options) end def self.multi_search(queries) - Searchkick::MultiSearch.new(queries).perform + 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 + Searchkick::MultiSearch.new(queries).perform + end end # callbacks def self.enable_callbacks @@ -158,33 +203,41 @@ else callbacks_value != false end end - def self.callbacks(value = nil) + # message is private + def self.callbacks(value = nil, message: nil) if block_given? previous_value = callbacks_value begin self.callbacks_value = value result = yield - indexer.perform if callbacks_value == :bulk + 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 def self.aws_credentials=(creds) - begin - # TODO remove in Searchkick 5 (just use aws_sigv4) - require "faraday_middleware/aws_signers_v4" - rescue LoadError - require "faraday_middleware/aws_sigv4" - end + require "faraday_middleware/aws_sigv4" + @aws_credentials = creds @client = nil # reset client end def self.reindex_status(index_name) @@ -195,11 +248,10 @@ completed: batches_left == 0, batches_left: batches_left } end - # TODO use ConnectionPool::Wrapper when redis is set so this is no longer needed def self.with_redis if redis if redis.respond_to?(:with) redis.with do |r| yield r @@ -213,33 +265,45 @@ def self.warn(message) super("[searchkick] WARNING: #{message}") end # private - def self.load_records(records, ids) - records = - if records.respond_to?(:primary_key) - # ActiveRecord - records.where(records.primary_key => ids) if records.primary_key - elsif records.respond_to?(:queryable) - # Mongoid 3+ - records.queryable.for_ids(ids) - elsif records.respond_to?(:unscoped) && :id.respond_to?(:in) - # Nobrainer - records.unscoped.where(:id.in => ids) - elsif records.respond_to?(:key_column_names) - records.where(records.key_column_names.first => ids) + 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 Searchkick::Error, "Not sure how to load records" if !records + raise Error, "Not sure how to load records" if !relation - records + relation end # private + 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 + + # private def self.indexer - Thread.current[:searchkick_indexer] ||= Searchkick::Indexer.new + Thread.current[:searchkick_indexer] ||= Indexer.new end # private def self.callbacks_value Thread.current[:searchkick_callbacks_enabled] @@ -249,57 +313,68 @@ def self.callbacks_value=(value) Thread.current[:searchkick_callbacks_enabled] = value end # private - def self.signer_middleware_key - defined?(FaradayMiddleware::AwsSignersV4) ? :aws_signers_v4 : :aws_sigv4 - end - - # private def self.signer_middleware_aws_params - if signer_middleware_key == :aws_sigv4 - {service: "es", region: "us-east-1"}.merge(aws_credentials) - else - { - credentials: aws_credentials[:credentials] || Aws::Credentials.new(aws_credentials[:access_key_id], aws_credentials[:secret_access_key]), - service_name: "es", - region: aws_credentials[:region] || "us-east-1" - } - end + {service: "es", region: "us-east-1"}.merge(aws_credentials) end # 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 def self.relation?(klass) if klass.respond_to?(:current_scope) !klass.current_scope.nil? - elsif defined?(Mongoid::Threaded) - !Mongoid::Threaded.current_scope(klass).nil? + else + klass.is_a?(Mongoid::Criteria) || !Mongoid::Threaded.current_scope(klass).nil? end end # private + 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 + + # private def self.not_found_error?(e) - (defined?(Elasticsearch) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) || + (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 # private def self.transport_error?(e) - (defined?(Elasticsearch) && e.is_a?(Elasticsearch::Transport::Transport::Error)) || + (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 + + # private + 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 end -require "active_model/callbacks" -ActiveModel::Callbacks.include(Searchkick::Model) -# TODO use -# ActiveSupport.on_load(:mongoid) do -# Mongoid::Document::ClassMethods.include Searchkick::Model -# end - ActiveSupport.on_load(:active_record) do extend Searchkick::Model end + +ActiveSupport.on_load(:mongoid) do + Mongoid::Document::ClassMethods.include Searchkick::Model +end + +ActiveSupport.on_load(:action_controller) do + include Searchkick::ControllerRuntime +end + +Searchkick::LogSubscriber.attach_to :searchkick