Skip to content

Commit

Permalink
wip: map recommend results to widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
aymeric-giraudet committed Apr 4, 2024
1 parent 2518d91 commit e041a2f
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 19 deletions.
21 changes: 18 additions & 3 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 @@ -1553,6 +1554,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 {array.<object>} 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;

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

RecommendResults.prototype = {
constructor: RecommendResults,
};

module.exports = RecommendResults;
3 changes: 2 additions & 1 deletion 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 @@ -1744,7 +1745,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ describe('index', () => {
createWidget({
dependsOn: 'recommend',
getWidgetParameters: jest.fn((parameters) => {
return parameters;
return parameters.addFrequentlyBoughtTogether({
$$id: 1,
objectID: 'abc',
});
}),
...args,
} as unknown as Widget);
Expand Down Expand Up @@ -2396,6 +2399,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
const paginationCreateURL = jest.fn();

const searchBox = createSearchBox({
dependsOn: 'search',
getRenderState: jest.fn((renderState, { helper, searchMetadata }) => {
return {
...renderState,
Expand Down Expand Up @@ -2512,6 +2516,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
const mainHelper = algoliasearchHelper(searchClient, 'indexName', {});
const instantSearchInstance = createInstantSearch({ mainHelper });
const searchBox = createSearchBox({
dependsOn: 'search',
getRenderState: jest.fn((renderState, { helper, searchMetadata }) => {
return {
...renderState,
Expand Down Expand Up @@ -2956,6 +2961,53 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
})
);
});

it('forwards recommend results when `dependsOn` is `recommend`', async () => {
const instance = index({ indexName: 'indexName' });
const searchClient = createSearchClient({
getRecommendations: jest.fn(() =>
Promise.resolve({
results: [{ hits: [{ objectID: '1', title: 'Recommend' }] }],
})
),
});
const mainHelper = algoliasearchHelper(searchClient, '', {});
const instantSearchInstance = createInstantSearch({
mainHelper,
});

const fbt = createFrequentlyBoughtTogether({
$$id: 1,
dependsOn: 'recommend',
shouldRender: () => true,
});
instance.addWidgets([fbt]);

instance.init(
createIndexInitOptions({
instantSearchInstance,
parent: null,
})
);
mainHelper.search();
await wait(0);
mainHelper.recommend();
await wait(0);

instance.render({
instantSearchInstance,
});

expect(fbt.render).toHaveBeenCalledWith(
expect.objectContaining({
results: expect.objectContaining({
hits: expect.arrayContaining([
{ objectID: '1', title: 'Recommend' },
]),
}),
})
);
});
});

describe('dispose', () => {
Expand Down
35 changes: 33 additions & 2 deletions packages/instantsearch.js/src/widgets/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
ScopedResult,
SearchClient,
IndexRenderState,
RenderOptions,
} from '../../types';
import type {
AlgoliaSearchHelper as Helper,
Expand All @@ -28,6 +29,7 @@ import type {
SearchResults,
AlgoliaSearchHelper,
RecommendParameters,
RecommendResultItem,
} from 'algoliasearch-helper';

const withUsage = createDocumentationMessageGenerator({
Expand Down Expand Up @@ -72,6 +74,9 @@ export type IndexWidget<TUiState extends UiState = UiState> = Omit<
getIndexId: () => string;
getHelper: () => Helper | null;
getResults: () => SearchResults | null;
getResultsForWidget: (
widget: IndexWidget | Widget
) => SearchResults | RecommendResultItem | null;
getPreviousState: () => SearchParameters | null;
getScopedResults: () => ScopedResult[];
getParent: () => IndexWidget | null;
Expand Down Expand Up @@ -297,6 +302,22 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
return derivedHelper.lastResults;
},

getResultsForWidget(widget) {
if (
widget.dependsOn !== 'recommend' ||
isIndexWidget(widget) ||
!widget.$$id
) {
return this.getResults();
}

if (!helper?.lastRecommendResults) {
return null;
}

return helper.lastRecommendResults[widget.$$id];
},

getPreviousState() {
return lastValidSearchParameters;
},
Expand Down Expand Up @@ -721,7 +742,11 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
if (widget.getRenderState) {
const renderState = widget.getRenderState(
instantSearchInstance.renderState[this.getIndexId()] || {},
createRenderArgs(instantSearchInstance, this)
createRenderArgs(
instantSearchInstance,
this,
widget
) as RenderOptions
);

storeRenderState({
Expand All @@ -741,7 +766,13 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
// not have results yet.

if (widget.render) {
widget.render(createRenderArgs(instantSearchInstance, this));
widget.render(
createRenderArgs(
instantSearchInstance,
this,
widget
) as RenderOptions
);
}
});
},
Expand Down
7 changes: 7 additions & 0 deletions tests/mocks/createAPIResponse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { RecommendResponse } from 'algoliasearch-helper';
import type {
SearchResponse,
SearchResponses,
Expand Down Expand Up @@ -86,3 +87,9 @@ export const createSFFVResponse = (
processingTimeMS: 1,
...args,
});

export const createRecommendResponse = (
requests: Array<{ objectID: string; model: string }>
): { results: RecommendResponse<Record<string, unknown>> } => {
return { results: requests.map(() => createSingleSearchResponse()) };
};
4 changes: 4 additions & 0 deletions tests/mocks/createSearchClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
createSingleSearchResponse,
createMultiSearchResponse,
createRecommendResponse,
createSFFVResponse,
} from './createAPIResponse';

Expand All @@ -9,6 +10,9 @@ import type { SearchClient, SearchResponses } from 'instantsearch.js';
export const createSearchClient = (
args: Partial<SearchClient> = {}
): SearchClient => ({
getRecommendations: jest.fn((requests) =>
Promise.resolve(createRecommendResponse(requests))
),
search: jest.fn((requests) =>
Promise.resolve(
createMultiSearchResponse(
Expand Down

0 comments on commit e041a2f

Please sign in to comment.