Site search on Core dna is an Elasticsearch index that the platform fills and queries, and a page you build. What reaches the index, search views, the search page, product facets, filtered listings that need no index, and when to go beyond the built-in index.
L2
Unit 02 · The search page
Pagination and result counts
The search pagePage 8 of 20Updated
How a search page pages its results, what the total count means once it passes 10,000, how deep a visitor can page before results stop, and a results header and page navigation in both engines that never link to an error.
Who this is for. The theme developer adding a result count and page links to a search page. Afterwards you can print an honest count, and page links that never lead to an empty page or a 404.
Page size and the page parameter
The page size comes from the view: 15 unless the view is given another number. A visitor can change it with ?limit= only if the view's page size is Changeable, which it is not on a new view; a 0 or a negative number then keeps the view's size. The page is ?page=, counted from 1, and anything that is not a positive number is page 1.
_pagination builds every page link from the current URL, so the query, the filters and the sort survive paging: it drops any page= already in the query string and adds its own, and leaves it off for page 1.
What the count means
$response->getTotalCount() is the number of matches Elasticsearch reports, and Elasticsearch counts exactly only up to 10,000. Past that it stops counting and reports 10,000, and the platform passes the number on: a search with 42,000 matches reports 10,000. An exact count over a large index means visiting every match, which is the costly part of a search; the platform does not ask for one, and no view setting changes that. So the count you get is already the cheap, approximate kind. Treat 10,000 as "10,000 or more":
{% set total = response.getTotalCount() %}
{% if total > 0 %}
{% set first = response.getFrom() + 1 %}
{% set last = min(response.getFrom() + response.getSize(), total) %}
<p id="search-status" role="status">
Results {{ first }} to {{ last }} of {{ total >= 10000 ? '10,000 or more' : total|number_format }}
</p>
{% endif %}
Where a precise total past 10,000 matters, which is rare on a results page, narrow the search, or take the number from the database instead: a plain listing counts its records exactly (Filtering a listing from the query string).
How deep a visitor can page
Elasticsearch also refuses to fetch any result past the 10,000th. A page that reaches beyond it, such as page 667 at 15 a page (results 9,991 to 10,005), is not a 404: the search fails, and the page renders with no results and HTTP 200, as any failed search does. A page past the last one, when the count is known, is a 404. On this site, whose view shows 50 results a page, a search for template has 213 matches, so ?q=template&page=6 answers 404; a list of everything with ?page=999 answers 200 with nothing in it.
Nobody reads page 200, so link to no page past the last one Elasticsearch will return, which is the page size divided into 10,000, rounded down, and steer visitors towards a narrower search instead. Previous and next links come from the pagination too; on the first page getPreviousPage() returns the first page itself, and on the last page getNextPage() returns the last, so hide each with the current page's isFirst() and isLast():
{% if _pagination.getPagesCount() > 1 %}
{% set current = _pagination.getCurrentPage() %}
{% set maxPage = (10000 / response.getSize())|round(0, 'floor') %}
<nav aria-label="Result pages">
{% if not current.isFirst() %}
<a rel="prev" href="{{ _pagination.getPreviousPage().getUrl()|e }}">Previous</a>
{% endif %}
{% for p in _pagination.getPages(7) %}
{% if p.getNumber() <= maxPage %}
{% if p.isCurrent() %}<span aria-current="page">{{ p.getNumber() }}</span>
{% else %}<a href="{{ p.getUrl()|e }}">{{ p.getNumber() }}</a>{% endif %}
{% endif %}
{% endfor %}
{% if not current.isLast() and current.getNumber() < maxPage %}
<a rel="next" href="{{ _pagination.getNextPage().getUrl()|e }}">Next</a>
{% endif %}
</nav>
{% endif %}
getPages(7) returns up to seven pages centred on the current one. The number must be at least 3 when there are more pages than that; a smaller one throws.
An empty result has no pages
With no results, getPagesCount() is 0, and getCurrentPage(), getFirstPage(), getPreviousPage(), getNextPage(), getLastPage() and a page's isCurrent() throw instead of returning nothing. A failed search renders the same way. Keep every one of those calls inside a test on getPagesCount(), as both examples on this page do, or the zero-result page itself fails to render (Empty and zero-result states).