Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination feature for v0.30.0 #878

30 changes: 5 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ const searchClient = instantMeiliSearch(
`instant-meilisearch` offers some options you can set to further fit your needs.

- [`placeholderSearch`](#placeholder-search): Enable or disable placeholder search (default: `true`).
- [`paginationTotalHits`](#pagination-total-hits): Maximum total number of hits to create a finite pagination (default: `200`).
- [`finitePagination`](#finite-pagination): Used to work with the [`pagination`](#-pagination) widget (default: `false`) .
- [`finitePagination`](#finite-pagination): Enable finite pagination when using the the [`pagination`](#-pagination) widget (default: `false`) .
- [`primaryKey`](#primary-key): Specify the primary key of your documents (default `undefined`).
- [`keepZeroFacets`](#keep-zero-facets): Show the facets value even when they have 0 matches (default `false`).
- [`matchingStrategy`](#matching-strategy): Determine the search strategy on words matching (default `last`).
Expand All @@ -100,7 +99,6 @@ const searchClient = instantMeiliSearch(
'https://integration-demos.meilisearch.com',
'99d1e034ed32eb569f9edc27962cccf90b736e4c5a70f7f5e76b9fab54d6a185',
{
paginationTotalHits: 30, // default: 200.
placeholderSearch: false, // default: true.
primaryKey: 'id', // default: undefined
// ...
Expand All @@ -117,34 +115,18 @@ When placeholder search is set to `false`, no results appears when searching on
{ placeholderSearch : true } // default true
```

### Pagination total hits

The total (and finite) number of hits (default: `200`) you can browse during pagination when using the [pagination widget](https://www.algolia.com/doc/api-reference/widgets/pagination/js/) or the [`infiniteHits` widget](#-infinitehits). If none of these widgets are used, `paginationTotalHits` is ignored.<br>

For example, using the `infiniteHits` widget, and a `paginationTotalHits` of 9. On the first search request 6 hits are shown, by clicking a second time on `load more` only 3 more hits are added. This is because `paginationTotalHits` is `9`.

Usage:

```js
{ paginationTotalHits: 50 } // default: 200
```

`hitsPerPage` has a value of `20` by default and can [be customized](#-hitsperpage).

### Finite Pagination

Finite pagination is used when you want to add a numbered pagination at the bottom of your hits (for example: `<< < 1, 2, 3 > >>`).
To be able to know the amount of page numbers you have, a search is done requesting `paginationTotalHits` documents (default: `200`).
With the amount of documents returned, instantsearch is able to render the correct amount of numbers in the pagination widget.

It requires the usage of the [`Pagination` widget](#-pagination).

Example:

```js
{ finitePagination: true } // default: false
```

⚠️ Meilisearch is not designed for pagination and this can lead to performances issues, so the usage `finitePagination` but also of the pagination widgets are not recommended.<br>
bidoubiwa marked this conversation as resolved.
Show resolved Hide resolved
More information about Meilisearch and the pagination [here](https://github.com/meilisearch/documentation/issues/561).

### Primary key

Expand Down Expand Up @@ -925,9 +907,7 @@ instantsearch.widgets.clearRefinements({

[Pagination references](https://www.algolia.com/doc/api-reference/widgets/pagination/js/)

The `pagination` widget displays a pagination system allowing the user to change the current page.

We do not recommend using this widget as pagination slows the search responses. Instead, the [InfiniteHits](#-infinitehits) component is recommended.
The `pagination` widget displays a pagination system allowing the user to change the current page. It should be used alongside the [`finitePagination`](#finite-pagination) setting to render the correct amount of pages.

- ✅ container: The CSS Selector or HTMLElement to insert the widget into. _required_
- ✅ showFirst: Whether to display the first-page link.
Expand Down Expand Up @@ -1094,4 +1074,4 @@ If you want to know more about the development workflow or want to contribute, p

<hr>

**Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository.
**Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository.
1 change: 0 additions & 1 deletion playgrounds/react/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const searchClient = instantMeiliSearch(
'https://integration-demos.meilisearch.com',
'99d1e034ed32eb569f9edc27962cccf90b736e4c5a70f7f5e76b9fab54d6a185',
{
paginationTotalHits: 60,
primaryKey: 'id',
}
)
Expand Down
61 changes: 17 additions & 44 deletions src/adapter/search-request-adapter/__tests__/search-params.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { MatchingStrategies } from '../../../types'

const DEFAULT_CONTEXT = {
indexUid: 'test',
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
pagination: { page: 0, hitsPerPage: 6, finite: false },
defaultFacetDistribution: {},
finitePagination: false,
}

describe('Parameters adapter', () => {
Expand Down Expand Up @@ -108,33 +107,24 @@ describe('Pagination adapter', () => {
test('adapting a searchContext with finite pagination', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
finitePagination: true,
pagination: { page: 0, hitsPerPage: 6, finite: true },
})

expect(searchParams.limit).toBe(20)
expect(searchParams.page).toBe(1)
expect(searchParams.hitsPerPage).toBe(6)
})

test('adapting a searchContext with finite pagination on a later page', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
pagination: { paginationTotalHits: 20, page: 10, hitsPerPage: 6 },
finitePagination: true,
pagination: { page: 10, hitsPerPage: 6, finite: true },
})

expect(searchParams.limit).toBe(20)
expect(searchParams.page).toBe(11)
expect(searchParams.hitsPerPage).toBe(6)
})

test('adapting a searchContext with finite pagination and pagination total hits lower than hitsPerPage', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
pagination: { paginationTotalHits: 4, page: 0, hitsPerPage: 6 },
finitePagination: true,
})

expect(searchParams.limit).toBe(4)
})

test('adapting a searchContext with no finite pagination', () => {
test('adapting a searchContext with no finite pagination on page 1', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
})
Expand All @@ -145,49 +135,32 @@ describe('Pagination adapter', () => {
test('adapting a searchContext with no finite pagination on page 2', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
pagination: { paginationTotalHits: 20, page: 1, hitsPerPage: 6 },
pagination: { page: 1, hitsPerPage: 6, finite: false },
})

expect(searchParams.limit).toBe(13)
})

test('adapting a searchContext with no finite pagination on page higher than paginationTotalHits', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
pagination: { paginationTotalHits: 20, page: 40, hitsPerPage: 6 },
})

expect(searchParams.limit).toBe(20)
})

test('adapting a searchContext with no finite pagination and pagination total hits lower than hitsPerPage', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
pagination: { paginationTotalHits: 4, page: 0, hitsPerPage: 6 },
})

expect(searchParams.limit).toBe(4)
})

test('adapting a searchContext placeholderSearch set to false', () => {
test('adapting a finite pagination with no placeholderSearch', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
query: '',
pagination: { paginationTotalHits: 4, page: 0, hitsPerPage: 6 },
pagination: { page: 4, hitsPerPage: 6, finite: true },
placeholderSearch: false,
})

expect(searchParams.limit).toBe(0)
expect(searchParams.page).toBe(5)
expect(searchParams.hitsPerPage).toBe(0)
})

test('adapting a searchContext placeholderSearch set to false', () => {
test('adapting a scroll pagination with no placeholderSearch', () => {
const searchParams = adaptSearchParams({
...DEFAULT_CONTEXT,
query: '',
pagination: { paginationTotalHits: 200, page: 0, hitsPerPage: 6 },
placeholderSearch: true,
pagination: { page: 4, hitsPerPage: 6, finite: false },
placeholderSearch: false,
})

expect(searchParams.limit).toBe(7)
expect(searchParams.limit).toBe(0)
})
})
68 changes: 51 additions & 17 deletions src/adapter/search-request-adapter/search-params-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ import {
} from './geo-rules-adapter'
import { adaptFilters } from './filter-adapter'

function setScrollPagination(
hitsPerPage: number,
page: number,
query?: string,
placeholderSearch?: boolean
): { limit: number } {
if (!placeholderSearch && query === '') {
return {
limit: 0,
}
}

return {
limit: (page + 1) * hitsPerPage + 1,
}
}

function setFinitePagination(
hitsPerPage: number,
page: number,
query?: string,
placeholderSearch?: boolean
): { hitsPerPage: number; page: number } {
if (!placeholderSearch && query === '') {
return {
hitsPerPage: 0,
page: page + 1,
}
} else {
return {
hitsPerPage: hitsPerPage,
page: page + 1,
}
}
}
/**
* Adapts instantsearch.js and instant-meilisearch options
* to meilisearch search query parameters.
Expand All @@ -29,7 +64,6 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
highlightPostTag,
placeholderSearch,
query,
finitePagination,
sort,
pagination,
matchingStrategy,
Expand Down Expand Up @@ -84,23 +118,23 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
}
},
addPagination() {
// Limit based on pagination preferences
if (
(!placeholderSearch && query === '') ||
pagination.paginationTotalHits === 0
) {
meiliSearchParams.limit = 0
} else if (finitePagination) {
meiliSearchParams.limit = pagination.paginationTotalHits
if (pagination.finite) {
const { hitsPerPage, page } = setFinitePagination(
pagination.hitsPerPage,
pagination.page,
query,
placeholderSearch
)
meiliSearchParams.hitsPerPage = hitsPerPage
meiliSearchParams.page = page
} else {
const limit = (pagination.page + 1) * pagination.hitsPerPage + 1
// If the limit is bigger than the total hits accepted
// force the limit to that amount
if (limit > pagination.paginationTotalHits) {
meiliSearchParams.limit = pagination.paginationTotalHits
} else {
meiliSearchParams.limit = limit
}
const { limit } = setScrollPagination(
pagination.hitsPerPage,
pagination.page,
query,
placeholderSearch
)
meiliSearchParams.limit = limit
}
},
addSort() {
Expand Down
14 changes: 3 additions & 11 deletions src/adapter/search-request-adapter/search-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,19 @@ export function SearchResolver(
): Promise<MeiliSearchResponse<Record<string, any>>> {
const { placeholderSearch, query } = searchContext

const { pagination } = searchContext

// In case we are in a `finitePagination`, only one big request is made
// containing a total of max the paginationTotalHits (default: 200).
// Thus we dont want the pagination to impact the cache as every
// hits are already cached.
const paginationCache = searchContext.finitePagination ? {} : pagination

// Create cache key containing a unique set of search parameters
const key = cache.formatKey([
searchParams,
searchContext.indexUid,
searchContext.query,
paginationCache,
searchContext.pagination,
])
const cachedResponse = cache.getEntry(key)

// Check if specific request is already cached with its associated search response.
if (cachedResponse) return cachedResponse

const facetsCache = extractFacets(searchContext, searchParams)
const cachedFacets = extractFacets(searchContext, searchParams)

// Make search request
const searchResponse = await client
Expand All @@ -56,7 +48,7 @@ export function SearchResolver(

// Add missing facets back into facetDistribution
searchResponse.facetDistribution = addMissingFacets(
facetsCache,
cachedFacets,
searchResponse.facetDistribution
)

Expand Down
Loading