Installation
# Gemfilegem 'esse'gem 'will_paginate'gem 'esse-will_paginate'On load the gem includes Esse::WillPaginate::Pagination::SearchQuery into Esse::Search::Query. Nothing else to configure.
Paginating a search
query = UsersIndex .search(body: { query: { match_all: {} } }) .paginate(page: params[:page], per_page: 20)
query.limit_value # => 20query.offset_value # => (page - 1) * 20paginate(options = {}) accepts:
:page— 1-indexed page number (defaults to1).:per_page— items per page (defaults toWillPaginate.per_page).
It delegates to .limit(per_page).offset((page - 1) * per_page), so the underlying ES request receives standard from / size params.
Passing from / size directly
If you already carry pagination state in the query body, skip .paginate — paginated_results reads limit_value / offset_value straight from the query:
query = UsersIndex.search(body: { query: { match: { name: 'john' } }, from: 20, size: 10 })query.paginated_results.current_page # => 3Rendering with WillPaginate helpers
paginated_results returns a WillPaginate::Collection so all standard helpers work out of the box:
<%= will_paginate @search.paginated_results %><% @search.paginated_results.each do |hit| %> <li><%= hit.dig('_source', 'name') %></li><% end %>Note that the collection holds the raw ES hit hashes — use hit['_source'] to reach your document fields.
Multiple indices
Esse.cluster.search(...) returns the same query class, so .paginate works across indices:
query = Esse.cluster .search(CitiesIndex, CountiesIndex, body: { query: { match: { name: 'river' } } }) .paginate(page: params[:page], per_page: 15)
query.paginated_resultsWillPaginate configuration
Standard WillPaginate defaults apply — set them wherever your app already configures WillPaginate:
WillPaginate.per_page = 25Patterns
Search service with pagination
class UserSearch def initialize(q:, page: 1, per_page: 20) @q, @page, @per_page = q, page, per_page end
def call UsersIndex .search(body: body) .paginate(page: @page, per_page: @per_page) end
private
def body { query: { multi_match: { query: @q, fields: %w[name email] } }, sort: [{ created_at: 'desc' }], } endend
result = UserSearch.new(q: 'john', page: params[:page]).callresult.paginated_resultsIterating all results
For iterating every document, prefer .scroll_hits / .search_after_hits from core Esse — pagination is for user-facing pages. ES by default caps from + size at 10k.
UsersIndex.search(body: { query: { match_all: {} } }).scroll_hits(batch_size: 1_000) do |batch| batch.each { |hit| ... }end