diff --git a/packages/instantsearch.js/src/lib/__tests__/server.test.ts b/packages/instantsearch.js/src/lib/__tests__/server.test.ts index ea265f451e..44b0e1731d 100644 --- a/packages/instantsearch.js/src/lib/__tests__/server.test.ts +++ b/packages/instantsearch.js/src/lib/__tests__/server.test.ts @@ -314,20 +314,26 @@ describe('getInitialResults', () => { // ...and only the latest duplicate params are in the returned results const expectedInitialResults = { indexName: expect.objectContaining({ - requestParams: expect.objectContaining({ - query: 'apple', - }), + requestParams: expect.arrayContaining([ + expect.objectContaining({ + query: 'apple', + }), + ]), }), indexName2: expect.objectContaining({ - requestParams: expect.objectContaining({ - query: 'samsung', - }), + requestParams: expect.arrayContaining([ + expect.objectContaining({ + query: 'samsung', + }), + ]), }), indexId: expect.objectContaining({ - requestParams: expect.objectContaining({ - query: 'apple', - hitsPerPage: 3, - }), + requestParams: expect.arrayContaining([ + expect.objectContaining({ + query: 'apple', + hitsPerPage: 3, + }), + ]), }), }; diff --git a/packages/instantsearch.js/src/lib/server.ts b/packages/instantsearch.js/src/lib/server.ts index 1911fc92b9..e92677305c 100644 --- a/packages/instantsearch.js/src/lib/server.ts +++ b/packages/instantsearch.js/src/lib/server.ts @@ -84,7 +84,14 @@ export function getInitialResults( const searchResults = widget.getResults(); const recommendResults = widget.getHelper()?.lastRecommendResults; if (searchResults || recommendResults) { - const requestParams = requestParamsList?.[requestParamsIndex++]; + const resultsCount = searchResults?._rawResults?.length || 0; + const requestParams = resultsCount + ? requestParamsList?.slice( + requestParamsIndex, + requestParamsIndex + resultsCount + ) + : []; + requestParamsIndex += resultsCount; initialResults[widget.getIndexId()] = { // We convert the Helper state to a plain object to pass parsable data // structures from server to client. diff --git a/packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts b/packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts index 877af08b9f..e470c76a9c 100644 --- a/packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts +++ b/packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts @@ -88,9 +88,11 @@ describe('hydrateSearchClient', () => { rawResults: [ { index: 'instant_search', params: 'source=results', nbHits: 1000 }, ], - requestParams: { - source: 'request', - }, + requestParams: [ + { + source: 'request', + }, + ], }, } as unknown as InitialResults); @@ -103,6 +105,72 @@ describe('hydrateSearchClient', () => { ); }); + it('should handle multiple indices and multiple queries per index', () => { + const setCache = jest.fn(); + client = { + transporter: { responsesCache: { set: setCache } }, + addAlgoliaAgent: jest.fn(), + } as unknown as SearchClient; + + hydrateSearchClient(client, { + instant_search: { + results: [ + { index: 'instant_search', params: 'source=results', nbHits: 1000 }, + { + index: 'instant_search', + params: 'source=results&hitsPerPage=0', + nbHits: 1000, + }, + ], + state: {}, + requestParams: [ + { + source: 'request', + }, + { + source: 'request', + hitsPerPage: 0, + }, + ], + }, + instant_search_price_desc: { + results: [ + { + index: 'instant_search_price_desc', + params: 'source=results', + nbHits: 1000, + }, + ], + state: {}, + requestParams: [ + { + source: 'request', + }, + ], + }, + } as unknown as InitialResults); + + expect(setCache).toHaveBeenCalledWith( + expect.objectContaining({ + args: [ + [ + { indexName: 'instant_search', params: 'source=request' }, + { + indexName: 'instant_search', + params: 'source=request&hitsPerPage=0', + }, + { + indexName: 'instant_search_price_desc', + params: 'source=request', + }, + ], + ], + method: 'search', + }), + expect.anything() + ); + }); + it('should use results params as a fallback', () => { const setCache = jest.fn(); client = { diff --git a/packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts b/packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts index e867c52d5c..4092194030 100644 --- a/packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts +++ b/packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts @@ -34,25 +34,35 @@ export function hydrateSearchClient( return; } - const cachedRequest = Object.keys(results).map((key) => { - const { state, requestParams, results: serverResults } = results[key]; - return serverResults && state - ? serverResults.map((result) => ({ - indexName: state.index || result.index, - // We normalize the params received from the server as they can - // be serialized differently depending on the engine. - // We use search parameters from the server request to craft the cache - // if possible, and fallback to those from results if not. - ...(requestParams || result.params - ? { - params: serializeQueryParameters( - requestParams || deserializeQueryParameters(result.params) - ), - } - : {}), - })) - : []; - }); + const cachedRequest = [ + Object.keys(results).reduce< + Array<{ + params?: string; + indexName?: string; + }> + >((acc, key) => { + const { state, requestParams, results: serverResults } = results[key]; + const mappedResults = + serverResults && state + ? serverResults.map((result, idx) => ({ + indexName: state.index || result.index, + // We normalize the params received from the server as they can + // be serialized differently depending on the engine. + // We use search parameters from the server request to craft the cache + // if possible, and fallback to those from results if not. + ...(requestParams?.[idx] || result.params + ? { + params: serializeQueryParameters( + requestParams?.[idx] || + deserializeQueryParameters(result.params) + ), + } + : {}), + })) + : []; + return acc.concat(mappedResults); + }, []), + ]; const cachedResults = Object.keys(results).reduce>>( (acc, key) => { diff --git a/packages/instantsearch.js/src/types/results.ts b/packages/instantsearch.js/src/types/results.ts index 9cca49b438..6306469f6f 100644 --- a/packages/instantsearch.js/src/types/results.ts +++ b/packages/instantsearch.js/src/types/results.ts @@ -105,7 +105,7 @@ type InitialResult = { params: RecommendParametersOptions['params']; results: RecommendResults['_rawResults']; }; - requestParams?: SearchOptions; + requestParams?: SearchOptions[]; }; export type InitialResults = Record; diff --git a/packages/react-instantsearch-core/src/server/__tests__/__snapshots__/getServerState.test.tsx.snap b/packages/react-instantsearch-core/src/server/__tests__/__snapshots__/getServerState.test.tsx.snap index f320e7e33f..af0218fcbe 100644 --- a/packages/react-instantsearch-core/src/server/__tests__/__snapshots__/getServerState.test.tsx.snap +++ b/packages/react-instantsearch-core/src/server/__tests__/__snapshots__/getServerState.test.tsx.snap @@ -3,20 +3,33 @@ exports[`getServerState returns initialResults 1`] = ` { "instant_search": { - "requestParams": { - "facetFilters": [ - [ - "brand:Apple", + "requestParams": [ + { + "facetFilters": [ + [ + "brand:Apple", + ], ], - ], - "facets": [ - "brand", - ], - "highlightPostTag": "__/ais-highlight__", - "highlightPreTag": "__ais-highlight__", - "maxValuesPerFacet": 10, - "query": "iphone", - }, + "facets": [ + "brand", + ], + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "maxValuesPerFacet": 10, + "query": "iphone", + }, + { + "analytics": false, + "clickAnalytics": false, + "facets": "brand", + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "hitsPerPage": 0, + "maxValuesPerFacet": 10, + "page": 0, + "query": "iphone", + }, + ], "results": [ { "exhaustiveFacetsCount": true, @@ -69,17 +82,33 @@ exports[`getServerState returns initialResults 1`] = ` }, }, "instant_search_price_asc": { - "requestParams": { - "analytics": false, - "clickAnalytics": false, - "facets": "brand", - "highlightPostTag": "__/ais-highlight__", - "highlightPreTag": "__ais-highlight__", - "hitsPerPage": 0, - "maxValuesPerFacet": 10, - "page": 0, - "query": "iphone", - }, + "requestParams": [ + { + "facetFilters": [ + [ + "brand:Apple", + ], + ], + "facets": [ + "brand", + ], + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "maxValuesPerFacet": 10, + "query": "iphone", + }, + { + "analytics": false, + "clickAnalytics": false, + "facets": "brand", + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "hitsPerPage": 0, + "maxValuesPerFacet": 10, + "page": 0, + "query": "iphone", + }, + ], "results": [ { "exhaustiveFacetsCount": true, @@ -124,17 +153,33 @@ exports[`getServerState returns initialResults 1`] = ` }, }, "instant_search_price_desc": { - "requestParams": { - "analytics": false, - "clickAnalytics": false, - "facets": "brand", - "highlightPostTag": "__/ais-highlight__", - "highlightPreTag": "__ais-highlight__", - "hitsPerPage": 0, - "maxValuesPerFacet": 10, - "page": 0, - "query": "iphone", - }, + "requestParams": [ + { + "facetFilters": [ + [ + "brand:Apple", + ], + ], + "facets": [ + "brand", + ], + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "maxValuesPerFacet": 10, + "query": "iphone", + }, + { + "analytics": false, + "clickAnalytics": false, + "facets": "brand", + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "hitsPerPage": 0, + "maxValuesPerFacet": 10, + "page": 0, + "query": "iphone", + }, + ], "results": [ { "exhaustiveFacetsCount": true, @@ -179,20 +224,33 @@ exports[`getServerState returns initialResults 1`] = ` }, }, "instant_search_rating_desc": { - "requestParams": { - "facetFilters": [ - [ - "brand:Apple", + "requestParams": [ + { + "facetFilters": [ + [ + "brand:Apple", + ], ], - ], - "facets": [ - "brand", - ], - "highlightPostTag": "__/ais-highlight__", - "highlightPreTag": "__ais-highlight__", - "maxValuesPerFacet": 10, - "query": "iphone", - }, + "facets": [ + "brand", + ], + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "maxValuesPerFacet": 10, + "query": "iphone", + }, + { + "analytics": false, + "clickAnalytics": false, + "facets": "brand", + "highlightPostTag": "__/ais-highlight__", + "highlightPreTag": "__ais-highlight__", + "hitsPerPage": 0, + "maxValuesPerFacet": 10, + "page": 0, + "query": "iphone", + }, + ], "results": [ { "exhaustiveFacetsCount": true, diff --git a/packages/vue-instantsearch/src/util/__tests__/createServerRootMixin.test.js b/packages/vue-instantsearch/src/util/__tests__/createServerRootMixin.test.js index 6d85689f43..8c6674780f 100644 --- a/packages/vue-instantsearch/src/util/__tests__/createServerRootMixin.test.js +++ b/packages/vue-instantsearch/src/util/__tests__/createServerRootMixin.test.js @@ -315,10 +315,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear }); expect(state.hello).toEqual({ - requestParams: { - hitsPerPage: 100, - query: '', - }, + requestParams: [ + { + hitsPerPage: 100, + query: '', + }, + ], results: [ { query: '', @@ -342,10 +344,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear // Parent's widgets state should not be merged into nested index state expect(state.nestedIndex).toEqual({ - requestParams: { - hitsPerPage: 100, - query: '', - }, + requestParams: [ + { + hitsPerPage: 100, + query: '', + }, + ], results: [ { query: '',