How to Build a Paginated Search Page

Many projects require a search page with a list of results, the total number of hits, and pagination.

Build a simple search page

Let's walk through this: Suppose you have a SearchPage to render your results. The simplest controller to do this is:

Copy
class SearchPageController < CmsController
  def index
    @query = params[:q] || ''
    if @query.present?
      @hits = Obj.where(:*, :contains_prefix, @query).take(10)
    else
      @hits = []
    end
  end
end

The conditional assignment to @hits is necessary for two reasons:

  1. Obj.where raises an exception if @query is empty
  2. The @hits instance variable is needed in the view

The corresponding view, app/views/search_page/index.html.erb:

Copy
<%= form_tag(scrivito_path(@obj)) do -%>
  <div>
    <%= text_field_tag 'q', @query, placeholder: 'Search' %>
    <%= button_tag 'Search', class: 'web_button green' -%>
  </div>
<% end -%>
<% if @query.present? && @hits.empty? -%>
  <h2>No Search Results</h2>
<% else -%>
  <% @hits.each do |hit| -%>
    <h3 class="search_hit">
      <%= link_to(hit[:title], scrivito_path(hit)) %>
    </h3>
  <% end -%>
<% end -%>

You now have a search that returns the first 10 results on a page. This is a great start but not very useful. Let's expand this and add a simple pagination.

Add pagination

Change your controller code to this:

Copy
class SearchPageController < CmsController
  HITS_PER_PAGE = 10

  def index
    @query = params[:q] || ''
    @hits = []

    if @query.present?
      search_query = Obj.where(:*, :contains_prefix, @query)
      search_query.batch_size(HITS_PER_PAGE).offset(offset)

      @hits = search_query.take(HITS_PER_PAGE)
      @total = search_query.size

      if offset > 0
        @previous_page = scrivito_path(@obj, q: @query, offset: offset - HITS_PER_PAGE)
      end

      if @total > offset + HITS_PER_PAGE
        @next_page = scrivito_path(@obj, q: @query, offset: offset + HITS_PER_PAGE)
      end
    end
  end

  private

  def offset
    params[:offset].to_i
  end
end

The batch_size is set explicitly here although it defaults to 10. Keeping the batch size in sync with the number of hits on a page ensures stable performance for more hits on a page. If you increase HITS_PER_PAGE but leave the batch_size at 10, the SDK needs more than one round trip to load those hits, which degrades performance.

Append these lines to the view:
Copy
<%= link_to 'previous page', @previous_page if @previous_page %>
<%= link_to 'next page', @next_page if @next_page %>

And you can also add the number of hits to the view, right above where the hits are iterated over:

Copy
<h2>Your search for "<%= h(@query) %>" turned up <%= @total %> results</h2>

You now have a search page with a summary at the top and with links to navigate through the results at the bottom.

Things you should keep in mind

ObjSearchEnumerator includes the Enumerable mixin and make use of methods like take, map or select.

Note that using load_batch directly may retrieve fewer items than expected in case your rate limit is exhausted. It can be used for special optimizations but is usually not needed on a day-to-day basis.

If you want to get all CMS objects at once using to_a, make sure you call batch_size with a sufficiently high value. Otherwise, the SDK is forced to do more round-trips than necessary.