From 638647e72d154c417c9b3677708cd587fb9ec2b9 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Mon, 14 Nov 2022 17:45:00 +0100 Subject: [PATCH 01/12] Update tests to remove totalHitsPerPage --- .../__tests__/search-params.tests.ts | 17 +-- .../__tests__/pagination-adapter.tests.ts | 121 ++++++++++++++---- tests/pagination.tests.ts | 96 +------------- tests/placeholder-search.tests.ts | 9 +- 4 files changed, 110 insertions(+), 133 deletions(-) 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..a00b2938 100644 --- a/src/adapter/search-request-adapter/__tests__/search-params.tests.ts +++ b/src/adapter/search-request-adapter/__tests__/search-params.tests.ts @@ -3,7 +3,7 @@ import { MatchingStrategies } from '../../../types' const DEFAULT_CONTEXT = { indexUid: 'test', - pagination: { paginationTotalHits: 20, page: 0, hitsPerPage: 6 }, + pagination: { page: 0, hitsPerPage: 6 }, defaultFacetDistribution: {}, finitePagination: false, } @@ -104,6 +104,7 @@ describe('Geo rules adapter', () => { }) }) +// TODO: UPDATE describe('Pagination adapter', () => { test('adapting a searchContext with finite pagination', () => { const searchParams = adaptSearchParams({ @@ -117,7 +118,7 @@ describe('Pagination adapter', () => { test('adapting a searchContext with finite pagination on a later page', () => { const searchParams = adaptSearchParams({ ...DEFAULT_CONTEXT, - pagination: { paginationTotalHits: 20, page: 10, hitsPerPage: 6 }, + pagination: { page: 10, hitsPerPage: 6 }, finitePagination: true, }) @@ -127,7 +128,7 @@ describe('Pagination adapter', () => { 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 }, + pagination: { page: 0, hitsPerPage: 6 }, finitePagination: true, }) @@ -145,7 +146,7 @@ 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 }, }) expect(searchParams.limit).toBe(13) @@ -154,7 +155,7 @@ describe('Pagination adapter', () => { 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: 40, hitsPerPage: 6 }, }) expect(searchParams.limit).toBe(20) @@ -163,7 +164,7 @@ describe('Pagination adapter', () => { 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 }, + pagination: { page: 0, hitsPerPage: 6 }, }) expect(searchParams.limit).toBe(4) @@ -173,7 +174,7 @@ describe('Pagination adapter', () => { const searchParams = adaptSearchParams({ ...DEFAULT_CONTEXT, query: '', - pagination: { paginationTotalHits: 4, page: 0, hitsPerPage: 6 }, + pagination: { page: 0, hitsPerPage: 6 }, placeholderSearch: false, }) @@ -184,7 +185,7 @@ describe('Pagination adapter', () => { const searchParams = adaptSearchParams({ ...DEFAULT_CONTEXT, query: '', - pagination: { paginationTotalHits: 200, page: 0, hitsPerPage: 6 }, + pagination: { page: 0, hitsPerPage: 6 }, placeholderSearch: true, }) 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..83d0af34 100644 --- a/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts +++ b/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts @@ -1,4 +1,4 @@ -import { adaptPagination } from '../pagination-adapter' +import { adaptPaginationContext } from '../pagination-adapter' import { ceiledDivision } from '../../../utils' const numberPagesTestParameters = [ @@ -6,26 +6,31 @@ 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", @@ -42,19 +47,31 @@ const paginateHitsTestsParameters = [ hits: [], page: 0, hitsPerPage: 20, - returnedHits: [], + returnedPagination: { + page: 0, + hitsPerPage: 20, + nbPages: 0, + }, }, { hits: [], page: 100, hitsPerPage: 0, - returnedHits: [], + returnedPagination: { + page: 100, + hitsPerPage: 0, + nbPages: 0, + }, }, { hits: [], page: 100, hitsPerPage: 20, - returnedHits: [], + returnedPagination: { + page: 100, + hitsPerPage: 20, + nbPages: 0, + }, }, // Page 0 @@ -62,25 +79,41 @@ const paginateHitsTestsParameters = [ hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 0, hitsPerPage: 20, - returnedHits: [{ id: 1 }, { id: 2 }, { id: 3 }], + returnedPagination: { + page: 0, + hitsPerPage: 20, + nbPages: 1, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 0, hitsPerPage: 0, - returnedHits: [], + returnedPagination: { + page: 0, + hitsPerPage: 0, + nbPages: 0, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 0, hitsPerPage: 20, - returnedHits: [{ id: 1 }, { id: 2 }, { id: 3 }], + returnedPagination: { + page: 0, + hitsPerPage: 20, + nbPages: 1, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 0, hitsPerPage: 2, - returnedHits: [{ id: 1 }, { id: 2 }], + returnedPagination: { + page: 0, + hitsPerPage: 2, + nbPages: 2, + }, }, // Page 1 @@ -88,19 +121,31 @@ const paginateHitsTestsParameters = [ hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 1, hitsPerPage: 2, - returnedHits: [{ id: 3 }], + returnedPagination: { + page: 1, + hitsPerPage: 2, + nbPages: 2, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 1, hitsPerPage: 20, - returnedHits: [], + returnedPagination: { + page: 1, + hitsPerPage: 20, + nbPages: 1, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 1, hitsPerPage: 0, - returnedHits: [], + returnedPagination: { + page: 1, + hitsPerPage: 0, + nbPages: 0, + }, }, // Page 2 @@ -108,19 +153,31 @@ const paginateHitsTestsParameters = [ hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 2, hitsPerPage: 20, - returnedHits: [], + returnedPagination: { + page: 2, + hitsPerPage: 20, + nbPages: 1, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 2, hitsPerPage: 20, - returnedHits: [], + returnedPagination: { + hitsPerPage: 20, + nbPages: 1, + page: 2, + }, }, { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 2, hitsPerPage: 0, - returnedHits: [], + returnedPagination: { + page: 2, + nbPages: 0, + hitsPerPage: 0, + }, }, ] @@ -129,7 +186,6 @@ 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) }) } @@ -137,26 +193,47 @@ describe.each(numberPagesTestParameters)( describe.each(paginateHitsTestsParameters)( 'Paginate hits tests', - ({ hits, page, hitsPerPage, returnedHits }) => { + ({ hits, page, hitsPerPage, returnedPagination }) => { it(`Should return ${JSON.stringify( - returnedHits + returnedPagination )} when hitsPerPage is ${hitsPerPage}, number of page is ${page} and when hits is ${JSON.stringify( hits )}`, () => { - const response = adaptPagination(hits, page, hitsPerPage) + const response = adaptPaginationContext( + { hits, page: page + 1, hitsPerPage, processingTimeMs: 0, query: '' }, + { hitsPerPage, page } + ) + expect(response).toEqual(returnedPagination) + }) + } +) - expect(response).toEqual(returnedHits) +describe.each(paginateHitsTestsParameters)( + 'Paginate hits tests', + ({ hits, page, hitsPerPage, returnedPagination }) => { + it(`Should return ${JSON.stringify( + returnedPagination + )} when hitsPerPage is ${hitsPerPage}, number of page is ${page} and when hits is ${JSON.stringify( + hits + )} but there is no page and hitsPerPage fields returned by Meilisearch`, () => { + const response = adaptPaginationContext( + { hits, processingTimeMs: 0, query: '' }, + { hitsPerPage, page } + ) + expect(response).toEqual(returnedPagination) }) } ) it('Should throw when hitsPerPage is negative', () => { try { - const hits: string[] = [] + const hits: Array> = [] const hitsPerPage = -1 const page = 0 - - adaptPagination(hits, page, hitsPerPage) + adaptPaginationContext( + { hits, page: page + 1, hitsPerPage, processingTimeMs: 0, query: '' }, + { hitsPerPage, page } + ) } catch (e: any) { expect(e.message).toBe( 'Value too small for "hitsPerPage" parameter, expected integer between 0 and 9223372036854775807' diff --git a/tests/pagination.tests.ts b/tests/pagination.tests.ts index 71c879e2..b00d4157 100644 --- a/tests/pagination.tests.ts +++ b/tests/pagination.tests.ts @@ -100,99 +100,5 @@ describe('Pagination browser test', () => { 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) - }) + // TODO: Add tests on finitepagination }) diff --git a/tests/placeholder-search.tests.ts b/tests/placeholder-search.tests.ts index b2c9acbd..1b9517f5 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 () => { @@ -24,7 +19,6 @@ describe('Pagination browser test', () => { 'http://localhost:7700', 'masterKey', { - paginationTotalHits: 5, placeholderSearch: true, } ) @@ -44,7 +38,6 @@ describe('Pagination browser test', () => { 'http://localhost:7700', 'masterKey', { - paginationTotalHits: 5, placeholderSearch: false, } ) From 5be50ba0ede7aa09db711908d23887ccb4290526 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 12:46:33 +0100 Subject: [PATCH 02/12] Put finitePagination in pagination context --- .../search-response-adapter/hits-adapter.ts | 10 +-- .../pagination-adapter.ts | 64 +++++++++++++------ .../search-response-adapter.ts | 25 +++----- .../total-hits-adapter.ts | 18 ++++++ src/contexts/pagination-context.ts | 5 +- src/contexts/search-context.ts | 3 +- src/types/types.ts | 9 ++- 7 files changed, 84 insertions(+), 50 deletions(-) create mode 100644 src/adapter/search-response-adapter/total-hits-adapter.ts diff --git a/src/adapter/search-response-adapter/hits-adapter.ts b/src/adapter/search-response-adapter/hits-adapter.ts index dae04ff8..79b2b77d 100644 --- a/src/adapter/search-response-adapter/hits-adapter.ts +++ b/src/adapter/search-response-adapter/hits-adapter.ts @@ -1,5 +1,4 @@ -import type { PaginationContext, SearchContext } from '../../types' -import { adaptPagination } from './pagination-adapter' +import type { SearchContext } from '../../types' import { adaptFormattedFields } from './format-adapter' import { adaptGeoResponse } from './geo-reponse-adapter' @@ -11,14 +10,11 @@ import { adaptGeoResponse } from './geo-reponse-adapter' */ export function adaptHits( hits: Array>, - searchContext: SearchContext, - paginationContext: PaginationContext + searchContext: SearchContext ): any { const { primaryKey } = searchContext - const { hitsPerPage, page } = paginationContext - const paginatedHits = adaptPagination(hits, page, hitsPerPage) - let adaptedHits = paginatedHits.map((hit: Record) => { + 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..d320f8da 100644 --- a/src/adapter/search-response-adapter/pagination-adapter.ts +++ b/src/adapter/search-response-adapter/pagination-adapter.ts @@ -1,21 +1,49 @@ -/** - * Slice the requested hits based on the pagination position. - * - * @param {Record, - page: number, +import type { MeiliSearchResponse, PaginationContext } from '../../types' +import { ceiledDivision } from '../../utils' + +function adaptHitsPerPage( + searchResponse: MeiliSearchResponse>, + paginationContext: PaginationContext +): number { + if (searchResponse.hitsPerPage) { + return searchResponse.hitsPerPage + } + + return paginationContext.hitsPerPage +} + +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) { + return searchResponse.totalPages + } + + return ceiledDivision(searchResponse.hits.length, hitsPerPage) +} + +function adaptPage( + searchResponse: MeiliSearchResponse>, + paginationContext: PaginationContext +): number { + if (searchResponse.page) { + return searchResponse.page - 1 + } + + return paginationContext.page +} + +export function adaptPaginationContext( + searchResponse: MeiliSearchResponse>, + paginationContext: PaginationContext +): PaginationContext & { nbPages: number } { + const hitsPerPage = adaptHitsPerPage(searchResponse, paginationContext) + const nbPages = adaptNbPages(searchResponse, hitsPerPage) + const page = adaptPage(searchResponse, paginationContext) + 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..92fa343a 100644 --- a/src/adapter/search-response-adapter/search-response-adapter.ts +++ b/src/adapter/search-response-adapter/search-response-adapter.ts @@ -3,14 +3,15 @@ import type { MeiliSearchResponse, AlgoliaSearchResponse, } from '../../types' -import { ceiledDivision } from '../../utils' import { adaptHits } from './hits-adapter' +import { adaptTotalHits } from './total-hits-adapter' +import { adaptPaginationContext } 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> }} @@ -20,21 +21,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 } = adaptPaginationContext( + searchResponse, + searchContext.pagination ) - const hits = adaptHits(searchResponse.hits, searchContext, pagination) - - const estimatedTotalHits = searchResponse.estimatedTotalHits - const processingTimeMs = searchResponse.processingTimeMs - const query = searchResponse.query + const hits = adaptHits(searchResponse.hits, searchContext) - const { hitsPerPage, page } = pagination + const nbHits = adaptTotalHits(searchResponse) // Create response object compliant with InstantSearch const adaptedSearchResponse = { @@ -43,7 +38,7 @@ export function adaptSearchResponse( page, facets, nbPages, - nbHits: estimatedTotalHits, + nbHits, // TODO: UPDATE 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..b2768844 --- /dev/null +++ b/src/adapter/search-response-adapter/total-hits-adapter.ts @@ -0,0 +1,18 @@ +import type { MeiliSearchResponse } from '../../types' + +export function adaptTotalHits( + searchResponse: MeiliSearchResponse> +): number { + const { + hitsPerPage = 0, + totalPages = 0, + estimatedTotalHits = undefined, + totalHits = undefined, + } = searchResponse + if (estimatedTotalHits !== undefined) { + return estimatedTotalHits + } else if (totalHits !== undefined) { + return totalHits + } + return hitsPerPage * totalPages +} diff --git a/src/contexts/pagination-context.ts b/src/contexts/pagination-context.ts index c6d6b32c..4a640a01 100644 --- a/src/contexts/pagination-context.ts +++ b/src/contexts/pagination-context.ts @@ -6,15 +6,14 @@ import { PaginationContext, PaginationParams } from '../types' * @returns {SearchContext} */ export function createPaginationContext({ - paginationTotalHits, + finite, hitsPerPage, page, }: PaginationParams): // searchContext: SearchContext PaginationContext { 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..23a0bf00 100644 --- a/src/contexts/search-context.ts +++ b/src/contexts/search-context.ts @@ -23,7 +23,7 @@ export function createSearchContext( const { params: instantSearchParams } = searchRequest const pagination = createPaginationContext({ - paginationTotalHits: options.paginationTotalHits, + finite: !!options.finitePagination, hitsPerPage: instantSearchParams?.hitsPerPage, // 20 by default page: instantSearchParams?.page, }) @@ -37,7 +37,6 @@ export function createSearchContext( 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..83b35a1a 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 = { @@ -62,13 +61,14 @@ export type GeoSearchContext = { } export type PaginationContext = { - paginationTotalHits: number + finite?: boolean hitsPerPage: number page: number } +// TODO: why do you exist? export type PaginationParams = { - paginationTotalHits?: number + finite?: boolean hitsPerPage?: number page?: number } @@ -77,7 +77,6 @@ export type SearchContext = Omit & InstantSearchParams & { defaultFacetDistribution: FacetDistribution pagination: PaginationContext - finitePagination: boolean indexUid: string insideBoundingBox?: InsideBoundingBox keepZeroFacets?: boolean From 48ffbbc48592412acadb3f43d6c6943ec9a057e7 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 12:46:52 +0100 Subject: [PATCH 03/12] Update adapting pagination --- .../search-params-adapter.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/adapter/search-request-adapter/search-params-adapter.ts b/src/adapter/search-request-adapter/search-params-adapter.ts index 46771e57..982874d1 100644 --- a/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/src/adapter/search-request-adapter/search-params-adapter.ts @@ -84,23 +84,12 @@ 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 (!placeholderSearch && query === '') { + meiliSearchParams.hitsPerPage = 0 + meiliSearchParams.page = pagination.page + 1 } 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 - } + meiliSearchParams.page = pagination.page + 1 + meiliSearchParams.hitsPerPage = pagination.hitsPerPage } }, addSort() { From 5d94095b8a81cc0266dd8b8554cfd1c242fbd219 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 12:47:07 +0100 Subject: [PATCH 04/12] Remove finitePagination from search context --- src/adapter/search-request-adapter/search-params-adapter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/adapter/search-request-adapter/search-params-adapter.ts b/src/adapter/search-request-adapter/search-params-adapter.ts index 982874d1..35bd3798 100644 --- a/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/src/adapter/search-request-adapter/search-params-adapter.ts @@ -29,7 +29,6 @@ export function MeiliParamsCreator(searchContext: SearchContext) { highlightPostTag, placeholderSearch, query, - finitePagination, sort, pagination, matchingStrategy, From bae030f9e8827b9431137fa85410645427a8aec0 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 12:47:33 +0100 Subject: [PATCH 05/12] Remove paginationTotalHits from playgrounds --- playgrounds/react/src/App.js | 1 - .../__tests__/search-params.tests.ts | 16 ++++++++-------- .../search-request-adapter/search-resolver.ts | 3 ++- src/cache/first-facets-distribution.ts | 4 ++-- tests/env/react/src/App.js | 1 - 5 files changed, 12 insertions(+), 13 deletions(-) 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 a00b2938..ec9aa152 100644 --- a/src/adapter/search-request-adapter/__tests__/search-params.tests.ts +++ b/src/adapter/search-request-adapter/__tests__/search-params.tests.ts @@ -152,14 +152,14 @@ describe('Pagination adapter', () => { expect(searchParams.limit).toBe(13) }) - test('adapting a searchContext with no finite pagination on page higher than paginationTotalHits', () => { - const searchParams = adaptSearchParams({ - ...DEFAULT_CONTEXT, - pagination: { page: 40, hitsPerPage: 6 }, - }) - - expect(searchParams.limit).toBe(20) - }) + // test('adapting a searchContext with no finite pagination on page higher than paginationTotalHits', () => { + // const searchParams = adaptSearchParams({ + // ...DEFAULT_CONTEXT, + // pagination: { 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({ diff --git a/src/adapter/search-request-adapter/search-resolver.ts b/src/adapter/search-request-adapter/search-resolver.ts index 7dcc7c4d..a1e666b1 100644 --- a/src/adapter/search-request-adapter/search-resolver.ts +++ b/src/adapter/search-request-adapter/search-resolver.ts @@ -30,10 +30,11 @@ export function SearchResolver( const { pagination } = searchContext // In case we are in a `finitePagination`, only one big request is made + // TODO: update // 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 + // TODO: const paginationCache = searchContext.finitePagination ? {} : pagination // Create cache key containing a unique set of search parameters const key = cache.formatKey([ diff --git a/src/cache/first-facets-distribution.ts b/src/cache/first-facets-distribution.ts index 13cff973..a7865576 100644 --- a/src/cache/first-facets-distribution.ts +++ b/src/cache/first-facets-distribution.ts @@ -9,9 +9,9 @@ export async function cacheFirstFacetDistribution( ...searchContext, // placeholdersearch true to ensure a request is made placeholderSearch: true, - // Set paginationTotalHits to ensure limit is set to 0 + // TODO: UPDATe // in order to retrieve 0 documents during the default search request - pagination: { ...searchContext.pagination, paginationTotalHits: 0 }, + pagination: { ...searchContext.pagination }, // query set to empty to ensure retrieving the default facetdistribution query: '', } 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', }) From 00a5f08aa3594271c3f25ad7c952c4b3ec6a0bdd Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 18:25:33 +0100 Subject: [PATCH 06/12] Implement finite pagination using page and hits per page --- .../search-params-adapter.ts | 56 +++++++++++++++++-- .../search-request-adapter/search-resolver.ts | 15 +---- .../search-response-adapter/hits-adapter.ts | 20 +++++-- .../pagination-adapter.ts | 48 ++++++++-------- .../search-response-adapter.ts | 9 ++- src/contexts/pagination-context.ts | 20 ++++--- src/contexts/search-context.ts | 15 +++-- src/types/types.ts | 16 +++--- src/utils/number.ts | 13 +++++ 9 files changed, 137 insertions(+), 75 deletions(-) diff --git a/src/adapter/search-request-adapter/search-params-adapter.ts b/src/adapter/search-request-adapter/search-params-adapter.ts index 35bd3798..48e30f97 100644 --- a/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/src/adapter/search-request-adapter/search-params-adapter.ts @@ -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. @@ -83,12 +118,23 @@ export function MeiliParamsCreator(searchContext: SearchContext) { } }, addPagination() { - if (!placeholderSearch && query === '') { - meiliSearchParams.hitsPerPage = 0 - meiliSearchParams.page = pagination.page + 1 + if (pagination.finite) { + const { hitsPerPage, page } = setFinitePagination( + pagination.hitsPerPage, + pagination.page, + query, + placeholderSearch + ) + meiliSearchParams.hitsPerPage = hitsPerPage + meiliSearchParams.page = page } else { - meiliSearchParams.page = pagination.page + 1 - meiliSearchParams.hitsPerPage = pagination.hitsPerPage + const { limit } = setScrollPagination( + pagination.hitsPerPage, + pagination.page, + query, + placeholderSearch + ) + meiliSearchParams.limit = limit } }, addSort() { diff --git a/src/adapter/search-request-adapter/search-resolver.ts b/src/adapter/search-request-adapter/search-resolver.ts index a1e666b1..13cb15cc 100644 --- a/src/adapter/search-request-adapter/search-resolver.ts +++ b/src/adapter/search-request-adapter/search-resolver.ts @@ -27,28 +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 - // TODO: update - // 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. - // TODO: 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 @@ -57,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/hits-adapter.ts b/src/adapter/search-response-adapter/hits-adapter.ts index 79b2b77d..0b89895a 100644 --- a/src/adapter/search-response-adapter/hits-adapter.ts +++ b/src/adapter/search-response-adapter/hits-adapter.ts @@ -1,19 +1,29 @@ -import type { SearchContext } from '../../types' +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>, + searchResponse: MeiliSearchResponse>, searchContext: SearchContext ): any { const { primaryKey } = searchContext + const { hits } = searchResponse + const { + pagination: { finite, page, hitsPerPage }, + } = searchContext + if (!finite) { + const deleteCount = page * hitsPerPage + hits.splice(0, deleteCount) + if (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) { diff --git a/src/adapter/search-response-adapter/pagination-adapter.ts b/src/adapter/search-response-adapter/pagination-adapter.ts index d320f8da..f4ac4b88 100644 --- a/src/adapter/search-response-adapter/pagination-adapter.ts +++ b/src/adapter/search-response-adapter/pagination-adapter.ts @@ -1,22 +1,15 @@ -import type { MeiliSearchResponse, PaginationContext } from '../../types' -import { ceiledDivision } from '../../utils' - -function adaptHitsPerPage( - searchResponse: MeiliSearchResponse>, - paginationContext: PaginationContext -): number { - if (searchResponse.hitsPerPage) { - return searchResponse.hitsPerPage - } - - return paginationContext.hitsPerPage -} +import type { + MeiliSearchResponse, + PaginationState, + InstantSearchPagination, +} from '../../types' +import { ceiledDivision, floorDivision } from '../../utils' function adaptNbPages( searchResponse: MeiliSearchResponse>, hitsPerPage: number ): number { - if (searchResponse.totalPages) { + if (searchResponse.totalPages != null) { return searchResponse.totalPages } @@ -25,22 +18,31 @@ function adaptNbPages( function adaptPage( searchResponse: MeiliSearchResponse>, - paginationContext: PaginationContext + hitsPerPage: number ): number { - if (searchResponse.page) { - return searchResponse.page - 1 + const { limit, page } = searchResponse + if (page === 0) { + return 0 + } + if (page != null) { + return page - 1 } - return paginationContext.page + if (!limit) { + return 0 + } else { + return floorDivision(limit, hitsPerPage) - 1 + } } -export function adaptPaginationContext( +export function adaptPaginationParameters( searchResponse: MeiliSearchResponse>, - paginationContext: PaginationContext -): PaginationContext & { nbPages: number } { - const hitsPerPage = adaptHitsPerPage(searchResponse, paginationContext) + paginationState: PaginationState +): InstantSearchPagination & { nbPages: number } { + const hitsPerPage = paginationState.hitsPerPage const nbPages = adaptNbPages(searchResponse, hitsPerPage) - const page = adaptPage(searchResponse, paginationContext) + const page = adaptPage(searchResponse, hitsPerPage) + return { page, nbPages, diff --git a/src/adapter/search-response-adapter/search-response-adapter.ts b/src/adapter/search-response-adapter/search-response-adapter.ts index 92fa343a..6fef2aef 100644 --- a/src/adapter/search-response-adapter/search-response-adapter.ts +++ b/src/adapter/search-response-adapter/search-response-adapter.ts @@ -5,7 +5,7 @@ import type { } from '../../types' import { adaptHits } from './hits-adapter' import { adaptTotalHits } from './total-hits-adapter' -import { adaptPaginationContext } from './pagination-adapter' +import { adaptPaginationParameters } from './pagination-adapter' /** * Adapt search response from Meilisearch @@ -13,7 +13,6 @@ import { adaptPaginationContext } from './pagination-adapter' * * @param {MeiliSearchResponse>} searchResponse * @param {SearchContext} searchContext - * @param {PaginationContext} paginationContext * @returns {{ results: Array> }} */ export function adaptSearchResponse( @@ -23,12 +22,12 @@ export function adaptSearchResponse( const searchResponseOptionals: Record = {} const { processingTimeMs, query, facetDistribution: facets } = searchResponse - const { hitsPerPage, page, nbPages } = adaptPaginationContext( + const { hitsPerPage, page, nbPages } = adaptPaginationParameters( searchResponse, searchContext.pagination ) - const hits = adaptHits(searchResponse.hits, searchContext) + const hits = adaptHits(searchResponse, searchContext) const nbHits = adaptTotalHits(searchResponse) // Create response object compliant with InstantSearch @@ -38,7 +37,7 @@ export function adaptSearchResponse( page, facets, nbPages, - nbHits, // TODO: UPDATE + nbHits, processingTimeMS: processingTimeMs, query, hits, diff --git a/src/contexts/pagination-context.ts b/src/contexts/pagination-context.ts index 4a640a01..40704665 100644 --- a/src/contexts/pagination-context.ts +++ b/src/contexts/pagination-context.ts @@ -1,16 +1,18 @@ -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({ - finite, - hitsPerPage, - page, -}: PaginationParams): // searchContext: SearchContext -PaginationContext { +export function createPaginationState( + finite?: boolean, + hitsPerPage?: number, + page?: number +): PaginationState { return { 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 diff --git a/src/contexts/search-context.ts b/src/contexts/search-context.ts index 23a0bf00..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,18 +21,18 @@ export function createSearchContext( const [indexUid, ...sortByArray] = searchRequest.indexName.split(':') const { params: instantSearchParams } = searchRequest - const pagination = createPaginationContext({ - finite: !!options.finitePagination, - 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 diff --git a/src/types/types.ts b/src/types/types.ts index 83b35a1a..ebd7b890 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -60,23 +60,23 @@ export type GeoSearchContext = { insidePolygon?: ReadonlyArray } -export type PaginationContext = { - finite?: boolean +// Current state of the pagination +export type PaginationState = { + finite: boolean hitsPerPage: number page: number } -// TODO: why do you exist? -export type PaginationParams = { - finite?: boolean - hitsPerPage?: number - page?: number +export type InstantSearchPagination = { + hitsPerPage: number + page: number + nbPages: number } export type SearchContext = Omit & InstantSearchParams & { defaultFacetDistribution: FacetDistribution - pagination: PaginationContext + pagination: PaginationState indexUid: string insideBoundingBox?: InsideBoundingBox keepZeroFacets?: boolean diff --git a/src/utils/number.ts b/src/utils/number.ts index 1eb11d7f..6222b698 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -10,3 +10,16 @@ export function ceiledDivision(dividend: number, divisor: number): number { } return 0 } + +/** + * @param {number} dividend + * @param {number} divisor + * @returns number + */ +export function floorDivision(dividend: number, divisor: number): number { + if (divisor > 0) { + const NumberPages = Math.floor(dividend / divisor) // total number of pages rounded up to the next largest integer. + return NumberPages + } + return 0 +} From 543bbe0fb2a3a81a3c8fcbda1a5d9a867c977e0a Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 18:27:41 +0100 Subject: [PATCH 07/12] Add tests --- .../__tests__/search-params.tests.ts | 62 +-- .../__tests__/pagination-adapter.tests.ts | 357 ++++++++++++++---- .../pagination-adapter.ts | 6 +- .../total-hits-adapter.ts | 2 + src/cache/first-facets-distribution.ts | 3 - tests/pagination.tests.ts | 3 - tests/placeholder-search.tests.ts | 6 +- 7 files changed, 304 insertions(+), 135 deletions(-) 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 ec9aa152..514be7c3 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: { page: 0, hitsPerPage: 6 }, + pagination: { page: 0, hitsPerPage: 6, finite: false }, defaultFacetDistribution: {}, - finitePagination: false, } describe('Parameters adapter', () => { @@ -104,38 +103,28 @@ describe('Geo rules adapter', () => { }) }) -// TODO: UPDATE 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: { 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: { 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, }) @@ -146,49 +135,32 @@ describe('Pagination adapter', () => { test('adapting a searchContext with no finite pagination on page 2', () => { const searchParams = adaptSearchParams({ ...DEFAULT_CONTEXT, - pagination: { 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: { 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: { 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: { 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: { 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) }) }) 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 83d0af34..7cef1622 100644 --- a/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts +++ b/src/adapter/search-response-adapter/__tests__/pagination-adapter.tests.ts @@ -1,4 +1,4 @@ -import { adaptPaginationContext } from '../pagination-adapter' +import { adaptPaginationParameters } from '../pagination-adapter' import { ceiledDivision } from '../../../utils' const numberPagesTestParameters = [ @@ -41,34 +41,33 @@ const numberPagesTestParameters = [ }, ] -const paginateHitsTestsParameters = [ +const finitePaginateHitsTestsParameters = [ // Empty hits { - hits: [], - page: 0, - hitsPerPage: 20, - returnedPagination: { + searchResponse: { + hits: [], + page: 0, + hitsPerPage: 20, + totalPages: 0, + }, + adaptedPagination: { page: 0, hitsPerPage: 20, nbPages: 0, }, }, { - hits: [], - page: 100, - hitsPerPage: 0, - returnedPagination: { - page: 100, + searchResponse: { hits: [], page: 100, hitsPerPage: 0, totalPages: 0 }, + adaptedPagination: { + page: 99, hitsPerPage: 0, nbPages: 0, }, }, { - hits: [], - page: 100, - hitsPerPage: 20, - returnedPagination: { - page: 100, + searchResponse: { hits: [], page: 100, hitsPerPage: 20, totalPages: 0 }, + adaptedPagination: { + page: 99, hitsPerPage: 20, nbPages: 0, }, @@ -76,40 +75,52 @@ const paginateHitsTestsParameters = [ // Page 0 { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 0, - hitsPerPage: 20, - returnedPagination: { + 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, - returnedPagination: { + 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, - returnedPagination: { + 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, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + page: 0, + hitsPerPage: 2, + totalPages: 2, + }, + adaptedPagination: { page: 0, hitsPerPage: 2, nbPages: 2, @@ -118,66 +129,238 @@ const paginateHitsTestsParameters = [ // Page 1 { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 1, - hitsPerPage: 2, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 1, hitsPerPage: 2, + totalPages: 2, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 2, nbPages: 2, }, }, { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 1, - hitsPerPage: 20, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 1, hitsPerPage: 20, + totalPages: 1, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 20, nbPages: 1, }, }, { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 1, - hitsPerPage: 0, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 1, hitsPerPage: 0, + totalPages: 0, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 0, nbPages: 0, }, }, // Page 2 { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 2, - hitsPerPage: 20, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 2, hitsPerPage: 20, + totalPages: 1, + }, + adaptedPagination: { + page: 1, + hitsPerPage: 20, nbPages: 1, }, }, { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 2, - hitsPerPage: 20, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + page: 2, + hitsPerPage: 20, + totalPages: 1, + }, + adaptedPagination: { hitsPerPage: 20, nbPages: 1, - page: 2, + page: 1, }, }, { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - page: 2, - hitsPerPage: 0, - returnedPagination: { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], page: 2, + hitsPerPage: 0, + totalPages: 0, + }, + adaptedPagination: { + page: 1, + nbPages: 0, + hitsPerPage: 0, + }, + }, +] + +const scrollPaginateHitsTestsParameters = [ + // Empty hits + { + searchResponse: { + hits: [], + limit: 0, + }, + searchContext: { + hitsPerPage: 20, + page: 0, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 20, + nbPages: 0, + }, + }, + { + searchResponse: { hits: [], limit: 0 }, + searchContext: { + hitsPerPage: 0, + page: 100, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 0, + nbPages: 0, + }, + }, + { + searchResponse: { hits: [], limit: 20 }, + searchContext: { + page: 100, + hitsPerPage: 20, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 20, nbPages: 0, + }, + }, + + // Page 0 + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + limit: 20, + }, + searchContext: { + page: 0, + hitsPerPage: 20, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 20, + nbPages: 1, + }, + }, + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + limit: 0, + }, + searchContext: { + page: 0, hitsPerPage: 0, }, + adaptedPagination: { + page: 0, + hitsPerPage: 0, + nbPages: 0, + }, + }, + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + limit: 20, + }, + searchContext: { + page: 0, + hitsPerPage: 20, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 20, + nbPages: 1, + }, + }, + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + limit: 2, + }, + searchContext: { + page: 0, + hitsPerPage: 2, + }, + adaptedPagination: { + page: 0, + hitsPerPage: 2, + nbPages: 2, + }, + }, + + // Page 1 + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + limit: 2, + }, + searchContext: { + page: 1, + hitsPerPage: 1, + }, + adaptedPagination: { + page: 1, + hitsPerPage: 1, + nbPages: 3, + }, + }, + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }], + limit: 5, + }, + searchContext: { + page: 1, + hitsPerPage: 2, + }, + adaptedPagination: { + page: 1, + hitsPerPage: 2, + nbPages: 3, + }, + }, + + // Page 2 + { + searchResponse: { + hits: [{ id: 1 }, { id: 2 }, { id: 3 }], + limit: 3, + }, + searchContext: { + page: 2, + hitsPerPage: 1, + }, + adaptedPagination: { + page: 2, + hitsPerPage: 1, + nbPages: 3, + }, }, ] @@ -191,36 +374,50 @@ describe.each(numberPagesTestParameters)( } ) -describe.each(paginateHitsTestsParameters)( - 'Paginate hits tests', - ({ hits, page, hitsPerPage, returnedPagination }) => { - it(`Should return ${JSON.stringify( - returnedPagination +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 = adaptPaginationContext( - { hits, page: page + 1, hitsPerPage, processingTimeMs: 0, query: '' }, - { hitsPerPage, page } + const response = adaptPaginationParameters( + { + hits, + page: page, + hitsPerPage, + processingTimeMs: 0, + query: '', + totalPages, + }, + { hitsPerPage, page, finite: true } ) - expect(response).toEqual(returnedPagination) + + expect(response).toEqual(adaptedPagination) }) } ) -describe.each(paginateHitsTestsParameters)( - 'Paginate hits tests', - ({ hits, page, hitsPerPage, returnedPagination }) => { - it(`Should return ${JSON.stringify( - returnedPagination - )} when hitsPerPage is ${hitsPerPage}, number of page is ${page} and when hits is ${JSON.stringify( - hits - )} but there is no page and hitsPerPage fields returned by Meilisearch`, () => { - const response = adaptPaginationContext( - { hits, processingTimeMs: 0, query: '' }, - { hitsPerPage, page } +describe.each(scrollPaginateHitsTestsParameters)( + 'Finite paginate hits tests', + ({ + searchResponse: { hits, limit }, + searchContext: { page, hitsPerPage }, + adaptedPagination, + }) => { + it(`should return ${JSON.stringify( + adaptedPagination + )} where limit is ${0} in the response and where the instantsearch pagination context is page: ${page} and hitsPerPage: ${hitsPerPage}`, () => { + const response = adaptPaginationParameters( + { hits, limit, processingTimeMs: 0, query: '' }, + { hitsPerPage, page, finite: false } ) - expect(response).toEqual(returnedPagination) + + expect(response).toEqual(adaptedPagination) }) } ) @@ -230,9 +427,9 @@ it('Should throw when hitsPerPage is negative', () => { const hits: Array> = [] const hitsPerPage = -1 const page = 0 - adaptPaginationContext( + adaptPaginationParameters( { hits, page: page + 1, hitsPerPage, processingTimeMs: 0, query: '' }, - { hitsPerPage, page } + { hitsPerPage, page, finite: true } ) } catch (e: any) { expect(e.message).toBe( diff --git a/src/adapter/search-response-adapter/pagination-adapter.ts b/src/adapter/search-response-adapter/pagination-adapter.ts index f4ac4b88..632b4352 100644 --- a/src/adapter/search-response-adapter/pagination-adapter.ts +++ b/src/adapter/search-response-adapter/pagination-adapter.ts @@ -21,13 +21,17 @@ function adaptPage( hitsPerPage: number ): number { const { limit, page } = searchResponse + + // Finite pagination environment if (page === 0) { + // Should not happen but safeguarding in case it does return 0 } if (page != null) { return page - 1 } + // Scroll pagination environment if (!limit) { return 0 } else { @@ -39,7 +43,7 @@ export function adaptPaginationParameters( searchResponse: MeiliSearchResponse>, paginationState: PaginationState ): InstantSearchPagination & { nbPages: number } { - const hitsPerPage = paginationState.hitsPerPage + const { hitsPerPage } = paginationState const nbPages = adaptNbPages(searchResponse, hitsPerPage) const page = adaptPage(searchResponse, hitsPerPage) diff --git a/src/adapter/search-response-adapter/total-hits-adapter.ts b/src/adapter/search-response-adapter/total-hits-adapter.ts index b2768844..518e8765 100644 --- a/src/adapter/search-response-adapter/total-hits-adapter.ts +++ b/src/adapter/search-response-adapter/total-hits-adapter.ts @@ -14,5 +14,7 @@ export function adaptTotalHits( } else if (totalHits !== undefined) { 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 a7865576..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, - // TODO: UPDATe - // in order to retrieve 0 documents during the default search request - pagination: { ...searchContext.pagination }, // query set to empty to ensure retrieving the default facetdistribution query: '', } diff --git a/tests/pagination.tests.ts b/tests/pagination.tests.ts index b00d4157..8bbffa76 100644 --- a/tests/pagination.tests.ts +++ b/tests/pagination.tests.ts @@ -1,4 +1,3 @@ -import { instantMeiliSearch } from '../src' import { searchClient, dataset, @@ -99,6 +98,4 @@ describe('Pagination browser test', () => { expect(hits.length).toBe(0) expect(hits).toEqual([]) }) - - // TODO: Add tests on finitepagination }) diff --git a/tests/placeholder-search.tests.ts b/tests/placeholder-search.tests.ts index 1b9517f5..16c4dac4 100644 --- a/tests/placeholder-search.tests.ts +++ b/tests/placeholder-search.tests.ts @@ -14,7 +14,7 @@ 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', @@ -30,10 +30,10 @@ 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', From cd32c6a952aaec31ea56b5d843bd26f47c6c5b44 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 15 Nov 2022 18:53:34 +0100 Subject: [PATCH 08/12] Update readme --- README.md | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 94c00cc7..d1d82f85 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 @@ -927,7 +909,7 @@ instantsearch.widgets.clearRefinements({ 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. +// TODO: explain what is needed for it to work - ✅ container: The CSS Selector or HTMLElement to insert the widget into. _required_ - ✅ showFirst: Whether to display the first-page link. @@ -1094,4 +1076,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. From 26a4be575a573bfe75a0bbe17441771831b449db Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Wed, 16 Nov 2022 11:43:02 +0100 Subject: [PATCH 09/12] Add explaination on finitePagination in thge pagination widget --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d1d82f85..c4c28734 100644 --- a/README.md +++ b/README.md @@ -907,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. - -// TODO: explain what is needed for it to work +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. From f62a29744e63f03b9d13a5a44f101ca939b53cf0 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Wed, 16 Nov 2022 11:44:35 +0100 Subject: [PATCH 10/12] Improve floor division --- src/utils/number.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/number.ts b/src/utils/number.ts index 6222b698..80410b00 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -14,12 +14,11 @@ export function ceiledDivision(dividend: number, divisor: number): number { /** * @param {number} dividend * @param {number} divisor - * @returns number + * @returns {number} */ export function floorDivision(dividend: number, divisor: number): number { if (divisor > 0) { - const NumberPages = Math.floor(dividend / divisor) // total number of pages rounded up to the next largest integer. - return NumberPages + return Math.floor(dividend / divisor) } return 0 } From aed200212a1b322c96abdef38cc7ba8c4fbf2c49 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Wed, 16 Nov 2022 15:32:44 +0100 Subject: [PATCH 11/12] Consistency in undefined checks --- src/adapter/search-response-adapter/total-hits-adapter.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/adapter/search-response-adapter/total-hits-adapter.ts b/src/adapter/search-response-adapter/total-hits-adapter.ts index 518e8765..b1a8ee2d 100644 --- a/src/adapter/search-response-adapter/total-hits-adapter.ts +++ b/src/adapter/search-response-adapter/total-hits-adapter.ts @@ -6,12 +6,12 @@ export function adaptTotalHits( const { hitsPerPage = 0, totalPages = 0, - estimatedTotalHits = undefined, - totalHits = undefined, + estimatedTotalHits, + totalHits, } = searchResponse - if (estimatedTotalHits !== undefined) { + if (estimatedTotalHits != null) { return estimatedTotalHits - } else if (totalHits !== undefined) { + } else if (totalHits != null) { return totalHits } From 842d989d83ed77958117c94fc44e8cb5b1c18122 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Wed, 16 Nov 2022 17:53:22 +0100 Subject: [PATCH 12/12] Use offset for lazy pagination --- .../__tests__/search-params.tests.ts | 5 +- .../search-params-adapter.ts | 9 +- .../__tests__/pagination-adapter.tests.ts | 92 ++++++++++--------- .../search-response-adapter/hits-adapter.ts | 14 +-- .../pagination-adapter.ts | 32 ++----- src/utils/index.ts | 1 - {src/utils => tests/assets}/number.ts | 12 --- 7 files changed, 74 insertions(+), 91 deletions(-) rename {src/utils => tests/assets}/number.ts (58%) 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 514be7c3..3e0a3aa1 100644 --- a/src/adapter/search-request-adapter/__tests__/search-params.tests.ts +++ b/src/adapter/search-request-adapter/__tests__/search-params.tests.ts @@ -130,6 +130,7 @@ describe('Pagination adapter', () => { }) expect(searchParams.limit).toBe(7) + expect(searchParams.offset).toBe(0) }) test('adapting a searchContext with no finite pagination on page 2', () => { @@ -138,7 +139,8 @@ describe('Pagination adapter', () => { pagination: { page: 1, hitsPerPage: 6, finite: false }, }) - expect(searchParams.limit).toBe(13) + expect(searchParams.limit).toBe(7) + expect(searchParams.offset).toBe(6) }) test('adapting a finite pagination with no placeholderSearch', () => { @@ -162,5 +164,6 @@ describe('Pagination adapter', () => { }) 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 48e30f97..af8004f1 100644 --- a/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/src/adapter/search-request-adapter/search-params-adapter.ts @@ -11,15 +11,17 @@ function setScrollPagination( page: number, query?: string, placeholderSearch?: boolean -): { limit: number } { +): { limit: number; offset: number } { if (!placeholderSearch && query === '') { return { limit: 0, + offset: 0, } } return { - limit: (page + 1) * hitsPerPage + 1, + limit: hitsPerPage + 1, + offset: page * hitsPerPage, } } @@ -128,13 +130,14 @@ export function MeiliParamsCreator(searchContext: SearchContext) { meiliSearchParams.hitsPerPage = hitsPerPage meiliSearchParams.page = page } else { - const { limit } = setScrollPagination( + const { limit, offset } = setScrollPagination( pagination.hitsPerPage, pagination.page, query, placeholderSearch ) meiliSearchParams.limit = limit + meiliSearchParams.offset = offset } }, addSort() { 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 7cef1622..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,5 +1,5 @@ import { adaptPaginationParameters } from '../pagination-adapter' -import { ceiledDivision } from '../../../utils' +import { ceiledDivision } from '../../../../tests/assets/number' const numberPagesTestParameters = [ { @@ -59,7 +59,7 @@ const finitePaginateHitsTestsParameters = [ { searchResponse: { hits: [], page: 100, hitsPerPage: 0, totalPages: 0 }, adaptedPagination: { - page: 99, + page: 100, hitsPerPage: 0, nbPages: 0, }, @@ -67,7 +67,7 @@ const finitePaginateHitsTestsParameters = [ { searchResponse: { hits: [], page: 100, hitsPerPage: 20, totalPages: 0 }, adaptedPagination: { - page: 99, + page: 100, hitsPerPage: 20, nbPages: 0, }, @@ -136,7 +136,7 @@ const finitePaginateHitsTestsParameters = [ totalPages: 2, }, adaptedPagination: { - page: 0, + page: 1, hitsPerPage: 2, nbPages: 2, }, @@ -149,7 +149,7 @@ const finitePaginateHitsTestsParameters = [ totalPages: 1, }, adaptedPagination: { - page: 0, + page: 1, hitsPerPage: 20, nbPages: 1, }, @@ -162,7 +162,7 @@ const finitePaginateHitsTestsParameters = [ totalPages: 0, }, adaptedPagination: { - page: 0, + page: 1, hitsPerPage: 0, nbPages: 0, }, @@ -177,7 +177,7 @@ const finitePaginateHitsTestsParameters = [ totalPages: 1, }, adaptedPagination: { - page: 1, + page: 2, hitsPerPage: 20, nbPages: 1, }, @@ -192,7 +192,7 @@ const finitePaginateHitsTestsParameters = [ adaptedPagination: { hitsPerPage: 20, nbPages: 1, - page: 1, + page: 2, }, }, { @@ -203,62 +203,64 @@ const finitePaginateHitsTestsParameters = [ totalPages: 0, }, adaptedPagination: { - page: 1, + page: 2, nbPages: 0, hitsPerPage: 0, }, }, ] -const scrollPaginateHitsTestsParameters = [ +const lazyPaginateHitsTestsParameters = [ // Empty hits { searchResponse: { hits: [], - limit: 0, + limit: 21, + offset: 0, }, - searchContext: { + paginationState: { hitsPerPage: 20, page: 0, }, adaptedPagination: { page: 0, hitsPerPage: 20, - nbPages: 0, + nbPages: 1, }, }, { - searchResponse: { hits: [], limit: 0 }, - searchContext: { - hitsPerPage: 0, + searchResponse: { hits: [], limit: 0, offset: 0 }, + paginationState: { page: 100, + hitsPerPage: 0, }, adaptedPagination: { - page: 0, + page: 100, hitsPerPage: 0, nbPages: 0, }, }, { - searchResponse: { hits: [], limit: 20 }, - searchContext: { + searchResponse: { hits: [], limit: 21, offset: 0 }, + paginationState: { page: 100, hitsPerPage: 20, }, adaptedPagination: { - page: 0, + page: 100, hitsPerPage: 20, - nbPages: 0, + nbPages: 1, }, }, - // Page 0 + // // Page 0 { searchResponse: { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - limit: 20, + limit: 21, + offset: 0, }, - searchContext: { + paginationState: { page: 0, hitsPerPage: 20, }, @@ -272,8 +274,9 @@ const scrollPaginateHitsTestsParameters = [ searchResponse: { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], limit: 0, + offset: 0, }, - searchContext: { + paginationState: { page: 0, hitsPerPage: 0, }, @@ -286,9 +289,10 @@ const scrollPaginateHitsTestsParameters = [ { searchResponse: { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - limit: 20, + limit: 21, + offset: 0, }, - searchContext: { + paginationState: { page: 0, hitsPerPage: 20, }, @@ -301,9 +305,10 @@ const scrollPaginateHitsTestsParameters = [ { searchResponse: { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - limit: 2, + limit: 3, + offset: 0, }, - searchContext: { + paginationState: { page: 0, hitsPerPage: 2, }, @@ -314,13 +319,14 @@ const scrollPaginateHitsTestsParameters = [ }, }, - // Page 1 + // // Page 1 { searchResponse: { hits: [{ id: 1 }, { id: 2 }, { id: 3 }], limit: 2, + offset: 1, }, - searchContext: { + paginationState: { page: 1, hitsPerPage: 1, }, @@ -333,9 +339,10 @@ const scrollPaginateHitsTestsParameters = [ { searchResponse: { hits: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }], - limit: 5, + limit: 3, + offset: 2, }, - searchContext: { + paginationState: { page: 1, hitsPerPage: 2, }, @@ -349,10 +356,11 @@ const scrollPaginateHitsTestsParameters = [ // Page 2 { searchResponse: { - hits: [{ id: 1 }, { id: 2 }, { id: 3 }], - limit: 3, + hits: [{ id: 3 }], + limit: 2, + offset: 2, }, - searchContext: { + paginationState: { page: 2, hitsPerPage: 1, }, @@ -402,18 +410,18 @@ describe.each(finitePaginateHitsTestsParameters)( } ) -describe.each(scrollPaginateHitsTestsParameters)( - 'Finite paginate hits tests', +describe.each(lazyPaginateHitsTestsParameters)( + 'Lazy paginate hits tests', ({ - searchResponse: { hits, limit }, - searchContext: { page, hitsPerPage }, + searchResponse: { hits, limit, offset }, + paginationState: { page, hitsPerPage }, adaptedPagination, }) => { it(`should return ${JSON.stringify( adaptedPagination - )} where limit is ${0} in the response and where the instantsearch pagination context is page: ${page} and hitsPerPage: ${hitsPerPage}`, () => { + )} where limit is ${limit} in the response and where the instantsearch pagination context is page: ${page} and hitsPerPage: ${hitsPerPage}`, () => { const response = adaptPaginationParameters( - { hits, limit, processingTimeMs: 0, query: '' }, + { hits, limit, offset, processingTimeMs: 0, query: '' }, { hitsPerPage, page, finite: false } ) diff --git a/src/adapter/search-response-adapter/hits-adapter.ts b/src/adapter/search-response-adapter/hits-adapter.ts index 0b89895a..a4c673ca 100644 --- a/src/adapter/search-response-adapter/hits-adapter.ts +++ b/src/adapter/search-response-adapter/hits-adapter.ts @@ -14,16 +14,16 @@ export function adaptHits( const { primaryKey } = searchContext const { hits } = searchResponse const { - pagination: { finite, page, hitsPerPage }, + pagination: { finite, hitsPerPage }, } = searchContext - if (!finite) { - const deleteCount = page * hitsPerPage - hits.splice(0, deleteCount) - if (hits.length > hitsPerPage) { - hits.splice(hits.length - 1, 1) - } + // 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) { diff --git a/src/adapter/search-response-adapter/pagination-adapter.ts b/src/adapter/search-response-adapter/pagination-adapter.ts index 632b4352..5262d9de 100644 --- a/src/adapter/search-response-adapter/pagination-adapter.ts +++ b/src/adapter/search-response-adapter/pagination-adapter.ts @@ -3,7 +3,6 @@ import type { PaginationState, InstantSearchPagination, } from '../../types' -import { ceiledDivision, floorDivision } from '../../utils' function adaptNbPages( searchResponse: MeiliSearchResponse>, @@ -12,40 +11,23 @@ function adaptNbPages( if (searchResponse.totalPages != null) { return searchResponse.totalPages } - - return ceiledDivision(searchResponse.hits.length, hitsPerPage) -} - -function adaptPage( - searchResponse: MeiliSearchResponse>, - hitsPerPage: number -): number { - const { limit, page } = searchResponse - - // Finite pagination environment - if (page === 0) { - // Should not happen but safeguarding in case it does + // Avoid dividing by 0 + if (hitsPerPage === 0) { return 0 } - if (page != null) { - return page - 1 - } - // Scroll pagination environment - if (!limit) { - return 0 - } else { - return floorDivision(limit, hitsPerPage) - 1 - } + 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 } = paginationState + const { hitsPerPage, page } = paginationState const nbPages = adaptNbPages(searchResponse, hitsPerPage) - const page = adaptPage(searchResponse, hitsPerPage) return { page, 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 58% rename from src/utils/number.ts rename to tests/assets/number.ts index 80410b00..1eb11d7f 100644 --- a/src/utils/number.ts +++ b/tests/assets/number.ts @@ -10,15 +10,3 @@ export function ceiledDivision(dividend: number, divisor: number): number { } return 0 } - -/** - * @param {number} dividend - * @param {number} divisor - * @returns {number} - */ -export function floorDivision(dividend: number, divisor: number): number { - if (divisor > 0) { - return Math.floor(dividend / divisor) - } - return 0 -}