From 1ff377c0bc869579358f77cca25c2541b2b6e872 Mon Sep 17 00:00:00 2001 From: andreyvolokitin Date: Mon, 30 May 2022 04:53:49 +0400 Subject: [PATCH 1/4] feat(plugins): allow multi-index categoryAttribute in query suggestions --- .../createQuerySuggestionsPlugin.test.ts | 94 +++++++++++++++++++ .../src/createQuerySuggestionsPlugin.ts | 44 +++++++-- .../src/types/QuerySuggestionsHit.ts | 2 +- 3 files changed, 132 insertions(+), 8 deletions(-) diff --git a/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts b/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts index 459fcb668..bba29e272 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts @@ -46,6 +46,47 @@ const hits: Hit = [ }, }, ]; +const multiIndexHits: Hit = [ + { + index_1: { + exact_nb_hits: 100, + facets: { + exact_matches: { + data_origin: [ + { + value: 'Index 1', + count: 100, + }, + ], + }, + }, + }, + index_2: { + exact_nb_hits: 200, + facets: { + exact_matches: { + data_origin: [ + { + value: 'Index 2', + count: 200, + }, + ], + }, + }, + }, + nb_words: 1, + popularity: 1230, + query: 'cooktop', + objectID: 'cooktop', + _highlightResult: { + query: { + value: 'cooktop', + matchLevel: 'none', + matchedWords: [], + }, + }, + }, +]; /* eslint-enable @typescript-eslint/camelcase */ const searchClient = createSearchClient({ @@ -394,6 +435,59 @@ describe('createQuerySuggestionsPlugin', () => { }); }); + test('accumulates suggestion categories from multiple indexes', async () => { + castToJestMock(searchClient.search).mockReturnValueOnce( + Promise.resolve( + createMultiSearchResponse({ + hits: multiIndexHits, + }) + ) + ); + + const querySuggestionsPlugin = createQuerySuggestionsPlugin({ + searchClient, + indexName: 'indexName', + categoryAttribute: [ + 'index_1|index_2', + 'facets', + 'exact_matches', + 'data_origin', + ], + categoriesPerItem: 2, + }); + + const container = document.createElement('div'); + const panelContainer = document.createElement('div'); + + document.body.appendChild(panelContainer); + + autocomplete({ + container, + panelContainer, + plugins: [querySuggestionsPlugin], + }); + + const input = container.querySelector('.aa-Input'); + + fireEvent.input(input, { target: { value: 'a' } }); + + await waitFor(() => { + expect( + within( + panelContainer.querySelector( + '[data-autocomplete-source-id="querySuggestionsPlugin"]' + ) + ) + .getAllByRole('option') + .map((option) => option.textContent) + ).toEqual([ + 'cooktop', // Query Suggestions item + 'in Index 2', // Category item + 'in Index 1', // Category item + ]); + }); + }); + test('fills the input with the query item key followed by a space on tap ahead', async () => { const querySuggestionsPlugin = createQuerySuggestionsPlugin({ searchClient, diff --git a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts index 4dddbb618..22feebd13 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts @@ -9,7 +9,11 @@ import { SearchOptions } from '@algolia/client-search'; import { SearchClient } from 'algoliasearch/lite'; import { getTemplates } from './getTemplates'; -import { AutocompleteQuerySuggestionsHit, QuerySuggestionsHit } from './types'; +import { + AutocompleteQuerySuggestionsHit, + QuerySuggestionsHit, + QuerySuggestionsFacetValue, +} from './types'; export type CreateQuerySuggestionsPluginParams< TItem extends QuerySuggestionsHit @@ -44,8 +48,10 @@ export type CreateQuerySuggestionsPluginParams< }): AutocompleteSource; /** * The attribute or attribute path to display categories for. + * Multiple index names can be passed using `|` symbol in first path segment (see docs). * * @example ["instant_search", "facets", "exact_matches", "categories"] + * @example ["instant_search_1|instant_search_2", "facets", "exact_matches", "categories"] * @example ["instant_search", "facets", "exact_matches", "hierarchicalCategories.lvl0"] * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-plugin-query-suggestions/createQuerySuggestionsPlugin/#param-categoryattribute */ @@ -125,12 +131,36 @@ export function createQuerySuggestionsPlugin< > = [current]; if (i <= itemsWithCategories - 1) { - const categories = getAttributeValueByPath( - current, - Array.isArray(categoryAttribute) - ? categoryAttribute - : [categoryAttribute] - ) + const path = Array.isArray(categoryAttribute) + ? categoryAttribute + : [categoryAttribute]; + const firstPathSegment = path[0]; + const remainingPath = path.slice(1); + const indexNames = firstPathSegment.split('|'); + + const categoriesValues = indexNames.reduce< + QuerySuggestionsFacetValue[] + >((totalCategories, indexName) => { + const attrVal = getAttributeValueByPath( + current, + [indexName].concat(remainingPath) + ); + + if (!attrVal) { + return totalCategories; + } + + // use only the first facet value if multiple indexes needs to be targeted by path + return totalCategories.concat( + indexNames.length > 1 ? attrVal[0] : attrVal + ); + }, []); + + if (indexNames.length > 1) { + categoriesValues.sort((a, b) => b.count - a.count); + } + + const categories = categoriesValues .map((x) => x.value) .slice(0, categoriesPerItem); diff --git a/packages/autocomplete-plugin-query-suggestions/src/types/QuerySuggestionsHit.ts b/packages/autocomplete-plugin-query-suggestions/src/types/QuerySuggestionsHit.ts index 0f263be88..9b5cc5140 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/types/QuerySuggestionsHit.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/types/QuerySuggestionsHit.ts @@ -1,6 +1,6 @@ import { Hit } from '@algolia/client-search'; -type QuerySuggestionsFacetValue = { value: string; count: number }; +export type QuerySuggestionsFacetValue = { value: string; count: number }; type QuerySuggestionsIndexMatch = Record< TKey, From a8f98138e6dd1ae70044666aebc38e8e11c2a554 Mon Sep 17 00:00:00 2001 From: andreyvolokitin Date: Tue, 31 May 2022 19:35:40 +0400 Subject: [PATCH 2/4] refactor(plugin): use nested arrays for multiple paths --- .../createQuerySuggestionsPlugin.test.ts | 16 +++++-- .../src/createQuerySuggestionsPlugin.ts | 42 +++++++++++-------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts b/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts index bba29e272..3ccc66881 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts @@ -448,10 +448,18 @@ describe('createQuerySuggestionsPlugin', () => { searchClient, indexName: 'indexName', categoryAttribute: [ - 'index_1|index_2', - 'facets', - 'exact_matches', - 'data_origin', + [ + 'index_1', + 'facets', + 'exact_matches', + 'data_origin', + ], + [ + 'index_2', + 'facets', + 'exact_matches', + 'data_origin', + ] ], categoriesPerItem: 2, }); diff --git a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts index 22feebd13..bea110edc 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts @@ -48,14 +48,24 @@ export type CreateQuerySuggestionsPluginParams< }): AutocompleteSource; /** * The attribute or attribute path to display categories for. - * Multiple index names can be passed using `|` symbol in first path segment (see docs). + * + * If suggestion index is connected to multiple indexes, array of paths can be used. + * The assumption in this case is that a single category gets split across multiple indexes, + * having a uniform value per index, so the total matches for category values will be accumulated + * by picking the first match per each path (and it should only have one). + * + * Multiple attribute names can be used if required, but they should all designate a single "entity", + * even if having different names. * * @example ["instant_search", "facets", "exact_matches", "categories"] - * @example ["instant_search_1|instant_search_2", "facets", "exact_matches", "categories"] * @example ["instant_search", "facets", "exact_matches", "hierarchicalCategories.lvl0"] + * @example [ + * ["index_1", "facets", "exact_matches", "data_origin"], + * ["index_2", "facets", "exact_matches", "data_origin"], + * ] * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-plugin-query-suggestions/createQuerySuggestionsPlugin/#param-categoryattribute */ - categoryAttribute?: string | string[]; + categoryAttribute?: string | string[] | string[][]; /** * How many items to display categories for. * @@ -131,32 +141,30 @@ export function createQuerySuggestionsPlugin< > = [current]; if (i <= itemsWithCategories - 1) { - const path = Array.isArray(categoryAttribute) + let paths = (Array.isArray(categoryAttribute[0]) ? categoryAttribute - : [categoryAttribute]; - const firstPathSegment = path[0]; - const remainingPath = path.slice(1); - const indexNames = firstPathSegment.split('|'); + : [categoryAttribute]) as string[][]; - const categoriesValues = indexNames.reduce< + if (typeof categoryAttribute === 'string') { + paths = [[categoryAttribute]]; + } + + const categoriesValues = paths.reduce< QuerySuggestionsFacetValue[] - >((totalCategories, indexName) => { - const attrVal = getAttributeValueByPath( - current, - [indexName].concat(remainingPath) - ); + >((totalCategories, path) => { + const attrVal = getAttributeValueByPath(current, path); if (!attrVal) { return totalCategories; } - // use only the first facet value if multiple indexes needs to be targeted by path + // use only the first facet value if multiple indexes needs to be targeted by multiple paths return totalCategories.concat( - indexNames.length > 1 ? attrVal[0] : attrVal + paths.length > 1 ? attrVal[0] : attrVal ); }, []); - if (indexNames.length > 1) { + if (paths.length > 1) { categoriesValues.sort((a, b) => b.count - a.count); } From 467a4deb57d6b6d02f5e0822356e2a0284388b5f Mon Sep 17 00:00:00 2001 From: andreyvolokitin Date: Fri, 3 Jun 2022 15:48:50 +0400 Subject: [PATCH 3/4] feat(plugins): accumulate all categories --- .../createQuerySuggestionsPlugin.test.ts | 40 ++++++++++++++++++- .../src/createQuerySuggestionsPlugin.ts | 25 ++++-------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts b/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts index 3ccc66881..b1a0cdb7d 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/__tests__/createQuerySuggestionsPlugin.test.ts @@ -58,6 +58,16 @@ const multiIndexHits: Hit = [ count: 100, }, ], + categories: [ + { + value: 'Appliances', + count: 252, + }, + { + value: 'Ranges, Cooktops & Ovens', + count: 229, + }, + ], }, }, }, @@ -71,6 +81,16 @@ const multiIndexHits: Hit = [ count: 200, }, ], + genre: [ + { + value: 'Poetry', + count: 340, + }, + { + value: 'Fiction', + count: 140, + }, + ], }, }, }, @@ -435,7 +455,7 @@ describe('createQuerySuggestionsPlugin', () => { }); }); - test('accumulates suggestion categories from multiple indexes', async () => { + test('accumulates suggestion categories from multiple indexes and attributes', async () => { castToJestMock(searchClient.search).mockReturnValueOnce( Promise.resolve( createMultiSearchResponse({ @@ -459,9 +479,21 @@ describe('createQuerySuggestionsPlugin', () => { 'facets', 'exact_matches', 'data_origin', + ], + [ + 'index_1', + 'facets', + 'exact_matches', + 'categories', + ], + [ + 'index_2', + 'facets', + 'exact_matches', + 'genre', ] ], - categoriesPerItem: 2, + categoriesPerItem: 6, }); const container = document.createElement('div'); @@ -490,7 +522,11 @@ describe('createQuerySuggestionsPlugin', () => { .map((option) => option.textContent) ).toEqual([ 'cooktop', // Query Suggestions item + 'in Poetry', // Category item + 'in Appliances', // Category item + 'in Ranges, Cooktops & Ovens', // Category item 'in Index 2', // Category item + 'in Fiction', // Category item 'in Index 1', // Category item ]); }); diff --git a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts index bea110edc..6f4d177a3 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts @@ -47,15 +47,7 @@ export type CreateQuerySuggestionsPluginParams< onTapAhead(item: TItem): void; }): AutocompleteSource; /** - * The attribute or attribute path to display categories for. - * - * If suggestion index is connected to multiple indexes, array of paths can be used. - * The assumption in this case is that a single category gets split across multiple indexes, - * having a uniform value per index, so the total matches for category values will be accumulated - * by picking the first match per each path (and it should only have one). - * - * Multiple attribute names can be used if required, but they should all designate a single "entity", - * even if having different names. + * The attribute attribute path, or array of paths to display categories for. * * @example ["instant_search", "facets", "exact_matches", "categories"] * @example ["instant_search", "facets", "exact_matches", "hierarchicalCategories.lvl0"] @@ -63,6 +55,10 @@ export type CreateQuerySuggestionsPluginParams< * ["index_1", "facets", "exact_matches", "data_origin"], * ["index_2", "facets", "exact_matches", "data_origin"], * ] + * @example [ + * ["index_1", "facets", "exact_matches", "attr_1"], + * ["index_2", "facets", "exact_matches", "attr_2"], + * ] * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-plugin-query-suggestions/createQuerySuggestionsPlugin/#param-categoryattribute */ categoryAttribute?: string | string[] | string[][]; @@ -154,14 +150,9 @@ export function createQuerySuggestionsPlugin< >((totalCategories, path) => { const attrVal = getAttributeValueByPath(current, path); - if (!attrVal) { - return totalCategories; - } - - // use only the first facet value if multiple indexes needs to be targeted by multiple paths - return totalCategories.concat( - paths.length > 1 ? attrVal[0] : attrVal - ); + return attrVal + ? totalCategories.concat(attrVal) + : totalCategories; }, []); if (paths.length > 1) { From 1b00aeac656cb6ccc86bd32ff69662f081dc8b61 Mon Sep 17 00:00:00 2001 From: andreyvolokitin Date: Fri, 3 Jun 2022 15:52:02 +0400 Subject: [PATCH 4/4] docs(plugins): add comment comma --- .../src/createQuerySuggestionsPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts index 6f4d177a3..1166f0a3a 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts @@ -47,7 +47,7 @@ export type CreateQuerySuggestionsPluginParams< onTapAhead(item: TItem): void; }): AutocompleteSource; /** - * The attribute attribute path, or array of paths to display categories for. + * The attribute, attribute path, or array of paths to display categories for. * * @example ["instant_search", "facets", "exact_matches", "categories"] * @example ["instant_search", "facets", "exact_matches", "hierarchicalCategories.lvl0"]