Skip to content

Commit 5446e6e

Browse files
Merge #707
707: Introduce finitePagination setting: remove default fetch of 200 documents r=bidoubiwa a=bidoubiwa Fixes: #569 fixes: #521 ## Previously By default instant-meilisearch used to fetch 200 documents on every search request. This was needed to ensure we can provide a finite pagination when the user used the `pagination` widget (with the 1, 2, 3, at the bottom of the screen). ## Issue This impacted the speed of the search considerably. Indeed meilisearch is designed to retrieve the most relevant documents, but not all the relevant documents. The more you ask, the more the search engine has to process. There is no difference regarding the search time between retrieving 6 or 7 documents, but between 6 and 200 documents to every request, the impact can be huge. It also impacted users using the `infiniteHits` widget as well while it is not necessary for them. The `infiniteHits` settings does not need to showcase a start and an end to the pagination. It only has to enable the `show more` button whenever there are more hits after the current showcased ones. ## New behavior This PR introduces a new parameter: `finitePagination`. `finitePagination` has a default value of `false`, when set to false, each search requests a limit: `(hitsPerPage + 1) * (page + 1)` - [hitsPerPage](https://github.com/meilisearch/instant-meilisearch#-hitsperpage) (parameter of `instantsearch` and default to `6`) is the number of results on each page, or added on the current page on each `show more` click in case of infiniteHits. In the case if infiniteHits, we need to be able to know if there are more hits to come to enable or disable the `show more` button. Thus we add `1` to `hitsPerPage` so that if Meilisearch returns `hitsPerPage + 1` documents we know it has more. - page: behind the scene value tracking the current page the user is on. For example `1` if you clicked once on `load more`. Because it starts at `0`, we add `+1` for the multiplication. The pagination will stop at `paginationTotalHits` (default 200). When settings `finitePagination` to `true`, one search request will ask for a limit of `paginationTotalHits` as limit. Thus in the default case, one search request will return `200` documents. This number can be changed with `paginationTotalHits` The following will make one request of `7` documents as `hitsPerPage` is at 6 by default. ```js instantMeiliSearch(host, key, { finitePagination: false // default false paginationTotalHits: 60 // default 200 }) ``` You can [change hitsPerPage](https://github.com/meilisearch/instant-meilisearch#-hitsperpage). The following will make one request of 200 documents ```js instantMeiliSearch(host, key, { finitePagination: true // default false paginationTotalHits: 60 // default 200 }) ``` Co-authored-by: Charlotte Vermandel <[email protected]>
2 parents b9807ee + a5d1614 commit 5446e6e

File tree

14 files changed

+262
-55
lines changed

14 files changed

+262
-55
lines changed

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const searchClient = instantMeiliSearch(
8080

8181
- [`placeholderSearch`](#placeholder-search): Enable or disable placeholder search (default: `true`).
8282
- [`paginationTotalHits`](#pagination-total-hits): Maximum total number of hits to create a finite pagination (default: `200`).
83+
- [`finitePagination`](#finite-pagination): Used to work with the [`pagination`](#-pagination) widget (default: `false`) .
8384
- [`primaryKey`](#primary-key): Specify the primary key of your documents (default `undefined`).
8485
- [`keepZeroFacets`](#keep-zero-facets): Show the facets value even when they have 0 matches (default `false`).
8586

@@ -111,17 +112,31 @@ When placeholder search is set to `false`, no results appears when searching on
111112

112113
### Pagination total hits
113114

114-
The total (and finite) number of hits you can browse during pagination when using the [pagination widget](https://www.algolia.com/doc/api-reference/widgets/pagination/js/). If the pagination widget is not used, `paginationTotalHits` is ignored.<br>
115+
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>
115116

116-
Which means that, with a `paginationTotalHits` default value of 200, and `hitsPerPage` default value of 20, you can browse `paginationTotalHits / hitsPerPage` => `200 / 20 = 10` pages during pagination. Each of the 10 pages containing 20 results.<br>
117+
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`.
117118

118-
The default value of `hitsPerPage` is set to `20` but it can be changed with [`InsantSearch.configure`](https://www.algolia.com/doc/api-reference/widgets/configure/js/#examples).<br>
119+
Usage:
119120

120121
```js
121-
{ paginationTotalHits : 20 } // default: 200
122+
{ paginationTotalHits: 50 } // default: 200
122123
```
123124

124-
⚠️ Meilisearch is not designed for pagination and this can lead to performances issues, so the usage of the pagination widget is not encouraged. However, the `paginationTotalHits` parameter lets you implement this pagination with less performance issue as possible: depending on your dataset (the size of each document and the number of documents) you might decrease the value of `paginationTotalHits`.<br>
125+
`hitsPerPage` has a value of `6` by default and can [be customized](#-hitsperpage).
126+
127+
### Finite Pagination
128+
129+
Finite pagination is used when you want to add a numbered pagination at the bottom of your hits (for example: `< << 1, 2, 3 > >>`).
130+
To be able to know the amount of page numbers you have, a search is done requesting `paginationTotalHits` documents (default: `200`).
131+
With the amount of documents returned, instantsearch is able to render the correct amount of numbers in the pagination widget.
132+
133+
Example:
134+
135+
```js
136+
{ finitePagination: true } // default: false
137+
```
138+
139+
⚠️ 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>
125140
More information about Meilisearch and the pagination [here](https://github.com/meilisearch/documentation/issues/561).
126141

127142
### Primary key

jest.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ module.exports = {
66
'jest-watch-typeahead/filename',
77
'jest-watch-typeahead/testname',
88
],
9+
collectCoverage: true,
10+
coveragePathIgnorePatterns: [
11+
'cypress/',
12+
'playgrounds/',
13+
'scripts',
14+
'templates',
15+
'tests',
16+
'__tests__',
17+
],
918
projects: [
1019
{
1120
displayName: 'build',

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": false,
55
"description": "The search client to use Meilisearch with InstantSearch.",
66
"scripts": {
7+
"clear_jest": "jest --clearCache",
78
"cleanup": "shx rm -rf dist/",
89
"test:watch": "yarn test --watch",
910
"test": "jest --runInBand --selectProjects dom --selectProjects node",

src/adapter/search-request-adapter/__tests__/search-params.tests.ts

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { adaptSearchParams } from '../search-params-adapter'
33
test('Adapt basic SearchContext ', () => {
44
const searchParams = adaptSearchParams({
55
indexUid: 'test',
6-
paginationTotalHits: 20,
6+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
77
defaultFacetDistribution: {},
8+
finitePagination: false,
89
})
910
expect(searchParams.attributesToHighlight).toContain('*')
1011
expect(searchParams.attributesToHighlight?.length).toBe(1)
@@ -13,10 +14,11 @@ test('Adapt basic SearchContext ', () => {
1314
test('Adapt SearchContext with filters, sort and no geo rules ', () => {
1415
const searchParams = adaptSearchParams({
1516
indexUid: 'test',
16-
paginationTotalHits: 20,
17+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
1718
facetFilters: [['genres:Drama', 'genres:Thriller'], ['title:Ariel']],
1819
sort: 'id < 1',
1920
defaultFacetDistribution: {},
21+
finitePagination: false,
2022
})
2123

2224
expect(searchParams.filter).toStrictEqual([
@@ -31,11 +33,12 @@ test('Adapt SearchContext with filters, sort and no geo rules ', () => {
3133
test('Adapt SearchContext with filters, sort and geo rules ', () => {
3234
const searchParams = adaptSearchParams({
3335
indexUid: 'test',
34-
paginationTotalHits: 20,
36+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
3537
facetFilters: [['genres:Drama', 'genres:Thriller'], ['title:Ariel']],
3638
insideBoundingBox: '0,0,0,0',
3739
sort: 'id < 1',
3840
defaultFacetDistribution: {},
41+
finitePagination: false,
3942
})
4043

4144
expect(searchParams.filter).toStrictEqual([
@@ -51,10 +54,11 @@ test('Adapt SearchContext with filters, sort and geo rules ', () => {
5154
test('Adapt SearchContext with only facetFilters and geo rules ', () => {
5255
const searchParams = adaptSearchParams({
5356
indexUid: 'test',
54-
paginationTotalHits: 20,
57+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
5558
facetFilters: [['genres:Drama', 'genres:Thriller'], ['title:Ariel']],
5659
insideBoundingBox: '0,0,0,0',
5760
defaultFacetDistribution: {},
61+
finitePagination: false,
5862
})
5963

6064
expect(searchParams.filter).toEqual([
@@ -69,10 +73,11 @@ test('Adapt SearchContext with only facetFilters and geo rules ', () => {
6973
test('Adapt SearchContext with only sort and geo rules ', () => {
7074
const searchParams = adaptSearchParams({
7175
indexUid: 'test',
72-
paginationTotalHits: 20,
76+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
7377
insideBoundingBox: '0,0,0,0',
7478
sort: 'id < 1',
7579
defaultFacetDistribution: {},
80+
finitePagination: false,
7681
})
7782

7883
expect(searchParams.filter).toEqual(['_geoRadius(0.00000, 0.00000, 0)'])
@@ -84,12 +89,97 @@ test('Adapt SearchContext with only sort and geo rules ', () => {
8489
test('Adapt SearchContext with no sort and no filters and geo rules ', () => {
8590
const searchParams = adaptSearchParams({
8691
indexUid: 'test',
87-
paginationTotalHits: 20,
92+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
8893
insideBoundingBox: '0,0,0,0',
8994
defaultFacetDistribution: {},
95+
finitePagination: false,
9096
})
9197

9298
expect(searchParams.filter).toEqual(['_geoRadius(0.00000, 0.00000, 0)'])
9399
expect(searchParams.attributesToHighlight).toContain('*')
94100
expect(searchParams.attributesToHighlight?.length).toBe(1)
95101
})
102+
103+
test('Adapt SearchContext with finite pagination', () => {
104+
const searchParams = adaptSearchParams({
105+
indexUid: 'test',
106+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
107+
insideBoundingBox: '0,0,0,0',
108+
defaultFacetDistribution: {},
109+
finitePagination: true,
110+
})
111+
112+
expect(searchParams.limit).toBe(20)
113+
})
114+
115+
test('Adapt SearchContext with finite pagination on a later page', () => {
116+
const searchParams = adaptSearchParams({
117+
indexUid: 'test',
118+
pagination: { paginationTotalHits: 20, page: 10, hitsPerPage: 6 },
119+
insideBoundingBox: '0,0,0,0',
120+
defaultFacetDistribution: {},
121+
finitePagination: true,
122+
})
123+
124+
expect(searchParams.limit).toBe(20)
125+
})
126+
127+
test('Adapt SearchContext with finite pagination and pagination total hits lower than hitsPerPage', () => {
128+
const searchParams = adaptSearchParams({
129+
indexUid: 'test',
130+
pagination: { paginationTotalHits: 4, page: 0, hitsPerPage: 6 },
131+
insideBoundingBox: '0,0,0,0',
132+
defaultFacetDistribution: {},
133+
finitePagination: true,
134+
})
135+
136+
expect(searchParams.limit).toBe(4)
137+
})
138+
139+
test('Adapt SearchContext with no finite pagination', () => {
140+
const searchParams = adaptSearchParams({
141+
indexUid: 'test',
142+
pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 },
143+
insideBoundingBox: '0,0,0,0',
144+
defaultFacetDistribution: {},
145+
finitePagination: false,
146+
})
147+
148+
expect(searchParams.limit).toBe(7)
149+
})
150+
151+
test('Adapt SearchContext with no finite pagination on page 2', () => {
152+
const searchParams = adaptSearchParams({
153+
indexUid: 'test',
154+
pagination: { paginationTotalHits: 20, page: 1, hitsPerPage: 6 },
155+
insideBoundingBox: '0,0,0,0',
156+
defaultFacetDistribution: {},
157+
finitePagination: false,
158+
})
159+
160+
expect(searchParams.limit).toBe(13)
161+
})
162+
163+
test('Adapt SearchContext with no finite pagination on page higher than paginationTotalHits', () => {
164+
const searchParams = adaptSearchParams({
165+
indexUid: 'test',
166+
pagination: { paginationTotalHits: 20, page: 40, hitsPerPage: 6 },
167+
insideBoundingBox: '0,0,0,0',
168+
defaultFacetDistribution: {},
169+
finitePagination: false,
170+
})
171+
172+
expect(searchParams.limit).toBe(20)
173+
})
174+
175+
test('Adapt SearchContext with no finite pagination and pagination total hits lower than hitsPerPage', () => {
176+
const searchParams = adaptSearchParams({
177+
indexUid: 'test',
178+
pagination: { paginationTotalHits: 4, page: 0, hitsPerPage: 6 },
179+
insideBoundingBox: '0,0,0,0',
180+
defaultFacetDistribution: {},
181+
finitePagination: false,
182+
})
183+
184+
expect(searchParams.limit).toBe(4)
185+
})

src/adapter/search-request-adapter/search-params-adapter.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,27 @@ export function adaptSearchParams(
5959

6060
const placeholderSearch = searchContext.placeholderSearch
6161
const query = searchContext.query
62-
const paginationTotalHits = searchContext.paginationTotalHits
6362

64-
// Limit
65-
if ((!placeholderSearch && query === '') || paginationTotalHits === 0) {
63+
// Pagination
64+
const { pagination } = searchContext
65+
66+
// Limit based on pagination preferences
67+
if (
68+
(!placeholderSearch && query === '') ||
69+
pagination.paginationTotalHits === 0
70+
) {
6671
meiliSearchParams.limit = 0
72+
} else if (searchContext.finitePagination) {
73+
meiliSearchParams.limit = pagination.paginationTotalHits
6774
} else {
68-
meiliSearchParams.limit = paginationTotalHits
75+
const limit = (pagination.page + 1) * pagination.hitsPerPage + 1
76+
// If the limit is bigger than the total hits accepted
77+
// force the limit to that amount
78+
if (limit > pagination.paginationTotalHits) {
79+
meiliSearchParams.limit = pagination.paginationTotalHits
80+
} else {
81+
meiliSearchParams.limit = limit
82+
}
6983
}
7084

7185
const sort = searchContext.sort

src/adapter/search-request-adapter/search-resolver.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,26 @@ export function SearchResolver(cache: SearchCacheInterface) {
2323
searchParams: MeiliSearchParams,
2424
client: MeiliSearch
2525
): Promise<MeiliSearchResponse<Record<string, any>>> {
26-
// Create key with relevant informations
26+
const { pagination } = searchContext
27+
28+
// In case we are in a `finitePagination`, only one big request is made
29+
// containing a total of max the paginationTotalHits (default: 200).
30+
// Thus we dont want the pagination to impact the cache as every
31+
// hits are already cached.
32+
const paginationCache = searchContext.finitePagination ? {} : pagination
33+
34+
// Create cache key containing a unique set of search parameters
2735
const key = cache.formatKey([
2836
searchParams,
2937
searchContext.indexUid,
3038
searchContext.query,
39+
paginationCache,
3140
])
32-
const entry = cache.getEntry(key)
41+
const cachedResponse = cache.getEntry(key)
3342

34-
// Request is cached.
35-
if (entry) return entry
43+
// Check if specific request is already cached with its associated search response.
44+
if (cachedResponse) return cachedResponse
3645

37-
// Cache filters: todo components
3846
const facetsCache = extractFacets(searchContext, searchParams)
3947

4048
// Make search request
Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { SearchContext, PaginationContext } from '../../types'
2-
31
/**
42
* Slice the requested hits based on the pagination position.
53
*
@@ -21,19 +19,3 @@ export function adaptPagination(
2119
const start = page * hitsPerPage
2220
return hits.slice(start, start + hitsPerPage)
2321
}
24-
25-
/**
26-
* @param {AlgoliaMultipleQueriesQuery} searchRequest
27-
* @param {Context} options
28-
* @returns {SearchContext}
29-
*/
30-
export function createPaginationContext(
31-
searchContext: SearchContext
32-
): PaginationContext {
33-
return {
34-
paginationTotalHits: searchContext.paginationTotalHits || 200,
35-
hitsPerPage:
36-
searchContext.hitsPerPage === undefined ? 20 : searchContext.hitsPerPage, // 20 is the Meilisearch's default limit value. `hitsPerPage` can be changed with `InsantSearch.configure`.
37-
page: searchContext?.page || 0, // default page is 0 if none is provided
38-
}
39-
}

src/adapter/search-response-adapter/search-response-adapter.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type {
55
} from '../../types'
66
import { ceiledDivision } from '../../utils'
77
import { adaptHits } from './hits-adapter'
8-
import { createPaginationContext } from './pagination-adapter'
98

109
/**
1110
* Adapt search response from Meilisearch
@@ -23,26 +22,25 @@ export function adaptSearchResponse<T>(
2322
const searchResponseOptionals: Record<string, any> = {}
2423

2524
const facets = searchResponse.facetsDistribution
25+
const { pagination } = searchContext
2626

2727
const exhaustiveFacetsCount = searchResponse?.exhaustiveFacetsCount
2828
if (exhaustiveFacetsCount) {
2929
searchResponseOptionals.exhaustiveFacetsCount = exhaustiveFacetsCount
3030
}
3131

32-
const paginationContext = createPaginationContext(searchContext)
33-
3432
const nbPages = ceiledDivision(
3533
searchResponse.hits.length,
36-
paginationContext.hitsPerPage
34+
pagination.hitsPerPage
3735
)
38-
const hits = adaptHits(searchResponse.hits, searchContext, paginationContext)
36+
const hits = adaptHits(searchResponse.hits, searchContext, pagination)
3937

4038
const exhaustiveNbHits = searchResponse.exhaustiveNbHits
4139
const nbHits = searchResponse.nbHits
4240
const processingTimeMs = searchResponse.processingTimeMs
4341
const query = searchResponse.query
4442

45-
const { hitsPerPage, page } = paginationContext
43+
const { hitsPerPage, page } = pagination
4644

4745
// Create response object compliant with InstantSearch
4846
const adaptedSearchResponse = {

src/client/instant-meilisearch-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
adaptSearchParams,
1212
SearchResolver,
1313
} from '../adapter'
14-
import { createSearchContext } from './contexts'
14+
import { createSearchContext } from '../contexts'
1515
import { SearchCache, cacheFirstFacetsDistribution } from '../cache/'
1616

1717
/**
@@ -79,7 +79,7 @@ export function instantMeiliSearch(
7979
throw new Error(e)
8080
}
8181
},
82-
searchForFacetValues: async function (_) {
82+
searchForFacetValues: async function (_: any) {
8383
return await new Promise((resolve, reject) => {
8484
reject(
8585
new Error('SearchForFacetValues is not compatible with Meilisearch')

src/contexts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { createSearchContext } from './search-context'

0 commit comments

Comments
 (0)