Pagination

It is often impractical for an application to deal with a very large number of results. For this reason, APIs and web services use pagination controls to allow applications to request the results in small chunks or pages.

You may have noticed that Elasticsearch by default does not return more than 10 results. The optional size parameter can be given in a search request to change this maximum. The following example asks for up to 5 search results to be returned:

results = es.search(
    query={
        'multi_match': {
            'query': query,
            'fields': ['name', 'summary', 'content'],
        }
    }, size=5
)

To access additional pages of results, the from_ parameter is used, which indicates from where in the complete list of results to start (since from is a reserved keyword in Python, from_ is used).

The next example retrieves a second page of 5 results:

results = es.search(
    query={
        'multi_match': {
            'query': query,
            'fields': ['name', 'summary', 'content'],
        }
    }, size=5, from_=5
)

Let's incorporate size and from_ into the handle_search() endpoint in app.py:

@app.post('/')
def handle_search():
    query = request.form.get('query', '')
    from_ = request.form.get('from_', type=int, default=0)
    results = es.search(
        query={
            'multi_match': {
                'query': query,
                'fields': ['name', 'summary', 'content'],
            }
        }, size=5, from_=from_
    )
    return render_template('index.html', results=results['hits']['hits'],
                           query=query, from_=from_,
                           total=results['hits']['total']['value'])

Here the page size is now hardcoded to 5 (feel free to use any other number that you like). The from_ argument is assumed to be given as an additional field in the submitted form, but this field is considered optional, defaulting to 0 when not present.

The search form that is available in index.html does not have a from_ field, so regular searches will always start from the first result. The template displays information about the range of results that shown, and what the total is. Here is how this is done using template expressions:

<div class="col-sm-auto my-auto">
    Showing results {{ from_ + 1 }}-{{ from_ + results|length }} out of {{ total }}.
</div>

The template also includes logic to display pagination buttons to move forwards or backwards in the list of results. Here is the implementation of the "Previous results" button:

{% if from_ > 0 %}
    <div class="col-sm-auto my-auto">
        <a href="javascript:history.back(1)" class="btn btn-primary">← Previous page</a>
    </div> 
{% endif %}

As you can see, the "Previous page" button is only rendered to the page when from_ is greater than zero. The implementation of this button uses the browser's history API to go back one page.

The "Next page" button has a much more interesting implementation:

{% if from_ + results|length < total %}
    <div class="col-sm-auto my-auto">
        <form method="POST">
            <input type="hidden" name="query" value="{{ query }}">
            <input type="hidden" name="from_" value="{{ from_ + results|length }}">
            <button type="submit" class="btn btn-primary">Next page →</button>
        </form>
    </div>
{% endif %}

This button isn't actually a standalone button, but a complete form that has two hidden fields in addition to the button. The form is similar to the main search form, but includes the optional from_ field, adjusted to point to the next page of results. When this button is clicked, the Flask application will receive a search request from this alternate form, which uses the same text query but a non-zero from_ value.

With this small and clever implementation of pagination you will be able to navigate through multiple pages of results.

Share this article