ankane/searchkick · error · ArgumentError

unsupported keywords: #{options.keys.map(&:inspect).join(",

Error message

unsupported keywords: #{options.keys.map(&:inspect).join(", ")}

What it means

When a where value falls through to a term filter, Searchkick checks that value.as_json is a scalar. If it is still an Enumerable (a Set, a custom object serializing to an array/hash, etc.), Elasticsearch could not term-match it, so Searchkick raises TypeError with the Active Record-style message "can't cast Set". Arrays are auto-converted to a terms (in) query earlier, which is why sets and other enumerables hit this instead.

Source

Thrown at lib/searchkick/index.rb:239

        return reindex_records(object, method_name: method_name, ignore_missing: ignore_missing, **options)
      end

      if !object.respond_to?(:searchkick_klass)
        raise Error, "Cannot reindex object"
      end

      scoped = Searchkick.relation?(object)
      # call searchkick_klass for inheritance
      relation = scoped ? object.all : Searchkick.scope(object.searchkick_klass).all

      refresh = options.fetch(:refresh, !scoped)
      options.delete(:refresh)

      if method_name || (scoped && !full)
        mode = options.delete(:mode) || :inline
        scope = options.delete(:scope)
        job_options = options.delete(:job_options)
        raise ArgumentError, "unsupported keywords: #{options.keys.map(&:inspect).join(", ")}" if options.any?

        # import only
        import_scope(relation, method_name: method_name, mode: mode, scope: scope, ignore_missing: ignore_missing, job_options: job_options)
        self.refresh if refresh
        true
      else
        async = options.delete(:async)
        if async
          if async.is_a?(Hash) && async[:wait]
            Searchkick.warn "async option is deprecated - use mode: :async, wait: true instead"
            options[:wait] = true unless options.key?(:wait)
          else
            Searchkick.warn "async option is deprecated - use mode: :async instead"
          end
          options[:mode] ||= :async
        end

        full_reindex(relation, **options)

View on GitHub (pinned to 93e901a75b)

Solutions

  1. Convert collections explicitly: where: {status: set.to_a} - Arrays become a terms/in query automatically
  2. Or use the explicit operator: where: {status: {in: [...]}}
  3. For custom value objects, pass the primitive (e.g. money.amount) instead of the wrapper

Example fix

# before
Product.search('*', where: {status: Set.new(['new', 'used'])})
# => TypeError: can't cast Set

# after
Product.search('*', where: {status: Set.new(['new', 'used']).to_a})
Defensive patterns

Strategy: type-guard

Type guard

def term_value(v)
  v = v.to_a if v.is_a?(Set)
  raise TypeError, "can't cast #{v.class.name}" if v.as_json.is_a?(Enumerable)
  v
end

Product.search('*', where: {status: term_value(input)})

Try / catch

begin
  Product.search('*', where: filters)
rescue TypeError => e
  raise unless e.message.start_with?("can't cast")
  filters.each { |k, v| filters[k] = v.to_a if v.is_a?(Set) }
  retry
end

Prevention

When it happens

Trigger: Product.search('*', where: {status: Set.new(['a', 'b'])}) - Sets are not Arrays so they skip the automatic {in: value} conversion; passing a Struct or any object whose as_json returns an Array/Hash as a term value.

Common situations: Using Set for tag/category filters; passing Money/Struct/value objects or ActionController::Parameters where a scalar is expected; params that arrive hash-like instead of as a plain value.

Related errors


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