Skip to content

Commit

Permalink
feat(recommend): map recommend results to widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
aymeric-giraudet authored and dhayab committed May 21, 2024
1 parent 27e6f50 commit a5a87dd
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 42 deletions.
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"files": [
{
"path": "packages/algoliasearch-helper/dist/algoliasearch.helper.js",
"maxSize": "40.25 kB"
"maxSize": "40.5 kB"
},
{
"path": "packages/algoliasearch-helper/dist/algoliasearch.helper.min.js",
"maxSize": "12.75 kB"
"maxSize": "13 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
Expand Down
23 changes: 19 additions & 4 deletions packages/algoliasearch-helper/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
RelatedProductsQuery as RecommendRelatedProductsQuery,
TrendingFacetsQuery as RecommendTrendingFacetsQuery,
TrendingItemsQuery as RecommendTrendingItemsQuery,
RecommendQueriesResponse,
} from '@algolia/recommend';

/**
Expand All @@ -42,7 +43,7 @@ declare namespace algoliasearchHelper {
state: SearchParameters;
recommendState: RecommendParameters;
lastResults: SearchResults | null;
lastRecommendResults: unknown | null; // TODO: Define type in dedicated PR
lastRecommendResults: RecommendResults | null;
derivedHelpers: DerivedHelper[];

on(
Expand Down Expand Up @@ -402,15 +403,15 @@ declare namespace algoliasearchHelper {
event: 'recommend:result',
cb: (res: {
recommend: {
results: unknown | null; // TODO: Define type in dedicated PR
results: RecommendResults | null;
state: RecommendParameters;
};
}) => void
): this;
on(event: 'error', cb: (res: { error: Error }) => void): this;

lastResults: SearchResults | null;
lastRecommendResults: unknown | null; // TODO: Define type in dedicated PR
lastRecommendResults: RecommendResults | null;
detach(): void;
getModifiedState(): SearchParameters;
getModifiedRecommendState(): RecommendParameters;
Expand Down Expand Up @@ -1568,7 +1569,7 @@ declare namespace algoliasearchHelper {
export type RecommendParametersWithId<
T extends PlainRecommendParameters = PlainRecommendParameters
> = T & {
$$id: string;
$$id: number;
};

export type RecommendParametersOptions = {
Expand Down Expand Up @@ -1596,6 +1597,20 @@ declare namespace algoliasearchHelper {
params: RecommendParametersWithId<LookingSimilarQuery>
): RecommendParameters;
}

type RecommendResponse<TObject> =
RecommendQueriesResponse<TObject>['results'];

type RecommendResultItem<TObject = any> = RecommendResponse<TObject>[0];

export class RecommendResults<T = any> {
constructor(state: RecommendParameters, results: RecommendResponse<T>);

_state: RecommendParameters;
_rawResults: RecommendResponse<T>;

[index: number]: RecommendResultItem<T>;
}
}

export = algoliasearchHelper;
28 changes: 28 additions & 0 deletions packages/algoliasearch-helper/src/RecommendResults/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

/**
* Constructor for SearchResults
* @class
* @classdesc SearchResults contains the results of a query to Algolia using the
* {@link AlgoliaSearchHelper}.
* @param {RecommendParameters} state state that led to the response
* @param {Record<string,RecommendResultItem>} results the results from algolia client
**/
function RecommendResults(state, results) {
this._state = state;
this._rawResults = results;

// eslint-disable-next-line consistent-this
var self = this;

state.params.forEach(function (param) {
var id = param.$$id;
self[id] = results[id];
});
}

RecommendResults.prototype = {
constructor: RecommendResults,
};

module.exports = RecommendResults;
22 changes: 19 additions & 3 deletions packages/algoliasearch-helper/src/algoliasearch.helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var merge = require('./functions/merge');
var objectHasKeys = require('./functions/objectHasKeys');
var omit = require('./functions/omit');
var RecommendParameters = require('./RecommendParameters');
var RecommendResults = require('./RecommendResults');
var requestBuilder = require('./requestBuilder');
var SearchParameters = require('./SearchParameters');
var SearchResults = require('./SearchResults');
Expand Down Expand Up @@ -1577,6 +1578,9 @@ AlgoliaSearchHelper.prototype._recommend = function () {
var recommendState = this.recommendState;
var index = this.getIndex();
var states = [{ state: recommendState, index: index, helper: this }];
var ids = recommendState.params.map(function (param) {
return param.$$id;
});

this.emit('fetch', {
recommend: {
Expand All @@ -1603,6 +1607,13 @@ AlgoliaSearchHelper.prototype._recommend = function () {
helper: derivedHelper,
});

ids = Array.prototype.concat.apply(
ids,
derivedState.params.map(function (param) {
return param.$$id;
})
);

derivedHelper.emit('fetch', {
recommend: {
state: derivedState,
Expand Down Expand Up @@ -1639,7 +1650,7 @@ AlgoliaSearchHelper.prototype._recommend = function () {
try {
this.client
.getRecommendations(queries)
.then(this._dispatchRecommendResponse.bind(this, queryId, states))
.then(this._dispatchRecommendResponse.bind(this, queryId, states, ids))
.catch(this._dispatchRecommendError.bind(this, queryId));
} catch (error) {
// If we reach this part, we're in an internal error state
Expand Down Expand Up @@ -1713,6 +1724,7 @@ AlgoliaSearchHelper.prototype._dispatchAlgoliaResponse = function (
AlgoliaSearchHelper.prototype._dispatchRecommendResponse = function (
queryId,
states,
ids,
content
) {
// @TODO remove the number of outdated queries discarded instead of just one
Expand All @@ -1728,7 +1740,11 @@ AlgoliaSearchHelper.prototype._dispatchRecommendResponse = function (

if (this._currentNbRecommendQueries === 0) this.emit('recommendQueueEmpty');

var results = content.results.slice();
var results = {};
content.results.forEach(function (result, index) {
var id = ids[index];
results[id] = result;
});

states.forEach(function (s) {
var state = s.state;
Expand All @@ -1744,7 +1760,7 @@ AlgoliaSearchHelper.prototype._dispatchRecommendResponse = function (
return;
}

helper.lastRecommendResults = results;
helper.lastRecommendResults = new RecommendResults(state, results);

// eslint-disable-next-line no-warning-comments
// TODO: emit "result" event when events for Recommend are implemented
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,12 @@ function getData() {
var recommendParams = new RecommendParameters({
params: [
{
$$id: '1',
$$id: 1,
objectID: 'A0E20000000279B',
model: 'bought-together',
},
{
$$id: '2',
$$id: 2,
facetName: 'brand',
model: 'trending-facets',
},
Expand Down
36 changes: 20 additions & 16 deletions packages/algoliasearch-helper/test/spec/recommend.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ describe('recommend()', () => {
var helper = algoliasearchHelper(client, 'indexName', {});

helper.addFrequentlyBoughtTogether({
$$id: '1',
$$id: 1,
objectID: 'A0E20000000279B',
});
helper.addTrendingFacets({ $$id: '2', facetName: 'brand' });
helper.addTrendingFacets({ $$id: 2, facetName: 'brand' });

// eslint-disable-next-line no-warning-comments
// TODO: listen to "result" event when events for Recommend are implemented
helper.on('recommend:result', (event) => {
var results = event.recommend.results;
expect(results).toHaveLength(2);
// As it also includes '_state' and '_rawResults'
expect(Object.keys(results)).toHaveLength(4);

expect(results[1]).toBe(testData.response.results[0]);
expect(results[2]).toBe(testData.response.results[1]);

var state = event.recommend.state;
expect(state.params).toEqual(testData.recommendParams.params);
Expand Down Expand Up @@ -53,7 +57,7 @@ describe('recommend()', () => {
};

var helper = algoliasearchHelper(client, 'indexName', {});
helper.addTrendingFacets({ $$id: '2', facetName: 'brand' });
helper.addTrendingFacets({ $$id: 2, facetName: 'brand' });

helper.on('fetch', (event) => {
var state = event.recommend.state;
Expand Down Expand Up @@ -84,7 +88,7 @@ describe('recommend()', () => {
console.warn = jest.fn();

var helper = algoliasearchHelper(client, 'indexName', {});
helper.addFrequentlyBoughtTogether({ $$id: '1' }).recommend();
helper.addFrequentlyBoughtTogether({ $$id: 1 }).recommend();

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenLastCalledWith(message);
Expand All @@ -99,20 +103,20 @@ describe('recommend()', () => {

var helper = algoliasearchHelper(client, 'indexName', {});

helper.addFrequentlyBoughtTogether({ $$id: '1' });
helper.removeFrequentlyBoughtTogether('1');
helper.addRelatedProducts({ $$id: '2' });
helper.removeRelatedProducts('2');
helper.addTrendingItems({ $$id: '3' });
helper.removeTrendingItems('3');
helper.addTrendingFacets({ $$id: '4' });
helper.removeTrendingFacets('4');
helper.addLookingSimilar({ $$id: '5' });
helper.removeLookingSimilar('5');
helper.addFrequentlyBoughtTogether({ $$id: 1 });
helper.removeFrequentlyBoughtTogether(1);
helper.addRelatedProducts({ $$id: 2 });
helper.removeRelatedProducts(2);
helper.addTrendingItems({ $$id: 3 });
helper.removeTrendingItems(3);
helper.addTrendingFacets({ $$id: 4 });
helper.removeTrendingFacets(4);
helper.addLookingSimilar({ $$id: 5 });
helper.removeLookingSimilar(5);

expect(client.getRecommendations).not.toHaveBeenCalled();

helper.addFrequentlyBoughtTogether({ $$id: '6' });
helper.addFrequentlyBoughtTogether({ $$id: 6 });
helper.recommend();

expect(client.getRecommendations).toHaveBeenCalledTimes(1);
Expand Down
9 changes: 5 additions & 4 deletions packages/instantsearch.js/src/lib/utils/render-args.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InstantSearch, UiState } from '../../types';
import type { InstantSearch, UiState, Widget } from '../../types';
import type { IndexWidget } from '../../widgets/index/index';

export function createInitArgs(
Expand Down Expand Up @@ -27,9 +27,10 @@ export function createInitArgs(

export function createRenderArgs(
instantSearchInstance: InstantSearch,
parent: IndexWidget
parent: IndexWidget,
widget: IndexWidget | Widget
) {
const results = parent.getResults()!;
const results = parent.getResultsForWidget(widget)!;
const helper = parent.getHelper()!;

return {
Expand All @@ -38,7 +39,7 @@ export function createRenderArgs(
instantSearchInstance,
results,
scopedResults: parent.getScopedResults(),
state: results ? results._state : helper.state,
state: results && '_state' in results ? results._state : helper.state,
renderState: instantSearchInstance.renderState,
templatesConfig: instantSearchInstance.templatesConfig,
createURL: parent.createURL,
Expand Down
36 changes: 28 additions & 8 deletions packages/instantsearch.js/src/types/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SearchParameters,
SearchResults,
RecommendParameters,
RecommendResultItem,
} from 'algoliasearch-helper';

export type ScopedResult = {
Expand Down Expand Up @@ -137,7 +138,7 @@ export type WidgetDescription = {
indexUiState?: Record<string, unknown>;
};

type SearchWidgetLifeCycle<TWidgetDescription extends WidgetDescription> = {
type SearchWidget<TWidgetDescription extends WidgetDescription> = {
dependsOn?: 'search';
getWidgetParameters?: (
state: SearchParameters,
Expand All @@ -149,8 +150,15 @@ type SearchWidgetLifeCycle<TWidgetDescription extends WidgetDescription> = {
) => SearchParameters;
};

type RecommendWidgetLifeCycle<TWidgetDescription extends WidgetDescription> = {
dependsOn?: 'recommend';
type RecommmendRenderOptions = SharedRenderOptions & {
results: RecommendResultItem;
};

type RecommendWidget<
TWidgetDescription extends WidgetDescription & WidgetParams
> = {
dependsOn: 'recommend';
$$id: number;
getWidgetParameters: (
state: RecommendParameters,
widgetParametersOptions: {
Expand All @@ -159,6 +167,20 @@ type RecommendWidgetLifeCycle<TWidgetDescription extends WidgetDescription> = {
>;
}
) => RecommendParameters;
getRenderState: (
renderState: Expand<
IndexRenderState & Partial<TWidgetDescription['indexRenderState']>
>,
renderOptions: InitOptions | RecommmendRenderOptions
) => IndexRenderState & TWidgetDescription['indexRenderState'];
getWidgetRenderState: (
renderOptions: InitOptions | RecommmendRenderOptions
) => Expand<
WidgetRenderState<
TWidgetDescription['renderState'],
TWidgetDescription['widgetParams']
>
>;
};

type RequiredWidgetLifeCycle<TWidgetDescription extends WidgetDescription> = {
Expand Down Expand Up @@ -247,10 +269,7 @@ type RequiredUiStateLifeCycle<TWidgetDescription extends WidgetDescription> = {
>;
}
) => SearchParameters;
} & (
| SearchWidgetLifeCycle<TWidgetDescription>
| RecommendWidgetLifeCycle<TWidgetDescription>
);
};

type UiStateLifeCycle<TWidgetDescription extends WidgetDescription> =
TWidgetDescription extends RequiredKeys<WidgetDescription, 'indexUiState'>
Expand Down Expand Up @@ -302,7 +321,8 @@ export type Widget<
WidgetType<TWidgetDescription> &
UiStateLifeCycle<TWidgetDescription> &
RenderStateLifeCycle<TWidgetDescription>
>;
> &
(SearchWidget<TWidgetDescription> | RecommendWidget<TWidgetDescription>);

export type TransformItemsMetadata = {
results?: SearchResults;
Expand Down
Loading

0 comments on commit a5a87dd

Please sign in to comment.