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"
endView on GitHub (pinned to 93e901a75b)
Solutions
- Remove the listed options and translate their intent into the raw body DSL (bool filter for `where`, sort for `order`, aggs for `aggs`).
- 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:`.
- If you need both generated and raw pieces, use `body_options:` (merged into the generated payload) instead of `body:`.
- 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 adopting `body:`, delete option-based query shaping from that call in the same edit — don't leave dead options.
- Use `body_options:` when you only need to merge extra payload keys into a generated query.
- Remember `load`, `page/per_page`, `includes`, `index_name` remain valid alongside `body:`.
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
- Multiple clients found - set Searchkick.client_type = :elast
- No client found - install the `elasticsearch` or `opensearch
- The `elasticsearch` gem must be 8+
- Use Searchkick.search to search multiple models
- Need primary key to load records
AI-assisted analysis of ankane/searchkick@93e901a75b (2026-08-21).
Data as JSON: /api/errors/7cffe8e2c9bceb3c.
Report an issue: GitHub.