Skip to content

Commit

Permalink
Add pagination feature for v0.30.0 (#878)
Browse files Browse the repository at this point in the history
* Update tests to remove totalHitsPerPage

* Put finitePagination in pagination context

* Update adapting pagination

* Remove finitePagination from search context

* Remove paginationTotalHits from playgrounds

* Implement finite pagination using page and hits per page

* Add tests

* Update readme

* Add explaination on finitePagination in thge pagination widget

* Improve floor division

* Consistency in undefined checks

* Use offset for lazy pagination
  • Loading branch information
bidoubiwa authored Nov 17, 2022
1 parent 05e5bc5 commit 31113ed
Show file tree
Hide file tree
Showing 19 changed files with 544 additions and 353 deletions.
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>
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
66 changes: 21 additions & 45 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,86 +107,63 @@ 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,
})

expect(searchParams.limit).toBe(7)
expect(searchParams.offset).toBe(0)
})

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

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 },
pagination: { page: 1, hitsPerPage: 6, finite: false },
})

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)
expect(searchParams.limit).toBe(7)
expect(searchParams.offset).toBe(6)
})

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)
expect(searchParams.offset).toBe(0)
})
})
71 changes: 54 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,43 @@ import {
} from './geo-rules-adapter'
import { adaptFilters } from './filter-adapter'

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

return {
limit: hitsPerPage + 1,
offset: page * hitsPerPage,
}
}

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 +66,6 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
highlightPostTag,
placeholderSearch,
query,
finitePagination,
sort,
pagination,
matchingStrategy,
Expand Down Expand Up @@ -84,23 +120,24 @@ 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, offset } = setScrollPagination(
pagination.hitsPerPage,
pagination.page,
query,
placeholderSearch
)
meiliSearchParams.limit = limit
meiliSearchParams.offset = offset
}
},
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

0 comments on commit 31113ed

Please sign in to comment.