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
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,47 @@ const hits: Hit<any> = [
},
},
];
const multiIndexHits: Hit<any> = [
{
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({
Expand Down Expand Up @@ -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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't really thought about this feature yet, but api-wise, I think it would be better if this was an array instead of a magic string:

Suggested change
'index_1|index_2',
['index_1', 'index_2'],

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably even nested arrays so that you can target different attribute names:

[
  ['index_1', 'facets', 'exact_matches', 'data_origin1'],
  ['index_2', 'facets', 'exact_matches', 'data_origin2']
]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, looking at the example again, the whole item should be an array if we accept multiple indices

Copy link
Author

@andreyvolokitin andreyvolokitin May 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought a lot about how it could work, but I didn't came out with a case where we need to use different attribute names (maybe there is a case). data_origin1 and data_origin2 basically need to be a single attribute — data_origin, but this attribute would contain different values per index. So basically it almost the same as current categoryAttribute, but it searches multiple indexes, and picks only the first match from each.

I need to wrap my head around a multi-attribute use-case, it wouldn't hurt probably, but currently I can't come up with an example usage for this — the current PR logic would accumulate and sort the multi-index matches like they're of a same "kind", but they can be completely different with multiple attributes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also described this in a "Details" section of PR description. Will providing different attribute names indeed make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see how in most/all use cases the facets would be completely identical, but I could imagine another index where the facet values are the same but translated, so they wouldn't be identical.

I prefer handling an optionally nested array over magic strings, as people could already be using | in their index names

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also agree with arrays, just wasn't sure about multiple attributes, will redo it shortly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it, also outlined the docs in categoryAttribute description

'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<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 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,
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 @@ -44,8 +48,10 @@ export type CreateQuerySuggestionsPluginParams<
}): AutocompleteSource<TItem>;
/**
* 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
*/
Expand Down Expand Up @@ -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);

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