ankane/searchkick · error · Searchkick::Error

Pass `scroll` option to the search method for scrolling

Error message

Pass `scroll` option to the search method for scrolling

What it means

Searchkick's Results#scroll raises this when the result set has no scroll cursor. Elasticsearch scroll requires a keep-alive window passed to the original search call; only then does the response include `_scroll_id` (checked at results.rb:172-177). Calling `.scroll` on a query that was issued without the `scroll:` option has no cursor to continue, so Searchkick raises instead of sending an invalid scroll request.

Source

Thrown at lib/searchkick/results.rb:177

    def with_score
      return enum_for(:with_score) unless block_given?

      with_hit.each do |result, hit|
        yield result, hit["_score"]
      end
    end

    def misspellings?
      @options[:misspellings]
    end

    def scroll_id
      @response["_scroll_id"]
    end

    def scroll
      raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id

      if block_given?
        records = self
        while records.any?
          yield records
          records = records.scroll
        end

        records.clear_scroll
      else
        begin
          # TODO Active Support notifications for this scroll call
          params = {
            scroll: options[:scroll],
            body: {scroll_id: scroll_id}
          }
          Searchkick.add_opaque_id(params, options[:opaque_id]) if options[:opaque_id]
          Results.new(@klass, Searchkick.client.scroll(params), @options)

View on GitHub (pinned to 93e901a75b)

Solutions

  1. Pass the `scroll:` option to the search method itself: `Product.search('*', scroll: '1m')` - the response then carries `_scroll_id` and `.scroll` works
  2. Use the block form `Product.search('*', scroll: '1m').scroll { |batch| ... }` so batches are drained and the scroll context is cleared automatically
  3. If you only need pages rather than a stable cursor, keep using `page:`/`per_page:` instead of scroll

Example fix

# before
results = Product.search("milk")
next_batch = results.scroll # raises: no _scroll_id in the response

# after
results = Product.search("milk", scroll: "1m")
next_batch = results.scroll
# or let Searchkick drain and clear the cursor:
Product.search("milk", scroll: "1m").scroll { |batch| process(batch) }
Defensive patterns

Strategy: validation

Validate before calling

# Route every full-dataset iteration through one helper that always issues the option
def search_scroll_all(model, term = "*", window: "1m", &block)
  model.search(term, scroll: window).scroll(&block)
end

# or check the public cursor accessor before scrolling:
results = Product.search("*")
results.scroll { |b| process(b) } if results.scroll_id

Try / catch

begin
  batch = results.scroll
rescue Searchkick::Error => e
  raise unless e.message.start_with?("Pass `scroll` option")
  batch = Product.search(term, scroll: "1m") # reissue the search with scrolling enabled
end

Prevention

When it happens

Trigger: Calling `.scroll` on results of a search that lacked the option, e.g. `Product.search('*').scroll`. Passing `scroll:` as an argument to `.scroll` itself instead of to the `search` method. Copying scroll-iteration code onto a normal paginated search result.

Common situations: Adopting scroll for the first time from the Searchkick docs and putting the option on the wrong call; converting existing page/per_page code to scroll iteration; invoking the block form `results.scroll { |batch| ... }` on a non-scroll result.

Related errors


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