ankane/searchkick · error · ArgumentError

Options incompatible with body option: #{ignored_options.joi

Error message

Options incompatible with body option: #{ignored_options.join(", ")}

What it means

`body:` replaces the entire query payload searchkick would generate, so query-shaping options would be silently ignored — for example `where`, `order`, `aggs`, `boost_by`, `misspellings`, `fields`, `operator`, `highlight`. To prevent silent no-ops, `Query#prepare` intersects your option keys with that list and raises `ArgumentError` listing any incompatible ones when `body:` is present.

Source

Thrown at lib/searchkick/query.rb:271

      max_result_window = searchkick_options[:max_result_window]
      original_per_page = per_page
      if max_result_window
        offset = max_result_window if offset > max_result_window
        per_page = max_result_window - offset if offset + per_page > max_result_window
      end

      # model and eager loading
      load = options[:load].nil? ? true : options[:load]

      all = term == "*"

      @json = options[:body]
      if @json
        ignored_options = options.keys & [:aggs, :boost,
          :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :exclude, :explain,
          :fields, :highlight, :indices_boost, :match, :misspellings, :operator, :order,
          :profile, :select, :smart_aggs, :suggest, :where]
        raise ArgumentError, "Options incompatible with body option: #{ignored_options.join(", ")}" if ignored_options.any?
        payload = @json
      else
        must_not = []
        should = []

        if options[:similar]
          like = options[:similar] == true ? term : options[:similar]
          query = {
            more_like_this: {
              like: like,
              min_doc_freq: 1,
              min_term_freq: 1,
              analyzer: "searchkick_search2"
            }
          }
          if fields.all? { |f| f.start_with?("*.") }
            raise ArgumentError, "Must specify fields to search"
          end

View on GitHub (pinned to 93e901a75b)

Solutions

  1. Remove the listed options and translate their intent into the raw body DSL (bool filter for `where`, sort for `order`, aggs for `aggs`).
  2. Keep the parts that are compatible — `load`, `includes`, `page/per_page/limit/offset`, `index_name`, `models`, `request_params`, `scroll` are not on the forbidden list and still work with `body:`.
  3. If you need both generated and raw pieces, use `body_options:` (merged into the generated payload) instead of `body:`.
  4. Write the body in a named scope/method so the search call stays option-free and greppable.

Example fix

# before
Product.search("milk",
  body: {query: {match_all: {}}, sort: [{price: "asc"}]},
  where: {in_stock: true}, order: {price: :asc}
) # => ArgumentError: Options incompatible with body option: order, where

# after
Product.search(nil,
  body: {
    query: {bool: {must: {match_all: {}}, filter: {term: {"in_stock": true}}}},
    sort: [{price: "asc"}]
  }
)
Defensive patterns

Strategy: validation

Validate before calling

FORBIDDEN_WITH_BODY = %i[aggs boost boost_by boost_by_distance boost_by_recency boost_where conversions conversions_term exclude explain fields highlight indices_boost match misspellings operator order profile select smart_aggs suggest where].freeze

def raw_search(term, body:, **opts)
  bad = opts.keys & FORBIDDEN_WITH_BODY
  raise ArgumentError, "not allowed with body: #{bad.join(', ')}" if bad.any?
  Product.search(term, body: body, **opts)
end

Type guard

def body_safe_options?(opts)
  forbidden = %i[aggs boost boost_by boost_by_distance boost_by_recency boost_where conversions conversions_term exclude explain fields highlight indices_boost match misspellings operator order profile select smart_aggs suggest where]
  (opts.keys & forbidden).empty?
end

Prevention

When it happens

Trigger: `Product.search("milk", body: {query: {...}}, where: {in_stock: true}, order: {price: :asc})` — the `where`/`order` keys trip the check. Also passing `fields:` or `misspellings:` alongside a hand-written body, or wrapping an existing search call with `body:` while leaving old options in place.

Common situations: Graduating from option-based queries to raw ES DSL for one complex query and forgetting to remove the now-dead options; merging code where a `body:` branch is added around an option-heavy call; copy-pasting a raw query from Kibana/Curl into `.search(body: ...)` while keeping surrounding options.

Related errors


AI-assisted analysis of ankane/searchkick@93e901a75b (2026-08-21). Data as JSON: /api/errors/7cffe8e2c9bceb3c. Report an issue: GitHub.