Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f3f36ec
pagination guide: zeroth draft
guimachiavelli Jun 21, 2022
4f76ad2
pagination guide: slightly improved draft
guimachiavelli Jun 22, 2022
693de12
pagination settings: API reference scaffolding
guimachiavelli Jun 23, 2022
6d624d5
pagination: new API reference page
guimachiavelli Jun 27, 2022
b2c4659
pagination: new API code samples
guimachiavelli Jun 27, 2022
dcd96bb
pagination: fix code sample key
guimachiavelli Jun 27, 2022
6ce7068
pagination: add API reference to menu
guimachiavelli Jun 27, 2022
88e9fc5
pagination: update global settings reference
guimachiavelli Jun 27, 2022
9cffdb1
pagination: add guide placeholder
guimachiavelli Jun 28, 2022
aec2da0
pagination: update known limitations
guimachiavelli Jun 28, 2022
b0572e0
pagination: update HTTP method
guimachiavelli Jun 28, 2022
9250fc0
Apply suggestions from code review
guimachiavelli Jun 28, 2022
a99a123
Apply suggestions from code review
guimachiavelli Jun 28, 2022
d63d87d
Apply suggestions from code review
guimachiavelli Jun 28, 2022
9d070d1
pagination guide: first draft
guimachiavelli Jun 29, 2022
1e88a91
Apply suggestions from code review
guimachiavelli Jun 29, 2022
df1d260
pagination settings: address review feedback
guimachiavelli Jun 29, 2022
f5653cf
pagination settings: improve note on side effects of high `maxTotalHits`
guimachiavelli Jun 29, 2022
2e8e715
pagination settings: fix broken hash
guimachiavelli Jun 29, 2022
b397e1f
pagination settings: update boxes with warnings on non-customizable m…
guimachiavelli Jun 29, 2022
6a618ad
Update reference/api/pagination.md
guimachiavelli Jun 30, 2022
f48fe20
Update reference/api/pagination.md
guimachiavelli Jun 30, 2022
213c326
Add pagination to core concepts/indexes
maryamsulemani97 Jun 30, 2022
0a340c2
Update learn/core_concepts/indexes.md
maryamsulemani97 Jun 30, 2022
752f0b3
Update indexes.md
maryamsulemani97 Jun 30, 2022
511f904
update setings
maryamsulemani97 Jun 30, 2022
8d01e10
settings_guide_pagination_1
maryamsulemani97 Jun 30, 2022
085dcb4
Update .code-samples.meilisearch.yaml
maryamsulemani97 Jun 30, 2022
486e470
Update learn/core_concepts/indexes.md
maryamsulemani97 Jun 30, 2022
31a1443
Apply suggestions from code review
guimachiavelli Jul 4, 2022
4827ac3
Update .code-samples.meilisearch.yaml
guimachiavelli Jul 4, 2022
4257e90
Update .code-samples.meilisearch.yaml
guimachiavelli Jul 4, 2022
3d3d3de
add return to code samples file
guimachiavelli Jul 4, 2022
24a46eb
Merge branch 'v0.28-pagination-settings' into update-indexes.md-to-ad…
guimachiavelli Jul 4, 2022
16b334d
Merge #1766
bors[bot] Jul 4, 2022
655c262
Update learn/advanced/pagination.md
guimachiavelli Jul 4, 2022
431c864
Merge #1759
bors[bot] Jul 5, 2022
60890b6
Address review feedback
guimachiavelli Jul 5, 2022
1693ada
Apply suggestions from code review
guimachiavelli Jul 6, 2022
7b8fbc7
Apply suggestions from code review
guimachiavelli Jul 6, 2022
a12ddd3
address review feedback
guimachiavelli Jul 6, 2022
b1861b3
Merge pull request #1753 from meilisearch/v0.28-pagination-guide
guimachiavelli Jul 7, 2022
f6a47b9
Merge branch 'v0.28' into v0.28-pagination
guimachiavelli Jul 7, 2022
8483f25
pagination: fix linter errors on guide, reference
guimachiavelli Jul 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ update_settings_1: |-
},
"disableOnAttributes": ["title"]
},
"pagination": {
"maxTotalHits": 5000

"faceting": {
"maxValuesPerFacet": 200
}
Expand Down Expand Up @@ -951,6 +954,26 @@ getting_started_typo_tolerance: |-
--data-binary '{
"minWordSizeForTypos": { "oneTypo": 4 }
}'
settings_guide_pagination_1: |-
curl \
-X PATCH 'http://localhost:7700/indexes/movies/settings/pagination' \
-H 'Content-Type: application/json' \
--data-binary '{
"maxTotalHits": 50
}'
get_pagination_settings_1: |-
curl \
-X GET 'http://localhost:7700/indexes/books/settings/pagination'
update_pagination_settings_1: |-
curl \
-X PATCH 'http://localhost:7700/indexes/books/settings/pagination' \
-H 'Content-Type: application/json' \
--data-binary '{
"maxTotalHits": 100
}'
reset_pagination_settings_1: |-
curl \
-X DELETE 'http://localhost:7700/indexes/books/settings/pagination'
get_faceting_settings_1: |-
curl \
-X GET 'http://localhost:7700/indexes/books/settings/faceting'
Expand Down
2 changes: 2 additions & 0 deletions .vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ module.exports = {
'/learn/advanced/asynchronous_operations',
'/learn/advanced/filtering_and_faceted_search',
'/learn/advanced/geosearch',
'/learn/advanced/pagination',
'/learn/advanced/sorting',
{
title: 'Updating Meilisearch',
Expand Down Expand Up @@ -325,6 +326,7 @@ module.exports = {
'/reference/api/distinct_attribute',
'/reference/api/faceting',
'/reference/api/filterable_attributes',
'/reference/api/pagination',
'/reference/api/ranking_rules',
'/reference/api/searchable_attributes',
'/reference/api/sortable_attributes',
Expand Down
4 changes: 4 additions & 0 deletions .vuepress/public/sample-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ updating_guide_retrieve_documents_old: |-
updating_guide_update_settings_old: |-
updating_guide_add_documents_old: |-
getting_started_typo_tolerance: |-
settings_guide_pagination_1: |-
get_pagination_settings_1: |-
update_pagination_settings_1: |-
reset_pagination_settings_1: |-
get_faceting_settings_1: |-
update_faceting_settings_1: |-
reset_faceting_settings_1: |-
Expand Down
4 changes: 2 additions & 2 deletions learn/advanced/known_limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@ user = 1 OR user = 2 […] OR user = 1500 OR user = 1501 […] OR user = 2000 OR

## Maximum number of results per search

**Limitation:** Meilisearch returns up to 1000 documents per search.
**Limitation:** By default, Meilisearch returns up to 1000 documents per search.

**Explanation:** This non-customizable limit ensures the database is protected from malicious scraping. This limit only applies to the [search route](/reference/api/search.md). If you want to get all documents in your database, you can use the [get documents endpoint](/reference/api/documents.md#get-documents) instead.
**Explanation:** Meilisearch limits the maximum amount of returned search results to protect your database from malicious scraping. You may change this by using the `maxTotalHits` property of the [pagination index settings](/reference/api/pagination.md#maxtotalhits). `maxTotalHits` only applies to the [search route](/reference/api/search.md) and has no effect on the [get documents endpoint](/reference/api/documents.md#get-documents).
223 changes: 223 additions & 0 deletions learn/advanced/pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Search result pagination

In a perfect world, users would not need to look beyond the first search result to find what they were looking for. In practice, however, it is usually necessary to create some kind of pagination interface to browse through long lists of results.

In this guide, we will discuss some of Meilisearch's current limitations, how these limitations impact common pagination interface patterns, and the recommended way of handling pagination when using Meilisearch.

## Choosing the right pagination UI

There are quite a few pagination interfaces you might want to implement in your application. Many common UI patterns have a page selector allowing users to jump to any search results page. To create a page selector, you must know the exact number of total results so you can calculate the precise number of result pages.

For performance reasons, however, Meilisearch cannot provide the exact number of results for a query. Instead, when using the [search endpoint](/reference/api/search.md), responses contain an `estimatedTotalHits` field. As its name indicates, `estimatedTotalHits` is only an estimate of how many documents match your user's query.

Because of this, we do not recommend creating interfaces with page selectors. If page selection is crucial to the software you are developing, see the [last section of this page](#not-recommended-page-selection) for tips that might help you work around Meilisearch's current limitations.

Many other pagination UIs are fully compatible with Meilisearch, such as infinite scrolling and buttons that manually load more results on click. For an experience similar to page selection, we recommend creating pagination interfaces centered around [previous and next buttons](#recommended-previous-and-next-buttons).

## Recommended: "Previous" and "Next" buttons

Using previous and next buttons for pagination means that users can easily navigate through results, but don't have the ability to jump to an arbitrary results page.

Though this approach offers less precision than a full-blown page selector, it does not require knowing the precise number of search results. This makes it a good fit for Meilisearch's current capabilities.

### Implementation

#### `limit` and `offset`

Previous and next buttons can be implemented using the [`limit`](/reference/api/search.md#limit) and [`offset`](/reference/api/search.md#offset) search parameters.

`limit` sets the size of a page. If you set `limit` to `10`, Meilisearch's response will contain a maximum of 10 search results. `offset` skips a number of search results. If you set `offset` to `20`, Meilisearch's response will skip the first 20 search results.

For example, you can use Meilisearch's JavaScript SDK to get the first ten films in a movies database:

```js
const results = await index.search("tarkovsky", { limit: 10, offset: 0 });
```

You can use both parameters together to effectively create search pages.

#### Search pages and calculating `offset`

If you set `limit` to `20` and `offset` to `0`, you get the first twenty search results. We can call this our first page.

```js
const results = await index.search("tarkovsky", { limit: 20, offset: 0 });
```

Likewise, if you set `limit` to `20` and `offset` to `40`, you skip the first 40 search results and get documents ranked from 40 through 59. We can call this the third results page.

```js
const results = await index.search("tarkovsky", { limit: 20, offset: 40 });
```

You can use this formula to calculate a page's offset value: `offset = limit * (target page number - 1)`. In the previous example, the calculation would look like this: `offset = 20 * (3 - 1)`. This gives us `40` as the result: `offset = 20 * 2 = 40`.

Once a query returns fewer `hits` than your configured `limit`, you have reached the last results page.

#### Keeping track of the current page number

Even though this UI pattern does not allow users to jump to a specific page, it is still useful to keep track of the current page number.

The following JavaScript snippet stores the page number in an HTML element, `.pagination`, and updates it every time the user moves to a different search results page:

```js
function updatePageNumber(elem) {
const directionBtn = elem.id
// Get the page number stored in the pagination element
let pageNumber = parseInt(document.querySelector('.pagination').dataset.pageNumber)

// Update page number
if (directionBtn === 'previous_button') {
pageNumber = pageNumber - 1
} else if (directionBtn === 'next_button') {
pageNumber = pageNumber + 1
}

// Store new page number in the pagination element
document.querySelector('.pagination').dataset.pageNumber = pageNumber
}

// Add data to our HTML element stating the user is on the first page
document.querySelector('.pagination').dataset.pageNumber = 0
// Each time a user clicks on the previous or next buttons, update the page number
document.querySelector('#previous_button').onclick = function () { updatePageNumber(this) }
document.querySelector('#next_button').onclick = function () { updatePageNumber(this) }
```

#### Disabling navigation buttons for first and last pages

It is often helpful to disable navigation buttons when the user cannot move to the "next" or "previous" page.

The "Previous" button should be disabled whenever your `offset` is `0`, as this indicates your user is on the first results page.

To know when to disable the "Next" button, we recommend setting your query's `limit` to the number of results you wish to display per page plus one. That extra `hit` should not be shown to the user. Its purpose is to indicate that there is at least one more document to display on the next page.

The following JavaScript snippet runs checks whether we should disable a button every time the user navigates to another search results page:

```js
function updatePageNumber() {
const pageNumber = parseInt(document.querySelector('.pagination').dataset.pageNumber)

const offset = pageNumber * 20
const results = await index.search('x', { limit: 21, offset })

// If offset equals 0, we're on the first results page
if (offset === 0 ) {
document.querySelector('#previous_button').disabled = true;
}

// If offset is bigger than 0, we're not on the first results page
if (offset > 0 ) {
document.querySelector('#previous_button').disabled = false;
}

// If Meilisearch returns 20 items or less,
// we are on the last page
if (results.hits.length < 21 ) {
document.querySelector('#next_button').disabled = true;
}

// If Meilisearch returns exactly 21 results
// and our page can only show 20 items at a time,
// we have at least one more page with one result in it
if (results.hits.length === 21 ) {
document.querySelector('#next_button').disabled = false;
}
}

document.querySelector('#previous_button').onclick = function () { updatePageNumber(this) }
document.querySelector('#next_button').onclick = function () { updatePageNumber(this) }
```

## Not recommended: Page selection

This type of pagination consists of a numbered list of pages accompanied by next and previous buttons.

This is a common UI pattern that offers users a significant amount of precision when navigating results. However, due to Meilisearch's [limitations](#choosing-the-right-pagination-ui), it is not a good fit for pagination with Meilisearch.

### Implementation

The general implementation of a page selection interface is similar to our [recommended solution](#recommended-previous-and-next-buttons), with one signficant addition: it includes a numbered page list.

To create a numbered page list, however, you must know the exact number of total results. For example, if you have 100 results and your search result pages contain 10 results each, your selector must show a list of numbers going from 1 to 10.

Since Meilisearch can only give you an estimate of total search results, it is difficult to implement page selectors when using Meilisearch.

We recommend two different workarounds to create this kind of pagination interface with Meilisearch.

#### Use `limit` to set a maximum number of search results

By default, a search request returns 20 search results. You can change this value to a much higher number and treat it as the effective maximum of search results you will show a user. Doing so means the size of your `hits` array is the exact number of search results you have to paginate.

For example, if you set `limit` to `300`, every search request made to Meilisearch returns at most 300 documents. If a query returns a `hits` array with 200 items and you want each page to display 20 results, you can create a page selector with 10 pages.

In the following JavaScript snippet, each time a user searches, we make a new query with `limit` set to `300`. Once we receive the search results, we store them in a variable and create a list of numbered pages. When users click on any number in the page list, we display a new page:

```js
// Retrieve search results and create the page selector
function getSearchResults(searchTerm) {
// The amount of results we want to display in each page
const hitsPerPage = 10

// The maximum amount of results we will display
const { hits } = await index.search(searchTerm, { limit: 300 })

// Clear the previous buttons…
const pagination = document.querySelector('.pagination')
pagination.innerHTML = ''

// …and create a new button for each search results page
for (let page = 0; page < hits.length; page += hitsPerPage) {
const numberBtn = document.createElement('button');
numberBtn.innerText = page / hitsPerPage

// When the user clicks on a page number, show that results page
numberBtn.onclick = function () {
const pageNumber = parseInt(this.innerText)
changePage(pageNumber, hits, hitsPerPage)
}

pagination.appendChild(numberBtn)
}

// Display first search results page
changePage(1, hits, hitsPerPage)
}

// Display a page of search results
function changePage(pageNumber, hits, hitsPerPage) {
const offset = (pageNumber - 1) * hitsPerPage
const paginatedHits = hits.slice(offset, offset + hitsPerPage)
const resultList = document.querySelector('.results')

resultList.innerHTML = ''

paginatedHits.map(hit => {
const resultItem = document.createElement('div');
resultItem.innerText = hit.title
resultList.appendChild(resultItem)
})
}

// Get the search bar and retrieve results every time the user makes a new query
const searchBar = document.querySelector('.searchBar')
searchBar.onChange(function () { getSearchResults('tarkovsky') })
```

This method provides control and reliability. It also prevents users from paginating too far, which might result in performance gains. However, it limits the number of results your users can see. Additionally, we recommend caution when setting high values for `limit` as it can negatively impact performance.

::: note
By default, Meilisearch returns a maximum of 1000 search results. Consult our [index setting reference](/reference/api/pagination.md) if you need to change this.
:::

#### Use `estimatedTotalHits`

Though we advise against it, you can use `estimatedTotalHits` to calculate the number of search result pages. This means your number of results and page count are likely to change until Meilisearch retrieves the last search result.

For example, a query's `estimatedTotalHits` might be `100` when you fetch the first page of search results. If you are showing 20 results per page, this means your interface will display a page selector with 5 pages. When you fetch the fifth and last page according to Meilisearch's current estimate, however, `estimatedTotalHits` might change to `300`. Your page list, previously displaying 5 pages, must now show 15 total pages.

This method gives users access to all search results. However, it also results in an interface that might feel unreliable due to constant and unpredictable changes.

## Unsatisfied? Let us know

Is the current state of pagination in Meilisearch creating problems for you? Please share your thoughts with us in this [GitHub discussion](https://github.com/meilisearch/product/discussions/483). We are actively working on improving this aspect of Meilisearch and your input is greatly appreciated.
17 changes: 17 additions & 0 deletions learn/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This page describes the **index-level settings** available in Meilisearch and ho
| **[displayedAttributes](/learn/configuration/settings.md#displayed-attributes)** | Fields displayed in the returned documents | All attributes found in the documents |
| **[distinctAttribute](/learn/configuration/settings.md#distinct-attribute)** | Search returns documents with distinct (different) values of the given field | `null` |
| **[filterableAttributes](/learn/configuration/settings.md#filterable-attributes)** | List of attributes that can be used for filtering | `null` |
| **[pagination](/learn/advanced/pagination.md)** | Pagination settings | `{}`
| **[faceting](/learn/advanced/filtering_and_faceted_search.md)** | Faceting settings | `{}`
| **[rankingRules](/learn/configuration/settings.md#ranking-rules)** | List of ranking rules sorted by order of importance | [A list of ordered built-in ranking rules](/learn/core_concepts/relevancy.md#built-in-rules) |
| **[searchableAttributes](/learn/configuration/settings.md#searchable-attributes)** | Fields in which to search for matching query words sorted by order of importance | All attributes found in the documents | |
Expand Down Expand Up @@ -96,6 +97,22 @@ To be able to filter search results on `director` and `genres` in a movie databa

<CodeSamples id="faceted_search_update_settings_1" />

## Pagination

The maximum number of results Meilisearch can return. By default, this value is `1000` which means you cannot access results beyond `1000`.

[Learn more about pagination in our dedicated guide.](/learn/advanced/pagination.md)

### Example

The code sample below updates `maxTotalHits` to `50`:

<CodeSamples id="settings_guide_pagination_1" />

::: note
`maxTotalHits` takes priority over search parameters such as [`limit`](/reference/api/search.md#limit) and [`offset`](/reference/api/search.md#offset).
:::

## Faceting

The faceting settings of an index. Facets are specific use-cases of filters that can be used to refine search results.
Expand Down
7 changes: 7 additions & 0 deletions learn/core_concepts/indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ You can customize the following index settings:
- [Stop words](#stop-words)
- [Displayed and searchable attributes](#displayed-and-searchable-attributes)
- [Typo tolerance](#typo-tolerance)
- [Pagination](#pagination)
- [Faceting](#faceting)

To change index settings, use the [update settings endpoint](/reference/api/settings.md#update-settings) or any of the [child routes](/reference/api/settings.md#all-settings).
Expand Down Expand Up @@ -131,6 +132,12 @@ You can update the typo tolerance settings using the [update settings endpoint](

[Learn more about typo tolerance](/learn/configuration/typo_tolerance.md)

### Pagination

To protect your database from malicious scraping, Meilisearch only returns up to `1000` results for a search query. You can change this limit using the [update settings endpoint](/reference/api/settings.md#update-settings) or the [update pagination settings endpoint](/reference/api/pagination.md#update-pagination-settings).

[Learn more about pagination](/learn/advanced/pagination.md)

### Faceting

Facets are a specific use-case of filters in Meilisearch: whether something is a facet or filter depends on your UI and UX design. Like filters, you need to add your facets to [`filterableAttributes`](/reference/api/filterable_attributes.md#update-filterable-attributes), then make a search query using the [`filter` search parameter](/reference/api/search.md#filter).
Expand Down
Loading