diff --git a/README.md b/README.md
index 94c00cc7..c4c28734 100644
--- a/README.md
+++ b/README.md
@@ -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`).
@@ -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
// ...
@@ -117,25 +115,11 @@ 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.
-
-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:
@@ -143,8 +127,6 @@ Example:
{ 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.
-More information about Meilisearch and the pagination [here](https://github.com/meilisearch/documentation/issues/561).
### Primary key
@@ -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.
@@ -1094,4 +1074,4 @@ If you want to know more about the development workflow or want to contribute, p
-**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.
\ No newline at end of file
+**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.
diff --git a/playgrounds/react/src/App.js b/playgrounds/react/src/App.js
index d8936b8a..6e84b522 100644
--- a/playgrounds/react/src/App.js
+++ b/playgrounds/react/src/App.js
@@ -20,7 +20,6 @@ const searchClient = instantMeiliSearch(
'https://integration-demos.meilisearch.com',
'99d1e034ed32eb569f9edc27962cccf90b736e4c5a70f7f5e76b9fab54d6a185',
{
- paginationTotalHits: 60,
primaryKey: 'id',
}
)
diff --git a/src/adapter/search-request-adapter/__tests__/search-params.tests.ts b/src/adapter/search-request-adapter/__tests__/search-params.tests.ts
index 2feaf5e1..3e0a3aa1 100644
--- a/src/adapter/search-request-adapter/__tests__/search-params.tests.ts
+++ b/src/adapter/search-request-adapter/__tests__/search-params.tests.ts
@@ -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', () => {
@@ -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)
})
})
diff --git a/src/adapter/search-request-adapter/search-params-adapter.ts b/src/adapter/search-request-adapter/search-params-adapter.ts
index 46771e57..af8004f1 100644
--- a/src/adapter/search-request-adapter/search-params-adapter.ts
+++ b/src/adapter/search-request-adapter/search-params-adapter.ts
@@ -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.
@@ -29,7 +66,6 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
highlightPostTag,
placeholderSearch,
query,
- finitePagination,
sort,
pagination,
matchingStrategy,
@@ -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() {
diff --git a/src/adapter/search-request-adapter/search-resolver.ts b/src/adapter/search-request-adapter/search-resolver.ts
index 7dcc7c4d..13cb15cc 100644
--- a/src/adapter/search-request-adapter/search-resolver.ts
+++ b/src/adapter/search-request-adapter/search-resolver.ts
@@ -27,27 +27,19 @@ export function SearchResolver(
): Promise>> {
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
@@ -56,7 +48,7 @@ export function SearchResolver(
// Add missing facets back into facetDistribution
searchResponse.facetDistribution = addMissingFacets(
- facetsCache,
+ cachedFacets,
searchResponse.facetDistribution
)
diff --git a/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts b/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts
index f0fdad98..953aa650 100644
--- a/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts
+++ b/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts
@@ -1,31 +1,36 @@
-import { adaptPagination } from '../pagination-adapter'
-import { ceiledDivision } from '../../../utils'
+import { adaptPaginationParameters } from '../pagination-adapter'
+import { ceiledDivision } from '../../../../tests/assets/number'
const numberPagesTestParameters = [
{
hitsPerPage: 0,
hitsLength: 100,
numberPages: 0,
+ page: 0,
},
{
hitsPerPage: 1,
hitsLength: 100,
numberPages: 100,
+ page: 1,
},
{
hitsPerPage: 20,
hitsLength: 24,
numberPages: 2,
+ page: 1,
},
{
hitsPerPage: 20,
hitsLength: 0,
numberPages: 0,
+ page: 1,
},
{
hitsPerPage: 0,
hitsLength: 0,
numberPages: 0,
+ page: 1,
},
// Not an Algolia behavior. Algolia returns an error:
// "Value too small for \"hitsPerPage\" parameter, expected integer between 0 and 9223372036854775807",
@@ -36,91 +41,334 @@ const numberPagesTestParameters = [
},
]
-const paginateHitsTestsParameters = [
+const finitePaginateHitsTestsParameters = [
// Empty hits
{
- hits: [],
- page: 0,
- hitsPerPage: 20,
- returnedHits: [],
+ searchResponse: {
+ hits: [],
+ page: 0,
+ hitsPerPage: 20,
+ totalPages: 0,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 20,
+ nbPages: 0,
+ },
},
{
- hits: [],
- page: 100,
- hitsPerPage: 0,
- returnedHits: [],
+ searchResponse: { hits: [], page: 100, hitsPerPage: 0, totalPages: 0 },
+ adaptedPagination: {
+ page: 100,
+ hitsPerPage: 0,
+ nbPages: 0,
+ },
},
{
- hits: [],
- page: 100,
- hitsPerPage: 20,
- returnedHits: [],
+ searchResponse: { hits: [], page: 100, hitsPerPage: 20, totalPages: 0 },
+ adaptedPagination: {
+ page: 100,
+ hitsPerPage: 20,
+ nbPages: 0,
+ },
},
// Page 0
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 0,
- hitsPerPage: 20,
- returnedHits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 0,
+ hitsPerPage: 20,
+ totalPages: 1,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 0,
- hitsPerPage: 0,
- returnedHits: [],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 0,
+ hitsPerPage: 0,
+ totalPages: 0,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 0,
+ nbPages: 0,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 0,
- hitsPerPage: 20,
- returnedHits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 0,
+ hitsPerPage: 20,
+ totalPages: 1,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 0,
- hitsPerPage: 2,
- returnedHits: [{ id: 1 }, { id: 2 }],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 0,
+ hitsPerPage: 2,
+ totalPages: 2,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 2,
+ nbPages: 2,
+ },
},
// Page 1
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 1,
- hitsPerPage: 2,
- returnedHits: [{ id: 3 }],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 1,
+ hitsPerPage: 2,
+ totalPages: 2,
+ },
+ adaptedPagination: {
+ page: 1,
+ hitsPerPage: 2,
+ nbPages: 2,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 1,
- hitsPerPage: 20,
- returnedHits: [],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 1,
+ hitsPerPage: 20,
+ totalPages: 1,
+ },
+ adaptedPagination: {
+ page: 1,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 1,
- hitsPerPage: 0,
- returnedHits: [],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 1,
+ hitsPerPage: 0,
+ totalPages: 0,
+ },
+ adaptedPagination: {
+ page: 1,
+ hitsPerPage: 0,
+ nbPages: 0,
+ },
},
// Page 2
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 2,
- hitsPerPage: 20,
- returnedHits: [],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 2,
+ hitsPerPage: 20,
+ totalPages: 1,
+ },
+ adaptedPagination: {
+ page: 2,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 2,
- hitsPerPage: 20,
- returnedHits: [],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 2,
+ hitsPerPage: 20,
+ totalPages: 1,
+ },
+ adaptedPagination: {
+ hitsPerPage: 20,
+ nbPages: 1,
+ page: 2,
+ },
},
{
- hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
- page: 2,
- hitsPerPage: 0,
- returnedHits: [],
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ page: 2,
+ hitsPerPage: 0,
+ totalPages: 0,
+ },
+ adaptedPagination: {
+ page: 2,
+ nbPages: 0,
+ hitsPerPage: 0,
+ },
+ },
+]
+
+const lazyPaginateHitsTestsParameters = [
+ // Empty hits
+ {
+ searchResponse: {
+ hits: [],
+ limit: 21,
+ offset: 0,
+ },
+ paginationState: {
+ hitsPerPage: 20,
+ page: 0,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
+ },
+ {
+ searchResponse: { hits: [], limit: 0, offset: 0 },
+ paginationState: {
+ page: 100,
+ hitsPerPage: 0,
+ },
+ adaptedPagination: {
+ page: 100,
+ hitsPerPage: 0,
+ nbPages: 0,
+ },
+ },
+ {
+ searchResponse: { hits: [], limit: 21, offset: 0 },
+ paginationState: {
+ page: 100,
+ hitsPerPage: 20,
+ },
+ adaptedPagination: {
+ page: 100,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
+ },
+
+ // // Page 0
+ {
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ limit: 21,
+ offset: 0,
+ },
+ paginationState: {
+ page: 0,
+ hitsPerPage: 20,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
+ },
+ {
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ limit: 0,
+ offset: 0,
+ },
+ paginationState: {
+ page: 0,
+ hitsPerPage: 0,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 0,
+ nbPages: 0,
+ },
+ },
+ {
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ limit: 21,
+ offset: 0,
+ },
+ paginationState: {
+ page: 0,
+ hitsPerPage: 20,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 20,
+ nbPages: 1,
+ },
+ },
+ {
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ limit: 3,
+ offset: 0,
+ },
+ paginationState: {
+ page: 0,
+ hitsPerPage: 2,
+ },
+ adaptedPagination: {
+ page: 0,
+ hitsPerPage: 2,
+ nbPages: 2,
+ },
+ },
+
+ // // Page 1
+ {
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }],
+ limit: 2,
+ offset: 1,
+ },
+ paginationState: {
+ page: 1,
+ hitsPerPage: 1,
+ },
+ adaptedPagination: {
+ page: 1,
+ hitsPerPage: 1,
+ nbPages: 3,
+ },
+ },
+ {
+ searchResponse: {
+ hits: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
+ limit: 3,
+ offset: 2,
+ },
+ paginationState: {
+ page: 1,
+ hitsPerPage: 2,
+ },
+ adaptedPagination: {
+ page: 1,
+ hitsPerPage: 2,
+ nbPages: 3,
+ },
+ },
+
+ // Page 2
+ {
+ searchResponse: {
+ hits: [{ id: 3 }],
+ limit: 2,
+ offset: 2,
+ },
+ paginationState: {
+ page: 2,
+ hitsPerPage: 1,
+ },
+ adaptedPagination: {
+ page: 2,
+ hitsPerPage: 1,
+ nbPages: 3,
+ },
},
]
@@ -129,34 +377,68 @@ describe.each(numberPagesTestParameters)(
({ hitsPerPage, hitsLength, numberPages }) => {
it(`Should return ${numberPages} pages when hitsPerPage is ${hitsPerPage} and hits length is ${hitsLength}`, () => {
const response = ceiledDivision(hitsLength, hitsPerPage)
-
expect(response).toBe(numberPages)
})
}
)
-describe.each(paginateHitsTestsParameters)(
- 'Paginate hits tests',
- ({ hits, page, hitsPerPage, returnedHits }) => {
- it(`Should return ${JSON.stringify(
- returnedHits
+describe.each(finitePaginateHitsTestsParameters)(
+ 'Finite paginate hits tests',
+ ({
+ searchResponse: { hits, page, hitsPerPage, totalPages },
+ adaptedPagination,
+ }) => {
+ it(`should return ${JSON.stringify(
+ adaptedPagination
)} when hitsPerPage is ${hitsPerPage}, number of page is ${page} and when hits is ${JSON.stringify(
hits
)}`, () => {
- const response = adaptPagination(hits, page, hitsPerPage)
+ const response = adaptPaginationParameters(
+ {
+ hits,
+ page: page,
+ hitsPerPage,
+ processingTimeMs: 0,
+ query: '',
+ totalPages,
+ },
+ { hitsPerPage, page, finite: true }
+ )
- expect(response).toEqual(returnedHits)
+ expect(response).toEqual(adaptedPagination)
+ })
+ }
+)
+
+describe.each(lazyPaginateHitsTestsParameters)(
+ 'Lazy paginate hits tests',
+ ({
+ searchResponse: { hits, limit, offset },
+ paginationState: { page, hitsPerPage },
+ adaptedPagination,
+ }) => {
+ it(`should return ${JSON.stringify(
+ adaptedPagination
+ )} where limit is ${limit} in the response and where the instantsearch pagination context is page: ${page} and hitsPerPage: ${hitsPerPage}`, () => {
+ const response = adaptPaginationParameters(
+ { hits, limit, offset, processingTimeMs: 0, query: '' },
+ { hitsPerPage, page, finite: false }
+ )
+
+ expect(response).toEqual(adaptedPagination)
})
}
)
it('Should throw when hitsPerPage is negative', () => {
try {
- const hits: string[] = []
+ const hits: Array> = []
const hitsPerPage = -1
const page = 0
-
- adaptPagination(hits, page, hitsPerPage)
+ adaptPaginationParameters(
+ { hits, page: page + 1, hitsPerPage, processingTimeMs: 0, query: '' },
+ { hitsPerPage, page, finite: true }
+ )
} catch (e: any) {
expect(e.message).toBe(
'Value too small for "hitsPerPage" parameter, expected integer between 0 and 9223372036854775807'
diff --git a/src/adapter/search-response-adapter/hits-adapter.ts b/src/adapter/search-response-adapter/hits-adapter.ts
index dae04ff8..a4c673ca 100644
--- a/src/adapter/search-response-adapter/hits-adapter.ts
+++ b/src/adapter/search-response-adapter/hits-adapter.ts
@@ -1,24 +1,30 @@
-import type { PaginationContext, SearchContext } from '../../types'
-import { adaptPagination } from './pagination-adapter'
+import type { SearchContext, MeiliSearchResponse } from '../../types'
import { adaptFormattedFields } from './format-adapter'
import { adaptGeoResponse } from './geo-reponse-adapter'
/**
- * @param {Array>} searchResponse
* @param {SearchContext} searchContext
- * @param {PaginationContext} paginationContext
- * @returns {any}
+ * @returns {Array>}
*/
export function adaptHits(
- hits: Array>,
- searchContext: SearchContext,
- paginationContext: PaginationContext
+ searchResponse: MeiliSearchResponse>,
+ searchContext: SearchContext
): any {
const { primaryKey } = searchContext
- const { hitsPerPage, page } = paginationContext
- const paginatedHits = adaptPagination(hits, page, hitsPerPage)
+ const { hits } = searchResponse
+ const {
+ pagination: { finite, hitsPerPage },
+ } = searchContext
- let adaptedHits = paginatedHits.map((hit: Record) => {
+ // if the length of the hits is bigger than the hitsPerPage
+ // It means that there is still pages to come as we append limit by hitsPerPage + 1
+ // In which case we still need to remove the additional hit returned by Meilisearch
+ if (!finite && hits.length > hitsPerPage) {
+ hits.splice(hits.length - 1, 1)
+ }
+
+ let adaptedHits = hits.map((hit: Record) => {
// Creates Hit object compliant with InstantSearch
if (Object.keys(hit).length > 0) {
const {
diff --git a/src/adapter/search-response-adapter/pagination-adapter.ts b/src/adapter/search-response-adapter/pagination-adapter.ts
index ddf69df7..5262d9de 100644
--- a/src/adapter/search-response-adapter/pagination-adapter.ts
+++ b/src/adapter/search-response-adapter/pagination-adapter.ts
@@ -1,21 +1,37 @@
-/**
- * Slice the requested hits based on the pagination position.
- *
- * @param {Record,
- page: number,
+import type {
+ MeiliSearchResponse,
+ PaginationState,
+ InstantSearchPagination,
+} from '../../types'
+
+function adaptNbPages(
+ searchResponse: MeiliSearchResponse>,
hitsPerPage: number
-): Array> {
- if (hitsPerPage < 0) {
- throw new TypeError(
- 'Value too small for "hitsPerPage" parameter, expected integer between 0 and 9223372036854775807'
- )
+): number {
+ if (searchResponse.totalPages != null) {
+ return searchResponse.totalPages
+ }
+ // Avoid dividing by 0
+ if (hitsPerPage === 0) {
+ return 0
+ }
+
+ const { limit = 20, offset = 0, hits } = searchResponse
+ const additionalPage = hits.length >= limit ? 1 : 0
+
+ return offset / hitsPerPage + 1 + additionalPage
+}
+
+export function adaptPaginationParameters(
+ searchResponse: MeiliSearchResponse>,
+ paginationState: PaginationState
+): InstantSearchPagination & { nbPages: number } {
+ const { hitsPerPage, page } = paginationState
+ const nbPages = adaptNbPages(searchResponse, hitsPerPage)
+
+ return {
+ page,
+ nbPages,
+ hitsPerPage,
}
- const start = page * hitsPerPage
- return hits.slice(start, start + hitsPerPage)
}
diff --git a/src/adapter/search-response-adapter/search-response-adapter.ts b/src/adapter/search-response-adapter/search-response-adapter.ts
index 57a25524..6fef2aef 100644
--- a/src/adapter/search-response-adapter/search-response-adapter.ts
+++ b/src/adapter/search-response-adapter/search-response-adapter.ts
@@ -3,16 +3,16 @@ import type {
MeiliSearchResponse,
AlgoliaSearchResponse,
} from '../../types'
-import { ceiledDivision } from '../../utils'
import { adaptHits } from './hits-adapter'
+import { adaptTotalHits } from './total-hits-adapter'
+import { adaptPaginationParameters } from './pagination-adapter'
/**
* Adapt search response from Meilisearch
* to search response compliant with instantsearch.js
*
- * @param {MeiliSearchResponse>} searchResponse
* @param {SearchContext} searchContext
- * @param {PaginationContext} paginationContext
* @returns {{ results: Array> }}
*/
export function adaptSearchResponse(
@@ -20,21 +20,15 @@ export function adaptSearchResponse(
searchContext: SearchContext
): { results: Array> } {
const searchResponseOptionals: Record = {}
+ const { processingTimeMs, query, facetDistribution: facets } = searchResponse
- const facets = searchResponse.facetDistribution
- const { pagination } = searchContext
-
- const nbPages = ceiledDivision(
- searchResponse.hits.length,
- pagination.hitsPerPage
+ const { hitsPerPage, page, nbPages } = adaptPaginationParameters(
+ searchResponse,
+ searchContext.pagination
)
- const hits = adaptHits(searchResponse.hits, searchContext, pagination)
-
- const estimatedTotalHits = searchResponse.estimatedTotalHits
- const processingTimeMs = searchResponse.processingTimeMs
- const query = searchResponse.query
- const { hitsPerPage, page } = pagination
+ const hits = adaptHits(searchResponse, searchContext)
+ const nbHits = adaptTotalHits(searchResponse)
// Create response object compliant with InstantSearch
const adaptedSearchResponse = {
@@ -43,7 +37,7 @@ export function adaptSearchResponse(
page,
facets,
nbPages,
- nbHits: estimatedTotalHits,
+ nbHits,
processingTimeMS: processingTimeMs,
query,
hits,
diff --git a/src/adapter/search-response-adapter/total-hits-adapter.ts b/src/adapter/search-response-adapter/total-hits-adapter.ts
new file mode 100644
index 00000000..b1a8ee2d
--- /dev/null
+++ b/src/adapter/search-response-adapter/total-hits-adapter.ts
@@ -0,0 +1,20 @@
+import type { MeiliSearchResponse } from '../../types'
+
+export function adaptTotalHits(
+ searchResponse: MeiliSearchResponse>
+): number {
+ const {
+ hitsPerPage = 0,
+ totalPages = 0,
+ estimatedTotalHits,
+ totalHits,
+ } = searchResponse
+ if (estimatedTotalHits != null) {
+ return estimatedTotalHits
+ } else if (totalHits != null) {
+ return totalHits
+ }
+
+ // Should not happen but safeguarding just in case
+ return hitsPerPage * totalPages
+}
diff --git a/src/cache/first-facets-distribution.ts b/src/cache/first-facets-distribution.ts
index 13cff973..3af0e29d 100644
--- a/src/cache/first-facets-distribution.ts
+++ b/src/cache/first-facets-distribution.ts
@@ -9,9 +9,6 @@ export async function cacheFirstFacetDistribution(
...searchContext,
// placeholdersearch true to ensure a request is made
placeholderSearch: true,
- // Set paginationTotalHits to ensure limit is set to 0
- // in order to retrieve 0 documents during the default search request
- pagination: { ...searchContext.pagination, paginationTotalHits: 0 },
// query set to empty to ensure retrieving the default facetdistribution
query: '',
}
diff --git a/src/contexts/pagination-context.ts b/src/contexts/pagination-context.ts
index c6d6b32c..40704665 100644
--- a/src/contexts/pagination-context.ts
+++ b/src/contexts/pagination-context.ts
@@ -1,20 +1,21 @@
-import { PaginationContext, PaginationParams } from '../types'
+import { PaginationState } from '../types'
/**
- * @param {AlgoliaMultipleQueriesQuery} searchRequest
- * @param {Context} options
+ * Create the current state of the pagination
+ *
+ * @param {boolean} [finite]
+ * @param {number} [hitsPerPage]
+ * @param {number} [page]
* @returns {SearchContext}
*/
-export function createPaginationContext({
- paginationTotalHits,
- hitsPerPage,
- page,
-}: PaginationParams): // searchContext: SearchContext
-PaginationContext {
+export function createPaginationState(
+ finite?: boolean,
+ hitsPerPage?: number,
+ page?: number
+): PaginationState {
return {
- paginationTotalHits:
- paginationTotalHits != null ? paginationTotalHits : 200,
hitsPerPage: hitsPerPage === undefined ? 20 : hitsPerPage, // 20 is the Meilisearch's default limit value. `hitsPerPage` can be changed with `InsantSearch.configure`.
page: page || 0, // default page is 0 if none is provided
+ finite: !!finite,
}
}
diff --git a/src/contexts/search-context.ts b/src/contexts/search-context.ts
index 8ed759c8..08548d4b 100644
--- a/src/contexts/search-context.ts
+++ b/src/contexts/search-context.ts
@@ -3,10 +3,9 @@ import {
AlgoliaMultipleQueriesQuery,
SearchContext,
FacetDistribution,
- MatchingStrategies,
} from '../types'
-import { createPaginationContext } from './pagination-context'
+import { createPaginationState } from './pagination-context'
/**
* @param {AlgoliaMultipleQueriesQuery} searchRequest
@@ -22,22 +21,21 @@ export function createSearchContext(
const [indexUid, ...sortByArray] = searchRequest.indexName.split(':')
const { params: instantSearchParams } = searchRequest
- const pagination = createPaginationContext({
- paginationTotalHits: options.paginationTotalHits,
- hitsPerPage: instantSearchParams?.hitsPerPage, // 20 by default
- page: instantSearchParams?.page,
- })
+ const paginationState = createPaginationState(
+ options.finitePagination,
+ instantSearchParams?.hitsPerPage,
+ instantSearchParams?.page
+ )
const searchContext: SearchContext = {
...options,
...instantSearchParams,
sort: sortByArray.join(':') || '',
indexUid,
- pagination,
+ pagination: paginationState,
defaultFacetDistribution: defaultFacetDistribution || {},
placeholderSearch: options.placeholderSearch !== false, // true by default
keepZeroFacets: !!options.keepZeroFacets, // false by default
- finitePagination: !!options.finitePagination, // false by default
}
return searchContext
}
diff --git a/src/types/types.ts b/src/types/types.ts
index 972acba2..ebd7b890 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -33,13 +33,12 @@ export const enum MatchingStrategies {
}
export type InstantMeiliSearchOptions = {
- paginationTotalHits?: number
placeholderSearch?: boolean
primaryKey?: string
keepZeroFacets?: boolean
- finitePagination?: boolean
clientAgents?: string[]
matchingStrategy?: MatchingStrategies
+ finitePagination?: boolean
}
export type SearchCacheInterface = {
@@ -61,23 +60,23 @@ export type GeoSearchContext = {
insidePolygon?: ReadonlyArray
}
-export type PaginationContext = {
- paginationTotalHits: number
+// Current state of the pagination
+export type PaginationState = {
+ finite: boolean
hitsPerPage: number
page: number
}
-export type PaginationParams = {
- paginationTotalHits?: number
- hitsPerPage?: number
- page?: number
+export type InstantSearchPagination = {
+ hitsPerPage: number
+ page: number
+ nbPages: number
}
export type SearchContext = Omit &
InstantSearchParams & {
defaultFacetDistribution: FacetDistribution
- pagination: PaginationContext
- finitePagination: boolean
+ pagination: PaginationState
indexUid: string
insideBoundingBox?: InsideBoundingBox
keepZeroFacets?: boolean
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 43ef3313..d05bae6f 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,5 +1,4 @@
export * from './array'
export * from './string'
-export * from './number'
export * from './object'
export * from './validate'
diff --git a/src/utils/number.ts b/tests/assets/number.ts
similarity index 100%
rename from src/utils/number.ts
rename to tests/assets/number.ts
diff --git a/tests/env/react/src/App.js b/tests/env/react/src/App.js
index e992c6f7..295beb62 100644
--- a/tests/env/react/src/App.js
+++ b/tests/env/react/src/App.js
@@ -17,7 +17,6 @@ import './App.css'
import { instantMeiliSearch } from '../../../../src/index'
const searchClient = instantMeiliSearch('http://localhost:7700', 'masterKey', {
- paginationTotalHits: 60,
primaryKey: 'id',
})
diff --git a/tests/pagination.tests.ts b/tests/pagination.tests.ts
index 71c879e2..8bbffa76 100644
--- a/tests/pagination.tests.ts
+++ b/tests/pagination.tests.ts
@@ -1,4 +1,3 @@
-import { instantMeiliSearch } from '../src'
import {
searchClient,
dataset,
@@ -99,100 +98,4 @@ describe('Pagination browser test', () => {
expect(hits.length).toBe(0)
expect(hits).toEqual([])
})
-
- test('pagination total hits ', async () => {
- const customClient = instantMeiliSearch(
- 'http://localhost:7700',
- 'masterKey',
- {
- paginationTotalHits: 1,
- }
- )
-
- const response = await customClient.search([
- {
- indexName: 'movies',
- },
- ])
-
- const hits = response.results[0].hits
- expect(hits.length).toBe(1)
- })
-
- test('zero pagination total hits ', async () => {
- const customClient = instantMeiliSearch(
- 'http://localhost:7700',
- 'masterKey',
- {
- paginationTotalHits: 0,
- }
- )
-
- const response = await customClient.search([
- {
- indexName: 'movies',
- },
- ])
-
- const hits = response.results[0].hits
- expect(hits.length).toBe(0)
- })
-
- test('bigger pagination total hits than nbr hits', async () => {
- const customClient = instantMeiliSearch(
- 'http://localhost:7700',
- 'masterKey',
- {
- paginationTotalHits: 1000,
- }
- )
-
- const response = await customClient.search([
- {
- indexName: 'movies',
- },
- ])
-
- const hits = response.results[0].hits
- expect(hits.length).toBe(6)
- })
-
- test('bigger pagination total hits than nbr hits', async () => {
- const customClient = instantMeiliSearch(
- 'http://localhost:7700',
- 'masterKey',
- {
- paginationTotalHits: 1000,
- }
- )
-
- const response = await customClient.search([
- {
- indexName: 'movies',
- },
- ])
-
- const hits = response.results[0].hits
- expect(hits.length).toBe(6)
- })
-
- test('pagination total hits with finite pagination', async () => {
- const customClient = instantMeiliSearch(
- 'http://localhost:7700',
- 'masterKey',
- {
- paginationTotalHits: 5,
- finitePagination: true,
- }
- )
-
- const response = await customClient.search([
- {
- indexName: 'movies',
- },
- ])
-
- const hits = response.results[0].hits
- expect(hits.length).toBe(5)
- })
})
diff --git a/tests/placeholder-search.tests.ts b/tests/placeholder-search.tests.ts
index b2c9acbd..16c4dac4 100644
--- a/tests/placeholder-search.tests.ts
+++ b/tests/placeholder-search.tests.ts
@@ -1,10 +1,5 @@
import { instantMeiliSearch } from '../src'
-import {
- searchClient,
- dataset,
- Movies,
- meilisearchClient,
-} from './assets/utils'
+import { dataset, Movies, meilisearchClient } from './assets/utils'
describe('Pagination browser test', () => {
beforeAll(async () => {
@@ -19,12 +14,11 @@ describe('Pagination browser test', () => {
await meilisearchClient.index('movies').waitForTask(documentsTask.taskUid)
})
- test('placeholdersearch set to false', async () => {
+ test('placeholdersearch set to true', async () => {
const customClient = instantMeiliSearch(
'http://localhost:7700',
'masterKey',
{
- paginationTotalHits: 5,
placeholderSearch: true,
}
)
@@ -36,15 +30,14 @@ describe('Pagination browser test', () => {
])
const hits = response.results[0].hits
- expect(hits.length).toBe(5)
+ expect(hits.length).toBe(6)
})
- test('placeholdersearch set to true', async () => {
+ test('placeholdersearch set to false', async () => {
const customClient = instantMeiliSearch(
'http://localhost:7700',
'masterKey',
{
- paginationTotalHits: 5,
placeholderSearch: false,
}
)