toptal/chewy

View on GitHub
lib/chewy/search/request.rb

Summary

Maintainability
C
1 day
Test Coverage
module Chewy
  module Search
    # The main request DSL class. Supports multiple index requests.
    # Supports ES5 search API and query DSL.
    #
    # @note The class tries to be as immutable as possible,
    #   so most of the methods return a new instance of the class.
    # @see Chewy::Search
    # @example
    #   scope = Chewy::Search::Request.new(PlacesIndex)
    #   # => <Chewy::Search::Request {:index=>["places"], :body=>{}}>
    #   scope.limit(20)
    #   # => <Chewy::Search::Request {:index=>["places"], :body=>{:size=>20}}>
    #   scope.order(:name).offset(10)
    #   # => <Chewy::Search::Request {:index=>["places"], :body=>{:sort=>["name"], :from=>10}}>
    class Request
      include Enumerable
      include Scoping
      include Scrolling
      UNDEFINED = Class.new.freeze
      EVERFIELDS = %w[_index _type _id _parent _routing].freeze
      DELEGATED_METHODS = %i[
        query filter post_filter knn order reorder docvalue_fields
        track_scores track_total_hits request_cache explain version profile
        search_type preference limit offset terminate_after
        timeout min_score source stored_fields search_after
        load script_fields suggest aggs aggregations collapse none
        indices_boost rescore highlight total total_count
        total_entries indices types delete_all count exists?
        exist? find pluck scroll_batches scroll_hits
        scroll_results scroll_wrappers ignore_unavailable
      ].to_set.freeze
      DEFAULT_BATCH_SIZE = 1000
      DEFAULT_PLUCK_BATCH_SIZE = 10_000
      DEFAULT_SCROLL = '1m'.freeze
      # An array of storage names that are modifying returned fields in hits
      FIELD_STORAGES = %i[
        source docvalue_fields script_fields stored_fields
      ].freeze
      # An array of storage names that are not related to hits at all.
      EXTRA_STORAGES = %i[aggs suggest].freeze
      # An array of storage names that are changing the returned hist collection in any way.
      WHERE_STORAGES = %i[
        query filter post_filter knn none min_score rescore indices_boost collapse
      ].freeze

      delegate :hits, :wrappers, :objects, :records, :documents,
               :object_hash, :record_hash, :document_hash,
               :total, :max_score, :took, :timed_out?, to: :response
      delegate :each, :size, :to_a, :[], to: :wrappers
      alias_method :to_ary, :to_a
      alias_method :total_count, :total
      alias_method :total_entries, :total

      # The class is initialized with the list of chewy indexes,
      # which are later used to compose requests.
      # Any symbol/string passed is treated as an index identifier.
      #
      # @example
      #   Chewy::Search::Request.new(:places)
      #   # => <Chewy::Search::Request {:index=>["places"], :body=>{}}>
      #   Chewy::Search::Request.new(PlacesIndex)
      #   # => <Chewy::Search::Request {:index=>["places"], :body=>{}}>
      #   Chewy::Search::Request.new(UsersIndex, PlacesIndex)
      #   # => <Chewy::Search::Request {:index=>["users", "places"], :body=>{}}>
      # @param indexes [Array<Chewy::Index, String, Symbol>] indexes
      def initialize(*indexes)
        parameters.modify!(:indices) do
          replace!(indices: indexes)
        end
      end

      # Underlying parameter storage collection.
      #
      # @return [Chewy::Search::Parameters]
      def parameters
        @parameters ||= Parameters.new
      end

      # Compare two scopes or scope with a collection of wrappers.
      # If other is a collection it performs the request to fetch
      # data from ES.
      #
      # @example
      #   PlacesIndex.limit(10) == PlacesIndex.limit(10) # => true
      #   PlacesIndex.limit(10) == PlacesIndex.limit(10).to_a # => true
      #   PlacesIndex.limit(10) == PlacesIndex.limit(10).objects # => true
      #
      #   PlacesIndex.limit(10) == UsersIndex.limit(10) # => false
      #   PlacesIndex.limit(10) == UsersIndex.limit(10).to_a # => false
      #
      #   PlacesIndex.limit(10) == Object.new # => false
      # @param other [Object] any object
      # @return [true, false] the result of comparison
      def ==(other)
        super || other.is_a?(Chewy::Search::Request) ? compare_internals(other) : to_a == other
      end

      # Access to ES response wrappers providing useful methods such as
      # {Chewy::Search::Response#total} or {Chewy::Search::Response#max_score}.
      #
      # @see Chewy::Search::Response
      # @return [Chewy::Search::Response] a response object instance
      def response
        @response ||= build_response(perform)
      end

      # Wraps and sets the raw Elasticsearch response to provide access
      # to convenience methods.
      #
      # @see Chewy::Search::Response
      # @param from_elasticsearch [Hash] An Elasticsearch response
      def response=(from_elasticsearch)
        @response = build_response(from_elasticsearch)
      end

      # ES request body
      #
      # @return [Hash] request body
      def render
        @render ||= parameters.render
      end

      # Includes the class name and the result of rendering.
      #
      # @return [String]
      def inspect
        "<#{self.class} #{render}>"
      end

      # @!group Chainable request modifications

      # @!method query(query_hash=nil, &block)
      #   Adds `query` parameter to the search request body.
      #
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-query.html
      #   @see Chewy::Search::Parameters::Query
      #   @return [Chewy::Search::Request, Chewy::Search::QueryProxy]
      #
      #   @overload query(query_hash)
      #     If pure hash is passed it goes straight to the `query` parameter storage.
      #     Acts exactly the same way as {Chewy::Search::QueryProxy#must}.
      #
      #     @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
      #     @example
      #       PlacesIndex.query(match: {name: 'Moscow'})
      #       # => <PlacesIndex::Query {..., :body=>{:query=>{:match=>{:name=>"Moscow"}}}}>
      #     @param query_hash [Hash] pure query hash
      #     @return [Chewy::Search::Request]
      #
      #   @overload query
      #     If block is passed instead of a pure hash, `elasticsearch-dsl"
      #     gem will be used to process it.
      #     Acts exactly the same way as {Chewy::Search::QueryProxy#must} with a block.
      #
      #     @see https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-dsl
      #     @example
      #       PlacesIndex.query { match name: 'Moscow' }
      #       # => <PlacesIndex::Query {..., :body=>{:query=>{:match=>{:name=>"Moscow"}}}}>
      #     @yield the block is processed by `elasticsearch-dsl` gem
      #     @return [Chewy::Search::Request]
      #
      #   @overload query
      #     If nothing is passed it returns a proxy for additional
      #     parameter manipulations.
      #
      #     @see Chewy::Search::QueryProxy
      #     @example
      #       PlacesIndex.query.should(match: {name: 'Moscow'}).query.not(match: {name: 'London'})
      #       # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #       #      :should=>{:match=>{:name=>"Moscow"}},
      #       #      :must_not=>{:match=>{:name=>"London"}}}}}}>
      #     @return [Chewy::Search::QueryProxy]
      #
      # @!method filter(query_hash=nil, &block)
      #   Adds `filter` context of the `query` parameter at the
      #   search request body.
      #
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html
      #   @see Chewy::Search::Parameters::Filter
      #   @return [Chewy::Search::Request, Chewy::Search::QueryProxy]
      #
      #   @overload filter(query_hash)
      #     If pure hash is passed it goes straight to the `filter` context of the `query` parameter storage.
      #     Acts exactly the same way as {Chewy::Search::QueryProxy#must}.
      #
      #     @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
      #     @example
      #       PlacesIndex.filter(match: {name: 'Moscow'})
      #       # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #       #      :filter=>{:match=>{:name=>"Moscow"}}}}}}>
      #     @param query_hash [Hash] pure query hash
      #     @return [Chewy::Search::Request]
      #
      #   @overload filter
      #     If block is passed instead of a pure hash, `elasticsearch-dsl"
      #     gem will be used to process it.
      #     Acts exactly the same way as {Chewy::Search::QueryProxy#must} with a block.
      #
      #     @see https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-dsl
      #     @example
      #       PlacesIndex.filter { match name: 'Moscow' }
      #       # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #       #      :filter=>{:match=>{:name=>"Moscow"}}}}}}>
      #     @yield the block is processed by `elasticsearch-dsl` gem
      #     @return [Chewy::Search::Request]
      #
      #   @overload filter
      #     If nothing is passed it returns a proxy for additional
      #     parameter manipulations.
      #
      #     @see Chewy::Search::QueryProxy
      #     @example
      #       PlacesIndex.filter.should(match: {name: 'Moscow'}).filter.not(match: {name: 'London'})
      #       # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #       #      :filter=>{:bool=>{:should=>{:match=>{:name=>"Moscow"}},
      #       #      :must_not=>{:match=>{:name=>"London"}}}}}}}}>
      #     @return [Chewy::Search::QueryProxy]
      #
      # @!method post_filter(query_hash=nil, &block)
      #   Adds `post_filter` parameter to the search request body.
      #
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#post-filter
      #   @see Chewy::Search::Parameters::PostFilter
      #   @return [Chewy::Search::Request, Chewy::Search::QueryProxy]
      #
      #   @overload post_filter(query_hash)
      #     If pure hash is passed it goes straight to the `post_filter` parameter storage.
      #     Acts exactly the same way as {Chewy::Search::QueryProxy#must}.
      #
      #     @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
      #     @example
      #       PlacesIndex.post_filter(match: {name: 'Moscow'})
      #       # => <PlacesIndex::Query {..., :body=>{:post_filter=>{:match=>{:name=>"Moscow"}}}}>
      #     @param query_hash [Hash] pure query hash
      #     @return [Chewy::Search::Request]
      #
      #   @overload post_filter
      #     If block is passed instead of a pure hash, `elasticsearch-dsl"
      #     gem will be used to process it.
      #     Acts exactly the same way as {Chewy::Search::QueryProxy#must} with a block.
      #
      #     @see https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-dsl
      #     @example
      #       PlacesIndex.post_filter { match name: 'Moscow' }
      #       # => <PlacesIndex::Query {..., :body=>{:post_filter=>{:match=>{:name=>"Moscow"}}}}>
      #     @yield the block is processed by `elasticsearch-dsl` gem
      #     @return [Chewy::Search::Request]
      #
      #   @overload post_filter
      #     If nothing is passed it returns a proxy for additional
      #     parameter manipulations.
      #
      #     @see Chewy::Search::QueryProxy
      #     @example
      #       PlacesIndex.post_filter.should(match: {name: 'Moscow'}).post_filter.not(match: {name: 'London'})
      #       # => <PlacesIndex::Query {..., :body=>{:post_filter=>{:bool=>{
      #       #      :should=>{:match=>{:name=>"Moscow"}},
      #       #      :must_not=>{:match=>{:name=>"London"}}}}}}>
      #     @return [Chewy::Search::QueryProxy]
      %i[query filter post_filter].each do |name|
        define_method name do |query_hash = UNDEFINED, &block|
          if block || query_hash != UNDEFINED
            modify(name) { must(block || query_hash) }
          else
            Chewy::Search::QueryProxy.new(name, self)
          end
        end
      end

      # @!method order(*values)
      #   Modifies `sort` request parameter. Updates the storage on every call.
      #
      #   @example
      #     PlacesIndex.order(:name, population: {order: :asc}).order(:coordinates)
      #     # => <PlacesIndex::Query {..., :body=>{:sort=>["name", {"population"=>{:order=>:asc}}, "coordinates"]}}>
      #   @see Chewy::Search::Parameters::Order
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html
      #   @param values [Array<Hash, String, Symbol>] sort fields and options
      #   @return [Chewy::Search::Request]
      #
      # @!method docvalue_fields(*values)
      #   Modifies `docvalue_fields` request parameter. Updates the storage on every call.
      #
      #   @example
      #     PlacesIndex.docvalue_fields(:name).docvalue_fields(:population, :coordinates)
      #     # => <PlacesIndex::Query {..., :body=>{:docvalue_fields=>["name", "population", "coordinates"]}}>
      #   @see Chewy::Search::Parameters::DocvalueFields
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#docvalue-fields
      #   @param values [Array<String, Symbol>] field names
      #   @return [Chewy::Search::Request]
      %i[order docvalue_fields].each do |name|
        define_method name do |value, *values|
          modify(name) { update!([value, *values]) }
        end
      end

      # Modifies `index` request parameter. Updates the storage on every call.
      # Added passed indexes to the parameter list.
      #
      # @example
      #   UsersIndex.indices(CitiesIndex).indices(:another)
      #   # => <UsersIndex::Query {:index=>["another", "cities", "users"]}>
      # @see Chewy::Search::Parameters::Indices
      # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html
      # @param values [Array<Chewy::Index, String, Symbol>] index names
      # @return [Chewy::Search::Request]
      def indices(value, *values)
        modify(:indices) { update!(indices: [value, *values]) }
      end

      # @overload reorder(*values)
      #   Replaces the value of the `sort` parameter with the provided value.
      #
      #   @example
      #     PlacesIndex.order(:name, population: {order: :asc}).reorder(:coordinates)
      #     # => <PlacesIndex::Query {..., :body=>{:sort=>["coordinates"]}}>
      #   @see Chewy::Search::Parameters::Order
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html
      #   @param values [Array<Hash, String, Symbol>] sort fields and options
      #   @return [Chewy::Search::Request]
      def reorder(value, *values)
        modify(:order) { replace!([value, *values]) }
      end

      # @!method track_scores(value = true)
      #   Replaces the value of the `track_scores` parameter with the provided value.
      #
      #   @example
      #     PlacesIndex.track_scores
      #     # => <PlacesIndex::Query {..., :body=>{:track_scores=>true}}>
      #     PlacesIndex.track_scores.track_scores(false)
      #     # => <PlacesIndex::Query {:index=>["places"]}>
      #   @see Chewy::Search::Parameters::TrackScores
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_track_scores
      #   @param value [true, false]
      #   @return [Chewy::Search::Request]
      #
      # @!method track_total_hits(value = true)
      #   Replaces the value of the `track_total_hits` parameter with the provided value.
      #
      #   @example
      #     PlacesIndex.track_total_hits
      #     # => <PlacesIndex::Query {..., :body=>{:track_total_hits=>true}}>
      #     PlacesIndex.track_total_hits.track_total_hits(false)
      #     # => <PlacesIndex::Query {:index=>["places"]}>
      #   @see Chewy::Search::Parameters::TrackTotalHits
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-your-data.html#track-total-hits
      #   @param value [true, false]
      #   @return [Chewy::Search::Request]
      #
      # @!method explain(value = true)
      #   Replaces the value of the `explain` parameter with the provided value.
      #
      #   @example
      #     PlacesIndex.explain
      #     # => <PlacesIndex::Query {..., :body=>{:explain=>true}}>
      #     PlacesIndex.explain.explain(false)
      #     # => <PlacesIndex::Query {:index=>["places"]}>
      #   @see Chewy::Search::Parameters::Explain
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html
      #   @param value [true, false]
      #   @return [Chewy::Search::Request]
      #
      # @!method version(value = true)
      #   Replaces the value of the `version` parameter with the provided value.
      #
      #   @example
      #     PlacesIndex.version
      #     # => <PlacesIndex::Query {..., :body=>{:version=>true}}>
      #     PlacesIndex.version.version(false)
      #     # => <PlacesIndex::Query {:index=>["places"]}>
      #   @see Chewy::Search::Parameters::Version
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html
      #   @param value [true, false]
      #   @return [Chewy::Search::Request]
      #
      # @!method profile(value = true)
      #   Replaces the value of the `profile` parameter with the provided value.
      #
      #   @example
      #     PlacesIndex.profile
      #     # => <PlacesIndex::Query {..., :body=>{:profile=>true}}>
      #     PlacesIndex.profile.profile(false)
      #     # => <PlacesIndex::Query {:index=>["places"]}>
      #   @see Chewy::Search::Parameters::Profile
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html
      #   @param value [true, false]
      #   @return [Chewy::Search::Request]
      #
      # @!method none(value = true)
      #   Enables `NullObject` pattern for the request, doesn't perform the
      #   request, `#hits` are empty, `#total` is 0, etc.
      #
      #   @example
      #     PlacesIndex.none.to_a
      #     # => []
      #     PlacesIndex.none.total
      #     # => 0
      #   @see Chewy::Search::Parameters::None
      #   @see https://en.wikipedia.org/wiki/Null_Object_pattern
      #   @param value [true, false]
      #   @return [Chewy::Search::Request]
      %i[track_scores track_total_hits explain version profile none].each do |name|
        define_method name do |value = true|
          modify(name) { replace!(value) }
        end
      end

      # @!method request_cache(value)
      #   Replaces the value of the `request_cache` parameter with the provided value.
      #   Unlike other boolean fields, the value have to be specified explicitly
      #   since it overrides the index-level setting.
      #
      #   @example
      #     PlacesIndex.request_cache(true)
      #     # => <PlacesIndex::Query {..., :body=>{:request_cache=>true}}>
      #     PlacesIndex.request_cache(false)
      #     # => <PlacesIndex::Query {..., :body=>{:request_cache=>false}}>
      #   @see Chewy::Search::Parameters::RequestCache
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/shard-request-cache.html#_enabling_and_disabling_caching_per_request
      #   @param value [true, false, nil]
      #   @return [Chewy::Search::Request]
      #
      # @!method search_type(value)
      #   Replaces the value of the `search_type` request part.
      #
      #   @example
      #     PlacesIndex.search_type(:dfs_query_then_fetch)
      #     # => <PlacesIndex::Query {..., :body=>{:search_type=>"dfs_query_then_fetch"}}>
      #   @see Chewy::Search::Parameters::SearchType
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-type
      #   @param value [String, Symbol]
      #   @return [Chewy::Search::Request]
      #
      # @!method preference(value)
      #   Replaces the value of the `preference` request part.
      #
      #   @example
      #     PlacesIndex.preference(:_primary_first)
      #     # => <PlacesIndex::Query {..., :body=>{:preference=>"_primary_first"}}>
      #   @see Chewy::Search::Parameters::Preference
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-preference
      #   @param value [String, Symbol]
      #   @return [Chewy::Search::Request]
      #
      # @!method timeout(value)
      #   Replaces the value of the `timeout` request part.
      #
      #   @example
      #     PlacesIndex.timeout('1m')
      #     <PlacesIndex::Query {..., :body=>{:timeout=>"1m"}}>
      #   @see Chewy::Search::Parameters::Timeout
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units
      #   @param value [String, Symbol]
      #   @return [Chewy::Search::Request]
      #
      # @!method limit(value)
      #   Replaces the value of the `size` request part.
      #
      #   @example
      #     PlacesIndex.limit(10)
      #     <PlacesIndex::Query {..., :body=>{:size=>10}}>
      #   @see Chewy::Search::Parameters::Limit
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
      #   @param value [String, Integer]
      #   @return [Chewy::Search::Request]
      #
      # @!method offset(value)
      #   Replaces the value of the `from` request part.
      #
      #   @example
      #     PlacesIndex.offset(10)
      #     <PlacesIndex::Query {..., :body=>{:from=>10}}>
      #   @see Chewy::Search::Parameters::Offset
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
      #   @param value [String, Integer]
      #   @return [Chewy::Search::Request]
      #
      # @!method terminate_after(value)
      #   Replaces the value of the `terminate_after` request part.
      #
      #   @example
      #     PlacesIndex.terminate_after(10)
      #     <PlacesIndex::Query {..., :body=>{:terminate_after=>10}}>
      #   @see Chewy::Search::Parameters::Offset
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-your-data.html#quickly-check-for-matching-docs
      #   @param value [String, Integer]
      #   @return [Chewy::Search::Request]
      #
      # @!method min_score(value)
      #   Replaces the value of the `min_score` request part.
      #
      #   @example
      #     PlacesIndex.min_score(2)
      #     <PlacesIndex::Query {..., :body=>{:min_score=>2.0}}>
      #   @see Chewy::Search::Parameters::Offset
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-api-min-score
      #   @param value [String, Integer, Float]
      #   @return [Chewy::Search::Request]
      #
      # @!method ignore_unavailable(value)
      #   Replaces the value of the `ignore_unavailable` request part.
      #
      #   @example
      #     PlacesIndex.ignore_unavailable(true)
      #     <PlacesIndex::Query {..., :ignore_unavailable => true, :body=>{ ... }}>
      #   @see Chewy::Search::Parameters::IgnoreUnavailable
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-index.html#multi-index
      #   @param value [true, false, nil]
      #   @return [Chewy::Search::Request]
      #
      # @!method collapse(value)
      #   Replaces the value of the `collapse` request part.
      #
      #   @example
      #     PlacesIndex.collapse(field: :name)
      #     # => <PlacesIndex::Query {..., :body=>{:collapse=>{"field"=>:name}}}>
      #   @see Chewy::Search::Parameters::Collapse
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/collapse-search-results.html
      #   @param value [Hash]
      #   @return [Chewy::Search::Request]
      #
      # @!method knn(value)
      #   Replaces the value of the `knn` request part.
      #
      #   @example
      #     PlacesIndex.knn(field: :vector, query_vector: [4, 2], k: 5, num_candidates: 50)
      #     # => <PlacesIndex::Query {..., :body=>{:knn=>{"field"=>:vector, "query_vector"=>[4, 2], "k"=>5, "num_candidates"=>50}}}>
      #   @see Chewy::Search::Parameters::Knn
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html
      #   @param value [Hash]
      #   @return [Chewy::Search::Request]
      %i[request_cache search_type preference timeout limit offset terminate_after min_score ignore_unavailable collapse knn].each do |name|
        define_method name do |value|
          modify(name) { replace!(value) }
        end
      end

      # @!method source(*values)
      #   Updates `_source` request part. Accepts either an array
      #   of field names/templates or a hash with `includes` and `excludes`
      #   keys. Source also can be disabled entirely or enabled again.
      #
      #   @example
      #     PlacesIndex.source(:name).source(includes: [:popularity], excludes: :description)
      #     # => <PlacesIndex::Query {..., :body=>{:_source=>{:includes=>["name", "popularity"], :excludes=>["description"]}}}>
      #     PlacesIndex.source(false)
      #     # => <PlacesIndex::Query {..., :body=>{:_source=>false}}>
      #   @see Chewy::Search::Parameters::Source
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering
      #   @param values [true, false, {Symbol => Array<String, Symbol>, String, Symbol}, Array<String, Symbol>, String, Symbol]
      #   @return [Chewy::Search::Request]
      #
      # @!method stored_fields(*values)
      #   Updates `stored_fields` request part. Accepts an array of field
      #   names. Can be entirely disabled and enabled back.
      #
      #   @example
      #     PlacesIndex.stored_fields(:name).stored_fields(:description)
      #     # => <PlacesIndex::Query {..., :body=>{:stored_fields=>["name", "description"]}}>
      #     PlacesIndex.stored_fields(false)
      #     # => <PlacesIndex::Query {..., :body=>{:stored_fields=>"_none_"}}>
      #   @see Chewy::Search::Parameters::StoredFields
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#stored-fields
      #   @param values [true, false, String, Symbol, Array<String, Symbol>]
      #   @return [Chewy::Search::Request]
      %i[source stored_fields].each do |name|
        define_method name do |value, *values|
          modify(name) { update!(values.empty? ? value : [value, *values]) }
        end
      end

      # @overload search_after(*values)
      # Replaces the storage value for `search_after` request part.
      #
      # @example
      #   PlacesIndex.search_after(42, 'Moscow').search_after('London')
      #   # => <PlacesIndex::Query {..., :body=>{:search_after=>["London"]}}>
      # @see Chewy::Search::Parameters::SearchAfter
      # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after
      # @param value [Array, Object]
      # @return [Chewy::Search::Request]
      def search_after(value, *values)
        modify(:search_after) { replace!(values.empty? ? value : [value, *values]) }
      end

      # Stores ORM/ODM objects loading options. Options
      # might be define per-index or be global, depends on the adapter
      # loading implementation. Also, there are 2 loading options to select
      # or exclude indexes from loading: `only` and `except` respectively.
      # Options are updated on further method calls.
      #
      # @example
      #   PlaceIndex.load(only: 'city').load(scope: -> { active })
      # @see Chewy::Search::Loader
      # @see Chewy::Search::Response#objects
      # @see Chewy::Search::Scrolling#scroll_objects
      # @param options [Hash] adapter-specific loading options
      def load(options = nil)
        modify(:load) { update!(options) }
      end

      # @!method script_fields(value)
      #   Add a `script_fields` part to the request. Further
      #   call values are merged to the storage hash.
      #
      #   @example
      #     PlacesIndex
      #       .script_fields(field1: {script: {lang: 'painless', inline: 'some script here'}})
      #       .script_fields(field2: {script: {lang: 'painless', inline: 'some script here'}})
      #     # => <PlacesIndex::Query {..., :body=>{:script_fields=>{
      #     #      "field1"=>{:script=>{:lang=>"painless", :inline=>"some script here"}},
      #     #      "field2"=>{:script=>{:lang=>"painless", :inline=>"some script here"}}}}}>
      #   @see Chewy::Search::Parameters::ScriptFields
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#script-fields
      #   @param value [Hash]
      #   @return [Chewy::Search::Request]
      #
      # @!method indices_boost(value)
      #   Add an `indices_boost` part to the request. Further
      #   call values are merged to the storage hash.
      #
      #   @example
      #     PlacesIndex.indices_boost(index1: 1.2, index2: 1.3).indices_boost(index1: 1.5)
      #     # => <PlacesIndex::Query {..., :body=>{:indices_boost=>[{"index2"=>1.3}, {"index1"=>1.5}]}}>
      #   @see Chewy::Search::Parameters::IndicesBoost
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multiple-indices.html#index-boost
      #   @param value [{String, Symbol => String, Integer, Float}]
      #   @return [Chewy::Search::Request]
      #
      # @!method rescore(value)
      #   Add a `rescore` part to the request. Further
      #   call values are added to the storage array.
      #
      #   @example
      #     PlacesIndex.rescore(window_size: 100, query: {}).rescore(window_size: 200, query: {})
      #     # => <PlacesIndex::Query {..., :body=>{:rescore=>[{:window_size=>100, :query=>{}}, {:window_size=>200, :query=>{}}]}}>
      #   @see Chewy::Search::Parameters::Rescore
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#rescore
      #   @param value [Hash, Array<Hash>]
      #   @return [Chewy::Search::Request]
      #
      # @!method highlight(value)
      #   Add a `highlight` configuration to the request. Further
      #   call values are merged to the storage hash.
      #
      #   @example
      #     PlacesIndex
      #       .highlight(fields: {description: {type: 'plain'}})
      #       .highlight(pre_tags: ['<em>'], post_tags: ['</em>'])
      #     # => <PlacesIndex::Query {..., :body=>{:highlight=>{
      #     #      "fields"=>{:description=>{:type=>"plain"}},
      #     #      "pre_tags"=>["<em>"], "post_tags"=>["</em>"]}}}>
      #   @see Chewy::Search::Parameters::Highlight
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html
      #   @param value [Hash]
      #   @return [Chewy::Search::Request]
      %i[script_fields indices_boost rescore highlight].each do |name|
        define_method name do |value|
          modify(name) { update!(value) }
        end
      end

      # A dual-purpose method.
      #
      # @overload suggest(value)
      #   With the value provided it adds a new suggester
      #   to the suggestion hash.
      #
      #   @example
      #     PlacesIndex
      #       .suggest(names: {text: 'tring out Elasticsearch'})
      #       .suggest(descriptions: {text: 'some other text'})
      #     # => <PlacesIndex::Query {..., :body=>{:suggest=>{
      #     #      "names"=>{:text=>"tring out Elasticsearch"},
      #     #      "descriptions"=>{:text=>"some other text"}}}}>
      #   @see Chewy::Search::Parameters::Suggest
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
      #   @param value [Hash]
      #   @return [Chewy::Search::Request]
      #
      # @overload suggest
      #   Without value provided, it performs the request and
      #   returns {Chewy::Search::Response#suggest} contents.
      #
      #   @example
      #     PlacesIndex.suggest(names: {text: 'tring out Elasticsearch'}).suggest
      #   @see Chewy::Search::Response#suggest
      #   @return [Hash]
      def suggest(value = UNDEFINED)
        if value == UNDEFINED
          response.suggest
        else
          modify(:suggest) { update!(value) }
        end
      end

      # A dual-purpose method.
      #
      # @overload aggs(value)
      #   With the value provided it adds a new aggregation
      #   to the aggregation hash.
      #
      #   @example
      #     PlacesIndex
      #       .aggs(avg_population: {avg: {field: :population}})
      #       .aggs(avg_age: {avg: {field: :age}})
      #     # => <PlacesIndex::Query {..., :body=>{:aggs=>{
      #     #      "avg_population"=>{:avg=>{:field=>:population}},
      #     #      "avg_age"=>{:avg=>{:field=>:age}}}}}>
      #   @see Chewy::Search::Parameters::Aggs
      #   @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
      #   @param value [Hash]
      #   @return [Chewy::Search::Request]
      #
      # @overload aggs
      #   Without value provided, it performs the request and
      #   returns {Chewy::Search::Response#aggs} contents.
      #
      #   @example
      #     PlacesIndex.aggs(avg_population: {avg: {field: :population}}).aggs
      #   @see Chewy::Search::Response#aggs
      #   @return [Hash]
      def aggs(value = UNDEFINED)
        if value == UNDEFINED
          response.aggs
        else
          modify(:aggs) { update!(value) }
        end
      end
      alias_method :aggregations, :aggs

      # @!group Scopes manipulation

      # Merges 2 scopes by merging their parameters.
      #
      # @example
      #   scope1 = PlacesIndex.limit(10).offset(10)
      #   scope2 = PlacesIndex.limit(20)
      #   scope1.merge(scope2)
      #   # => <PlacesIndex::Query {..., :body=>{:size=>20, :from=>10}}>
      #   scope2.merge(scope1)
      #   # => <PlacesIndex::Query {..., :body=>{:size=>10, :from=>10}}>
      # @see Chewy::Search::Parameters#merge
      # @param other [Chewy::Search::Request] scope to merge
      # @return [Chewy::Search::Request] new scope
      def merge(other)
        chain { parameters.merge!(other.parameters) }
      end

      # @!method and(other)
      #   Takes `query`, `filter`, `post_filter` from the passed scope
      #   and performs {Chewy::Search::QueryProxy#and} operation for each
      #   of them. Unlike merge, every other parameter is kept unmerged
      #   (values from the first scope are used in the result scope).
      #
      #   @see Chewy::Search::QueryProxy#and
      #   @example
      #     scope1 = PlacesIndex.filter(term: {name: 'Moscow'}).query(match: {name: 'London'})
      #     scope2 = PlacesIndex.filter.not(term: {name: 'Berlin'}).query(match: {name: 'Washington'})
      #     scope1.and(scope2)
      #     # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #     #      :must=>[{:match=>{:name=>"London"}}, {:match=>{:name=>"Washington"}}],
      #     #      :filter=>{
      #     #        :bool=>{:must=>[{:term=>{:name=>"Moscow"}}, {:bool=>{:must_not=>{:term=>{:name=>"Berlin"}}}}]}
      #     #      }
      #     #    }}}}>
      #   @param other [Chewy::Search::Request] scope to merge
      #   @return [Chewy::Search::Request] new scope
      #
      # @!method or(other)
      #   Takes `query`, `filter`, `post_filter` from the passed scope
      #   and performs {Chewy::Search::QueryProxy#or} operation for each
      #   of them. Unlike merge, every other parameter is kept unmerged
      #   (values from the first scope are used in the result scope).
      #
      #   @see Chewy::Search::QueryProxy#or
      #   @example
      #     scope1 = PlacesIndex.filter(term: {name: 'Moscow'}).query(match: {name: 'London'})
      #     scope2 = PlacesIndex.filter.not(term: {name: 'Berlin'}).query(match: {name: 'Washington'})
      #     scope1.or(scope2)
      #     # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #     #      :should=>[{:match=>{:name=>"London"}}, {:match=>{:name=>"Washington"}}],
      #     #      :filter=>{
      #     #        :bool=>{:should=>[{:term=>{:name=>"Moscow"}}, {:bool=>{:must_not=>{:term=>{:name=>"Berlin"}}}}]}
      #     #      }
      #     #    }}}}>
      #   @param other [Chewy::Search::Request] scope to merge
      #   @return [Chewy::Search::Request] new scope
      #
      # @!method not(other)
      #   Takes `query`, `filter`, `post_filter` from the passed scope
      #   and performs {Chewy::Search::QueryProxy#not} operation for each
      #   of them. Unlike merge, every other parameter is kept unmerged
      #   (values from the first scope are used in the result scope).
      #
      #   @see Chewy::Search::QueryProxy#not
      #   @example
      #     scope1 = PlacesIndex.filter(term: {name: 'Moscow'}).query(match: {name: 'London'})
      #     scope2 = PlacesIndex.filter.not(term: {name: 'Berlin'}).query(match: {name: 'Washington'})
      #     scope1.not(scope2)
      #     # => <PlacesIndex::Query {..., :body=>{:query=>{:bool=>{
      #     #      :must=>{:match=>{:name=>"London"}}, :must_not=>{:match=>{:name=>"Washington"}},
      #     #      :filter=>{
      #     #        :bool=>{
      #     #          :must=>{:term=>{:name=>"Moscow"}},
      #     #          :must_not=>{:bool=>{:must_not=>{:term=>{:name=>"Berlin"}}}}
      #     #        }
      #     #      }
      #     #    }}}}>
      #   @param other [Chewy::Search::Request] scope to merge
      #   @return [Chewy::Search::Request] new scope
      %i[and or not].each do |name|
        define_method name do |other|
          %i[query filter post_filter].inject(self) do |scope, parameter_name|
            scope.send(parameter_name).send(name, other.parameters[parameter_name].value)
          end
        end
      end

      # Returns a new scope containing only specified storages.
      #
      # @example
      #   PlacesIndex.limit(10).offset(10).order(:name).only(:offset, :order)
      #   # => <PlacesIndex::Query {..., :body=>{:from=>10, :sort=>["name"]}}>
      # @param values [Array<String, Symbol>]
      # @return [Chewy::Search::Request] new scope
      def only(*values)
        chain { parameters.only!(values.flatten(1) + [:indices]) }
      end

      # Returns a new scope containing all the storages except specified.
      #
      # @example
      #   PlacesIndex.limit(10).offset(10).order(:name).except(:offset, :order)
      #   # => <PlacesIndex::Query {..., :body=>{:size=>10}}>
      # @param values [Array<String, Symbol>]
      # @return [Chewy::Search::Request] new scope
      def except(*values)
        chain { parameters.except!(values.flatten(1)) }
      end

      # @!group Additional actions

      # Returns total count of hits for the request. If the request
      # was already performed - it uses the `total` value, otherwise
      # it executes a fast count request.
      #
      # @return [Integer] total hits count
      def count
        if performed?
          total
        else
          Chewy.client.count(only(WHERE_STORAGES).render)['count']
        end
      rescue Elasticsearch::Transport::Transport::Errors::NotFound
        0
      end

      # Checks if any of the document exist for this request. If
      # the request was already performed - it uses the `total`,
      # otherwise it executes a fast request to check existence.
      #
      # @return [true, false] wether hits exist or not
      def exists?
        if performed?
          total != 0
        else
          limit(0).terminate_after(1).total != 0
        end
      end
      alias_method :exist?, :exists?

      # Return first wrapper object or a collection of first N wrapper
      # objects if the argument is provided.
      # Tries to use cached results of possible. If the amount of
      # cached results is insufficient - performs a new request.
      #
      # @overload first
      #   If nothing is passed - it returns a single object.
      #
      #   @return [Chewy::Index] result document
      #
      # @overload first(limit)
      #   If limit is provided - it returns the limit amount or less
      #   of wrapper objects.
      #
      #   @param limit [Integer] amount of requested results
      #   @return [Array<Chewy::Index>] result document collection
      def first(limit = UNDEFINED)
        request_limit = limit == UNDEFINED ? 1 : limit

        if performed? && (request_limit <= size || size == total)
          limit == UNDEFINED ? wrappers.first : wrappers.first(limit)
        else
          result = except(EXTRA_STORAGES).limit(request_limit).to_a
          limit == UNDEFINED ? result.first : result
        end
      end

      # Finds documents with specified ids for the current request scope.
      #
      # @raise [Chewy::DocumentNotFound] in case of any document is missing
      # @overload find(id)
      #   If single id is passed - it returns a single object.
      #
      #   @param id [Integer, String] id of the desired document
      #   @return [Chewy::Index] result document
      #
      # @overload find(*ids)
      #   If several field are passed - it returns an array of wrappers.
      #   Respect the amount of passed ids and if it is more than the default
      #   batch size - uses scroll API to retrieve everything.
      #
      #   @param ids [Array<Integer, String>] ids of the desired documents
      #   @return [Array<Chewy::Index>] result documents
      def find(*ids)
        return super if block_given?

        ids = ids.flatten(1).map(&:to_s)
        scope = except(EXTRA_STORAGES).filter(ids: {values: ids})

        results = if ids.size > DEFAULT_BATCH_SIZE
          scope.scroll_wrappers
        else
          scope.limit(ids.size)
        end.to_a

        if ids.size != results.size
          missing_ids = ids - results.map(&:id).map(&:to_s)
          raise Chewy::DocumentNotFound, "Could not find documents for ids: #{missing_ids.to_sentence}"
        end
        results.one? ? results.first : results
      end

      # Returns and array of values for specified fields.
      # Uses `source` to restrict the list of returned fields.
      # Fields `_id`, `_type`, `_routing` and `_index` are also supported.
      #
      # @overload pluck(field)
      #   If single field is passed - it returns and array of values.
      #
      #   @param field [String, Symbol] field name
      #   @return [Array<Object>] specified field values
      #
      # @overload pluck(*fields)
      #   If several field are passed - it returns an array of arrays of values.
      #
      #   @param fields [Array<String, Symbol>] field names
      #   @return [Array<Array<Object>>] specified field values
      def pluck(*fields)
        fields = fields.flatten(1).reject(&:blank?).map(&:to_s)

        source_fields = fields - EVERFIELDS
        scope = except(FIELD_STORAGES, EXTRA_STORAGES)
          .source(source_fields.presence || false)

        hits = raw_limit_value ? scope.hits : scope.scroll_hits(batch_size: DEFAULT_PLUCK_BATCH_SIZE)
        hits.map do |hit|
          if fields.one?
            fetch_field(hit, fields.first)
          else
            fields.map do |field|
              fetch_field(hit, field)
            end
          end
        end
      end

      # Deletes all the documents from the specified scope it uses
      # `delete_by_query`
      #
      # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html
      # @note The result hash is different for different API used.
      # @param refresh [true, false] Refreshes all shards involved in the delete by query
      # @param wait_for_completion [true, false] wait for request completion or run it asynchronously
      #    and return task reference at `.tasks/task/${taskId}`.
      # @param requests_per_second [Float] The throttle for this request in sub-requests per second
      # @param scroll_size [Integer] Size of the scroll request that powers the operation

      # @return [Hash] the result of query execution
      def delete_all(refresh: true, wait_for_completion: nil, requests_per_second: nil, scroll_size: nil)
        request_body = only(WHERE_STORAGES).render.merge(
          {
            refresh: refresh,
            wait_for_completion: wait_for_completion,
            requests_per_second: requests_per_second,
            scroll_size: scroll_size
          }.compact
        )
        ActiveSupport::Notifications.instrument 'delete_query.chewy', notification_payload(request: request_body) do
          request_body[:body] = {query: {match_all: {}}} if request_body[:body].empty?
          Chewy.client.delete_by_query(request_body)
        end
      end

      # Returns whether or not the query has been performed.
      #
      # @return [true, false]
      def performed?
        !@response.nil?
      end

    protected

      def initialize_clone(origin)
        @parameters = origin.parameters.clone
        reset
      end

    private

      def build_response(raw_response)
        Response.new(raw_response, loader, collection_paginator)
      end

      def compare_internals(other)
        parameters == other.parameters
      end

      def modify(name, &block)
        chain { parameters.modify!(name, &block) }
      end

      def chain(&block)
        clone.tap { |r| r.instance_exec(&block) }
      end

      def reset
        @response, @render, @loader = nil
      end

      def perform(additional = {})
        request_body = render.merge(additional)
        ActiveSupport::Notifications.instrument 'search_query.chewy', notification_payload(request: request_body) do
          Chewy.client.search(request_body)
        rescue Elasticsearch::Transport::Transport::Errors::NotFound
          {}
        end
      end

      def notification_payload(additional)
        {
          indexes: _indices,
          index: _indices.one? ? _indices.first : _indices
        }.merge(additional)
      end

      def _indices
        parameters[:indices].indices
      end

      def raw_limit_value
        parameters[:limit].value
      end

      def raw_offset_value
        parameters[:offset].value
      end

      def loader
        @loader ||= Loader.new(
          indexes: parameters[:indices].indices,
          **parameters[:load].value
        )
      end

      def fetch_field(hit, field)
        if EVERFIELDS.include?(field)
          hit[field]
        else
          hit.fetch('_source', {})[field]
        end
      end

      def collection_paginator
        method(:paginated_collection).to_proc if respond_to?(:paginated_collection, true)
      end
    end
  end
end