Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): allow multi-index categoryAttribute in query suggestions #981

Open
wants to merge 6 commits into
base: next
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,67 @@ const hits: Hit<any> = [
},
},
];
const multiIndexHits: Hit<any> = [
{
index_1: {
exact_nb_hits: 100,
facets: {
exact_matches: {
data_origin: [
{
value: 'Index 1',
count: 100,
},
],
categories: [
{
value: 'Appliances',
count: 252,
},
{
value: 'Ranges, Cooktops & Ovens',
count: 229,
},
],
},
},
},
index_2: {
exact_nb_hits: 200,
facets: {
exact_matches: {
data_origin: [
{
value: 'Index 2',
count: 200,
},
],
genre: [
{
value: 'Poetry',
count: 340,
},
{
value: 'Fiction',
count: 140,
},
],
},
},
},
nb_words: 1,
popularity: 1230,
query: 'cooktop',
objectID: 'cooktop',
_highlightResult: {
query: {
value: 'cooktop',
matchLevel: 'none',
matchedWords: [],
},
},
},
];
/* eslint-enable @typescript-eslint/camelcase */

const searchClient = createSearchClient({
Expand Down Expand Up @@ -512,6 +573,83 @@ describe('createQuerySuggestionsPlugin', () => {
});
});

test('accumulates suggestion categories from multiple indexes and attributes', async () => {
castToJestMock(searchClient.search).mockReturnValueOnce(
Promise.resolve(
createMultiSearchResponse({
hits: multiIndexHits,
})
)
);

const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient,
indexName: 'indexName',
categoryAttribute: [
[
'index_1',
'facets',
'exact_matches',
'data_origin',
],
[
'index_2',
'facets',
'exact_matches',
'data_origin',
],
[
'index_1',
'facets',
'exact_matches',
'categories',
],
[
'index_2',
'facets',
'exact_matches',
'genre',
]
],
categoriesPerItem: 6,
});

const container = document.createElement('div');
const panelContainer = document.createElement('div');

document.body.appendChild(panelContainer);

autocomplete({
container,
panelContainer,
plugins: [querySuggestionsPlugin],
});

const input = container.querySelector<HTMLInputElement>('.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 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
]);
});
});

test('fills the input with the query item key followed by a space on tap ahead', async () => {
const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,13 +47,21 @@ export type CreateQuerySuggestionsPluginParams<
onTapAhead(item: TItem): void;
}): AutocompleteSource<TItem>;
/**
* The attribute or attribute path 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"]
* @example [
* ["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[];
categoryAttribute?: string | string[] | string[][];
/**
* How many items to display categories for.
*
Expand Down Expand Up @@ -118,6 +130,7 @@ export function createQuerySuggestionsPlugin<
}

let itemsWithCategoriesAdded = 0;

return querySuggestionsHits.reduce<
Array<AutocompleteQuerySuggestionsHit<typeof indexName>>
>((acc, current) => {
Expand All @@ -126,14 +139,29 @@ export function createQuerySuggestionsPlugin<
> = [current];

if (itemsWithCategoriesAdded < itemsWithCategories) {
const categories = (
getAttributeValueByPath(
current,
Array.isArray(categoryAttribute)
? categoryAttribute
: [categoryAttribute]
) || []
)
let paths = (Array.isArray(categoryAttribute[0])
? categoryAttribute
: [categoryAttribute]) as string[][];

if (typeof categoryAttribute === 'string') {
paths = [[categoryAttribute]];
}

const categoriesValues = paths.reduce<
QuerySuggestionsFacetValue[]
>((totalCategories, path) => {
const attrVal = getAttributeValueByPath(current, path);

return attrVal
? totalCategories.concat(attrVal)
: totalCategories;
}, []);

if (paths.length > 1) {
categoriesValues.sort((a, b) => b.count - a.count);
}

const categories = categoriesValues
.map((x) => x.value)
.slice(0, categoriesPerItem);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<TKey extends string> = Record<
TKey,
Expand Down