Skip to content

Commit 9e9d839

Browse files
authoredJul 5, 2021
feat(facets): apply result from facet ordering (#4784)
* feat(facets): apply sort from facetOrdering * feat(facets): apply result from facet ordering This adds a new option "facetOrdering" (boolean) to refinementList, menu, hierarchicalMenu which will read facet ordering from the results if available, but fall back to sortBy if no facetOrdering is available. The option facetOrdering defaults to `true` if no sortBy is given, to make it apply out of the box. references: - NLP-110 - [RFC 45](https://github.com/algolia/instantsearch-rfcs/blob/master/accepted/flexible-facet-values.md) * forward facetOrdering option from widget * suppress v3 ts errors * remove option * test: rename
1 parent dc2fd95 commit 9e9d839

File tree

8 files changed

+475
-12
lines changed

8 files changed

+475
-12
lines changed
 

‎src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts

+160
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,166 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hierarchica
691691
canToggleShowMore: false,
692692
});
693693
});
694+
695+
describe('facetOrdering', () => {
696+
const resultsViaFacetOrdering = [
697+
{
698+
count: 47,
699+
data: null,
700+
exhaustive: true,
701+
isRefined: false,
702+
label: 'Outdoor',
703+
value: 'Outdoor',
704+
},
705+
{
706+
count: 880,
707+
data: [
708+
{
709+
count: 173,
710+
data: null,
711+
exhaustive: true,
712+
isRefined: false,
713+
label: 'Frames & pictures',
714+
value: 'Decoration > Frames & pictures',
715+
},
716+
{
717+
count: 193,
718+
data: null,
719+
exhaustive: true,
720+
isRefined: false,
721+
label: 'Candle holders & candles',
722+
value: 'Decoration > Candle holders & candles',
723+
},
724+
],
725+
exhaustive: true,
726+
isRefined: true,
727+
label: 'Decoration',
728+
value: 'Decoration',
729+
},
730+
];
731+
const resultsViaSortBy = [
732+
{
733+
count: 880,
734+
data: [
735+
{
736+
count: 193,
737+
data: null,
738+
exhaustive: true,
739+
isRefined: false,
740+
label: 'Candle holders & candles',
741+
value: 'Decoration > Candle holders & candles',
742+
},
743+
{
744+
count: 173,
745+
data: null,
746+
exhaustive: true,
747+
isRefined: false,
748+
label: 'Frames & pictures',
749+
value: 'Decoration > Frames & pictures',
750+
},
751+
],
752+
exhaustive: true,
753+
isRefined: true,
754+
label: 'Decoration',
755+
value: 'Decoration',
756+
},
757+
{
758+
count: 47,
759+
data: null,
760+
exhaustive: true,
761+
isRefined: false,
762+
label: 'Outdoor',
763+
value: 'Outdoor',
764+
},
765+
];
766+
767+
test.each`
768+
facetOrderingInResult | sortBy | expected
769+
${true} | ${undefined} | ${resultsViaFacetOrdering}
770+
${false} | ${undefined} | ${resultsViaSortBy}
771+
${true} | ${['name:asc']} | ${resultsViaSortBy}
772+
${false} | ${['name:asc']} | ${resultsViaSortBy}
773+
`(
774+
'renderingContent present: $facetOrderingInResult, sortBy: $sortBy',
775+
({ facetOrderingInResult, sortBy, expected }) => {
776+
const renderFn = jest.fn();
777+
const unmountFn = jest.fn();
778+
const createHierarchicalMenu = connectHierarchicalMenu(
779+
renderFn,
780+
unmountFn
781+
);
782+
const hierarchicalMenu = createHierarchicalMenu({
783+
attributes: ['category', 'subCategory'],
784+
sortBy,
785+
});
786+
const helper = algoliasearchHelper(
787+
createSearchClient(),
788+
'indexName',
789+
hierarchicalMenu.getWidgetSearchParameters!(
790+
new SearchParameters(),
791+
{
792+
uiState: {
793+
hierarchicalMenu: {
794+
category: ['Decoration'],
795+
},
796+
},
797+
}
798+
)
799+
);
800+
801+
hierarchicalMenu.init!(createInitOptions({ helper }));
802+
803+
const renderingContent = facetOrderingInResult
804+
? {
805+
facetOrdering: {
806+
values: {
807+
category: {
808+
order: ['Outdoor'],
809+
sortRemainingBy: 'alpha' as const,
810+
},
811+
subCategory: {
812+
order: ['Decoration > Frames & pictures'],
813+
sortRemainingBy: 'count' as const,
814+
},
815+
},
816+
},
817+
}
818+
: undefined;
819+
820+
const results = new SearchResults(helper.state, [
821+
createSingleSearchResponse({
822+
renderingContent,
823+
facets: {
824+
category: {
825+
Decoration: 880,
826+
},
827+
subCategory: {
828+
'Decoration > Candle holders & candles': 193,
829+
'Decoration > Frames & pictures': 173,
830+
},
831+
},
832+
}),
833+
createSingleSearchResponse({
834+
facets: {
835+
category: {
836+
Decoration: 880,
837+
Outdoor: 47,
838+
},
839+
},
840+
}),
841+
]);
842+
843+
const renderState = hierarchicalMenu.getWidgetRenderState(
844+
createRenderOptions({
845+
helper,
846+
results,
847+
})
848+
);
849+
850+
expect(renderState.items).toEqual(expected);
851+
}
852+
);
853+
});
694854
});
695855

696856
describe('getWidgetUiState', () => {

‎src/connectors/hierarchical-menu/connectHierarchicalMenu.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const withUsage = createDocumentationMessageGenerator({
2323
connector: true,
2424
});
2525

26+
const DEFAULT_SORT = ['name:asc'];
27+
2628
export type HierarchicalMenuItem = {
2729
/**
2830
* Value of the menu item.
@@ -79,6 +81,8 @@ export type HierarchicalMenuConnectorParams = {
7981
/**
8082
* How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
8183
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
84+
*
85+
* If a facetOrdering is set in the index settings, it is used when sortBy isn't passed
8286
*/
8387
sortBy?: SortBy<HierarchicalMenuItem>;
8488
/**
@@ -174,7 +178,7 @@ const connectHierarchicalMenu: HierarchicalMenuConnector = function connectHiera
174178
limit = 10,
175179
showMore = false,
176180
showMoreLimit = 20,
177-
sortBy = ['name:asc'],
181+
sortBy = DEFAULT_SORT,
178182
transformItems = (items => items) as TransformItems<HierarchicalMenuItem>,
179183
} = widgetParams || {};
180184

@@ -273,11 +277,6 @@ const connectHierarchicalMenu: HierarchicalMenuConnector = function connectHiera
273277
);
274278
},
275279

276-
/**
277-
* @param {Object} param0 cleanup arguments
278-
* @param {any} param0.state current search parameters
279-
* @returns {any} next search parameters
280-
*/
281280
dispose({ state }) {
282281
unmountFn();
283282

@@ -336,6 +335,7 @@ const connectHierarchicalMenu: HierarchicalMenuConnector = function connectHiera
336335
if (results) {
337336
const facetValues = results.getFacetValues(hierarchicalFacetName, {
338337
sortBy,
338+
facetOrdering: sortBy === DEFAULT_SORT,
339339
});
340340
const facetItems =
341341
facetValues && !Array.isArray(facetValues) && facetValues.data

‎src/connectors/menu/__tests__/connectMenu-test.ts

+174-1
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co
560560
});
561561

562562
describe('getWidgetRenderState', () => {
563-
test('returns the widget render state', () => {
563+
test('returns the widget render state (init)', () => {
564564
const renderFn = jest.fn();
565565
const unmountFn = jest.fn();
566566
const createMenu = connectMenu(renderFn, unmountFn);
@@ -589,6 +589,179 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co
589589
widgetParams: { attribute: 'brand' },
590590
});
591591
});
592+
593+
test('returns the widget render state (render)', () => {
594+
const renderFn = jest.fn();
595+
const unmountFn = jest.fn();
596+
const createMenu = connectMenu(renderFn, unmountFn);
597+
const menu = createMenu({
598+
attribute: 'brand',
599+
});
600+
const helper = jsHelper(
601+
createSearchClient(),
602+
'indexName',
603+
menu.getWidgetSearchParameters!(new SearchParameters(), { uiState: {} })
604+
);
605+
606+
const renderState1 = menu.getWidgetRenderState(
607+
createRenderOptions({
608+
helper,
609+
results: new SearchResults(helper.state, [
610+
createSingleSearchResponse({
611+
facets: {
612+
brand: {
613+
Apple: 100,
614+
Samsung: 1,
615+
},
616+
},
617+
}),
618+
]),
619+
})
620+
);
621+
622+
expect(renderState1).toEqual({
623+
items: [
624+
{
625+
count: 100,
626+
data: null,
627+
exhaustive: true,
628+
isRefined: false,
629+
label: 'Apple',
630+
value: 'Apple',
631+
},
632+
{
633+
count: 1,
634+
data: null,
635+
exhaustive: true,
636+
isRefined: false,
637+
label: 'Samsung',
638+
value: 'Samsung',
639+
},
640+
],
641+
createURL: expect.any(Function),
642+
refine: expect.any(Function),
643+
sendEvent: expect.any(Function),
644+
canRefine: true,
645+
isShowingMore: false,
646+
toggleShowMore: expect.any(Function),
647+
canToggleShowMore: false,
648+
widgetParams: { attribute: 'brand' },
649+
});
650+
});
651+
652+
describe('facetOrdering', () => {
653+
const resultsViaFacetOrdering = [
654+
{
655+
count: 1,
656+
data: null,
657+
exhaustive: true,
658+
isRefined: false,
659+
label: 'Samsung',
660+
value: 'Samsung',
661+
},
662+
{
663+
count: 100,
664+
data: null,
665+
exhaustive: true,
666+
isRefined: false,
667+
label: 'Apple',
668+
value: 'Apple',
669+
},
670+
{
671+
count: 3,
672+
data: null,
673+
exhaustive: true,
674+
isRefined: false,
675+
label: 'Algolia',
676+
value: 'Algolia',
677+
},
678+
];
679+
const resultsViaSortBy = [
680+
{
681+
count: 3,
682+
data: null,
683+
exhaustive: true,
684+
isRefined: false,
685+
label: 'Algolia',
686+
value: 'Algolia',
687+
},
688+
{
689+
count: 100,
690+
data: null,
691+
exhaustive: true,
692+
isRefined: false,
693+
label: 'Apple',
694+
value: 'Apple',
695+
},
696+
{
697+
count: 1,
698+
data: null,
699+
exhaustive: true,
700+
isRefined: false,
701+
label: 'Samsung',
702+
value: 'Samsung',
703+
},
704+
];
705+
706+
test.each`
707+
facetOrderingInResult | sortBy | expected
708+
${true} | ${undefined} | ${resultsViaFacetOrdering}
709+
${false} | ${undefined} | ${resultsViaSortBy}
710+
${true} | ${['name:asc']} | ${resultsViaSortBy}
711+
${false} | ${['name:asc']} | ${resultsViaSortBy}
712+
`(
713+
'renderingContent present: $facetOrderingInResult, sortBy: $sortBy',
714+
({ facetOrderingInResult, sortBy, expected }) => {
715+
const renderFn = jest.fn();
716+
const unmountFn = jest.fn();
717+
const createMenu = connectMenu(renderFn, unmountFn);
718+
const menu = createMenu({
719+
attribute: 'brand',
720+
sortBy,
721+
});
722+
const helper = jsHelper(
723+
createSearchClient(),
724+
'indexName',
725+
menu.getWidgetSearchParameters!(new SearchParameters(), {
726+
uiState: {},
727+
})
728+
);
729+
730+
const renderingContent = facetOrderingInResult
731+
? {
732+
facetOrdering: {
733+
values: {
734+
brand: {
735+
order: ['Samsung'],
736+
sortRemainingBy: 'count' as const,
737+
},
738+
},
739+
},
740+
}
741+
: undefined;
742+
743+
const renderState1 = menu.getWidgetRenderState(
744+
createRenderOptions({
745+
helper,
746+
results: new SearchResults(helper.state, [
747+
createSingleSearchResponse({
748+
renderingContent,
749+
facets: {
750+
brand: {
751+
Apple: 100,
752+
Algolia: 3,
753+
Samsung: 1,
754+
},
755+
},
756+
}),
757+
]),
758+
})
759+
);
760+
761+
expect(renderState1.items).toEqual(expected);
762+
}
763+
);
764+
});
592765
});
593766

594767
describe('showMore', () => {

‎src/connectors/menu/connectMenu.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const withUsage = createDocumentationMessageGenerator({
1919
connector: true,
2020
});
2121

22+
const DEFAULT_SORT = ['isRefined', 'name:asc'];
23+
2224
export type MenuItem = {
2325
/**
2426
* The value of the menu item.
@@ -59,6 +61,8 @@ export type MenuConnectorParams = {
5961
* How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
6062
*
6163
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
64+
*
65+
* If a facetOrdering is set in the index settings, it is used when sortBy isn't passed
6266
*/
6367
sortBy?: SortBy<MenuItem>;
6468
/**
@@ -147,7 +151,7 @@ const connectMenu: MenuConnector = function connectMenu(
147151
limit = 10,
148152
showMore = false,
149153
showMoreLimit = 20,
150-
sortBy = ['isRefined', 'name:asc'],
154+
sortBy = DEFAULT_SORT,
151155
transformItems = (items => items) as TransformItems<MenuItem>,
152156
} = widgetParams || {};
153157

@@ -286,6 +290,7 @@ const connectMenu: MenuConnector = function connectMenu(
286290
if (results) {
287291
const facetValues = results.getFacetValues(attribute, {
288292
sortBy,
293+
facetOrdering: sortBy === DEFAULT_SORT,
289294
});
290295
const facetItems =
291296
facetValues && !Array.isArray(facetValues) && facetValues.data

‎src/connectors/refinement-list/__tests__/connectRefinementList-test.ts

+113
Original file line numberDiff line numberDiff line change
@@ -2589,6 +2589,119 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement-
25892589
})
25902590
);
25912591
});
2592+
2593+
describe('facetOrdering', () => {
2594+
const resultsViaFacetOrdering = [
2595+
{
2596+
count: 66,
2597+
highlighted: 'Microsoft',
2598+
isRefined: false,
2599+
label: 'Microsoft',
2600+
value: 'Microsoft',
2601+
},
2602+
{
2603+
count: 88,
2604+
highlighted: 'Apple',
2605+
isRefined: true,
2606+
label: 'Apple',
2607+
value: 'Apple',
2608+
},
2609+
{
2610+
count: 44,
2611+
highlighted: 'Samsung',
2612+
isRefined: true,
2613+
label: 'Samsung',
2614+
value: 'Samsung',
2615+
},
2616+
];
2617+
const resultsViaSortBy = [
2618+
{
2619+
count: 88,
2620+
highlighted: 'Apple',
2621+
isRefined: true,
2622+
label: 'Apple',
2623+
value: 'Apple',
2624+
},
2625+
{
2626+
count: 44,
2627+
highlighted: 'Samsung',
2628+
isRefined: true,
2629+
label: 'Samsung',
2630+
value: 'Samsung',
2631+
},
2632+
{
2633+
count: 66,
2634+
highlighted: 'Microsoft',
2635+
isRefined: false,
2636+
label: 'Microsoft',
2637+
value: 'Microsoft',
2638+
},
2639+
];
2640+
2641+
test.each`
2642+
facetOrderingInResult | sortBy | expected
2643+
${true} | ${undefined} | ${resultsViaFacetOrdering}
2644+
${false} | ${undefined} | ${resultsViaSortBy}
2645+
${true} | ${['isRefined']} | ${resultsViaSortBy}
2646+
${false} | ${['isRefined']} | ${resultsViaSortBy}
2647+
`(
2648+
'renderingContent present: $facetOrderingInResult, sortBy: $sortBy',
2649+
({ facetOrderingInResult, sortBy, expected }) => {
2650+
const renderFn = jest.fn();
2651+
const unmountFn = jest.fn();
2652+
const createRefinementList = connectRefinementList(
2653+
renderFn,
2654+
unmountFn
2655+
);
2656+
const refinementList = createRefinementList({
2657+
attribute: 'brand',
2658+
sortBy,
2659+
});
2660+
const helper = jsHelper(
2661+
createSearchClient(),
2662+
'indexName',
2663+
refinementList.getWidgetSearchParameters!(new SearchParameters(), {
2664+
uiState: {
2665+
refinementList: { brand: ['Apple', 'Samsung'] },
2666+
},
2667+
})
2668+
);
2669+
2670+
const renderingContent = facetOrderingInResult
2671+
? {
2672+
facetOrdering: {
2673+
values: {
2674+
brand: {
2675+
order: ['Microsoft'],
2676+
sortRemainingBy: 'alpha' as const,
2677+
},
2678+
},
2679+
},
2680+
}
2681+
: undefined;
2682+
2683+
const renderState1 = refinementList.getWidgetRenderState(
2684+
createRenderOptions({
2685+
helper,
2686+
results: new SearchResults(helper.state, [
2687+
createSingleSearchResponse({
2688+
renderingContent,
2689+
facets: {
2690+
brand: {
2691+
Apple: 88,
2692+
Microsoft: 66,
2693+
Samsung: 44,
2694+
},
2695+
},
2696+
}),
2697+
]),
2698+
})
2699+
);
2700+
2701+
expect(renderState1.items).toEqual(expected);
2702+
}
2703+
);
2704+
});
25922705
});
25932706

25942707
describe('getWidgetSearchParameters', () => {

‎src/connectors/refinement-list/connectRefinementList.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const withUsage = createDocumentationMessageGenerator({
2626
connector: true,
2727
});
2828

29+
const DEFAULT_SORT = ['isRefined', 'count:desc', 'name:asc'];
30+
2931
export type RefinementListItem = {
3032
/**
3133
* The value of the refinement list item.
@@ -74,6 +76,10 @@ export type RefinementListConnectorParams = {
7476
showMoreLimit?: number;
7577
/**
7678
* How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
79+
*
80+
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
81+
*
82+
* If a facetOrdering is set in the index settings, it is used when sortBy isn't passed
7783
*/
7884
sortBy?: SortBy<RefinementListItem>;
7985
/**
@@ -182,7 +188,7 @@ const connectRefinementList: RefinementListConnector = function connectRefinemen
182188
limit = 10,
183189
showMore = false,
184190
showMoreLimit = 20,
185-
sortBy = ['isRefined', 'count:desc', 'name:asc'],
191+
sortBy = DEFAULT_SORT,
186192
escapeFacetValues = true,
187193
transformItems = (items => items) as TransformItems<RefinementListItem>,
188194
} = widgetParams || {};
@@ -386,6 +392,7 @@ const connectRefinementList: RefinementListConnector = function connectRefinemen
386392
if (results) {
387393
const values = results.getFacetValues(attribute, {
388394
sortBy,
395+
facetOrdering: sortBy === DEFAULT_SORT,
389396
});
390397
facetValues = values && Array.isArray(values) ? values : [];
391398
items = transformItems(

‎src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,21 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hierarchica
133133

134134
expect(results.getFacetValues).toHaveBeenCalledTimes(1);
135135
expect(results.getFacetValues).toHaveBeenCalledWith('hello', {
136+
facetOrdering: true,
136137
sortBy: ['name:asc'],
137138
});
138139
});
139140

140141
it('has a sortBy option', () => {
141-
widget = hierarchicalMenu({ ...options, sortBy: ['name:asc'] });
142+
widget = hierarchicalMenu({ ...options, sortBy: ['name:desc'] });
142143

143144
widget.init!(createInitOptions({ helper }));
144145
widget.render!(createRenderOptions({ results, state }));
145146

146147
expect(results.getFacetValues).toHaveBeenCalledTimes(1);
147148
expect(results.getFacetValues).toHaveBeenCalledWith('hello', {
148-
sortBy: ['name:asc'],
149+
facetOrdering: false,
150+
sortBy: ['name:desc'],
149151
});
150152
});
151153

‎tsconfig.v3.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
// v3 has a wrong definition for optionalWords (only accepts string[])
1414
"src/connectors/voice-search/__tests__/connectVoiceSearch-test.ts",
1515
// v3 does not have renderingContent (only errors in the test)
16-
"src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts"
16+
"src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts",
17+
"src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts",
18+
"src/connectors/menu/__tests__/connectMenu-test.ts",
19+
"src/connectors/refinement-list/__tests__/connectRefinementList-test.ts"
1720
]
1821
}

0 commit comments

Comments
 (0)
Please sign in to comment.