Skip to content

feat(recommend): Add trending types and methods #1396

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

Merged
merged 9 commits into from
Mar 14, 2022
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
},
{
"path": "packages/recommend/dist/recommend.umd.js",
"maxSize": "4.1KB"
"maxSize": "4.2KB"
}
]
}
46 changes: 46 additions & 0 deletions packages/recommend/src/__tests__/getTrendingFacets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TestSuite } from '../../../client-common/src/__tests__/TestSuite';

const recommend = new TestSuite('recommend').recommend;

function createMockedClient() {
const client = recommend('appId', 'apiKey');
jest.spyOn(client.transporter, 'read').mockImplementation(() => Promise.resolve());

return client;
}

describe('getTrendingFacets', () => {
test('builds the request', async () => {
const client = createMockedClient();

await client.getTrendingFacets(
[
{
indexName: 'products',
facetName: 'company',
},
],
{}
);

expect(client.transporter.read).toHaveBeenCalledTimes(1);
expect(client.transporter.read).toHaveBeenCalledWith(
{
cacheable: true,
data: {
requests: [
{
indexName: 'products',
model: 'trending-facets',
facetName: 'company',
threshold: 0,
},
],
},
method: 'POST',
path: '1/indexes/*/recommendations',
},
{}
);
});
});
44 changes: 44 additions & 0 deletions packages/recommend/src/__tests__/getTrendingItems.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TestSuite } from '../../../client-common/src/__tests__/TestSuite';

const recommend = new TestSuite('recommend').recommend;

function createMockedClient() {
const client = recommend('appId', 'apiKey');
jest.spyOn(client.transporter, 'read').mockImplementation(() => Promise.resolve());

return client;
}

describe('getTrendingItems', () => {
test('builds the request', async () => {
const client = createMockedClient();

await client.getTrendingItems(
[
{
indexName: 'products',
},
],
{}
);

expect(client.transporter.read).toHaveBeenCalledTimes(1);
expect(client.transporter.read).toHaveBeenCalledWith(
{
cacheable: true,
data: {
requests: [
{
indexName: 'products',
model: 'trending-items',
threshold: 0,
},
],
},
method: 'POST',
path: '1/indexes/*/recommendations',
},
{}
);
});
});
10 changes: 9 additions & 1 deletion packages/recommend/src/builds/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { createBrowserXhrRequester } from '@algolia/requester-browser-xhr';
import { createUserAgent } from '@algolia/transporter';

import { createRecommendClient } from '../createRecommendClient';
import { getFrequentlyBoughtTogether, getRecommendations, getRelatedProducts } from '../methods';
import {
getFrequentlyBoughtTogether,
getRecommendations,
getRelatedProducts,
getTrendingFacets,
getTrendingItems,
} from '../methods';
import { BaseRecommendClient, RecommendOptions, WithRecommendMethods } from '../types';

export default function recommend(
Expand Down Expand Up @@ -47,6 +53,8 @@ export default function recommend(
getFrequentlyBoughtTogether,
getRecommendations,
getRelatedProducts,
getTrendingFacets,
getTrendingItems,
},
});
}
Expand Down
10 changes: 9 additions & 1 deletion packages/recommend/src/builds/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { createNodeHttpRequester } from '@algolia/requester-node-http';
import { createUserAgent } from '@algolia/transporter';

import { createRecommendClient } from '../createRecommendClient';
import { getFrequentlyBoughtTogether, getRecommendations, getRelatedProducts } from '../methods';
import {
getFrequentlyBoughtTogether,
getRecommendations,
getRelatedProducts,
getTrendingFacets,
getTrendingItems,
} from '../methods';
import { BaseRecommendClient, RecommendOptions, WithRecommendMethods } from '../types';

export default function recommend(
Expand Down Expand Up @@ -41,6 +47,8 @@ export default function recommend(
getFrequentlyBoughtTogether,
getRecommendations,
getRelatedProducts,
getTrendingFacets,
getTrendingItems,
},
});
}
Expand Down
32 changes: 32 additions & 0 deletions packages/recommend/src/methods/getTrendingFacets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MethodEnum } from '@algolia/requester-common';

import { BaseRecommendClient, TrendingFacetsQuery, WithRecommendMethods } from '../types';

type GetTrendingFacets = (
base: BaseRecommendClient
) => WithRecommendMethods<BaseRecommendClient>['getTrendingFacets'];

export const getTrendingFacets: GetTrendingFacets = base => {
return (queries: readonly TrendingFacetsQuery[], requestOptions) => {
const requests: readonly TrendingFacetsQuery[] = queries.map(query => ({
...query,
model: 'trending-facets',
// The `threshold` param is required by the endpoint to make it easier
// to provide a default value later, so we default it in the client
// so that users don't have to provide a value.
threshold: query.threshold || 0,
}));

return base.transporter.read(
{
method: MethodEnum.Post,
path: '1/indexes/*/recommendations',
data: {
requests,
},
cacheable: true,
},
requestOptions
);
};
};
32 changes: 32 additions & 0 deletions packages/recommend/src/methods/getTrendingItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MethodEnum } from '@algolia/requester-common';

import { BaseRecommendClient, TrendingItemsQuery, WithRecommendMethods } from '../types';

type GetTrendingItems = (
base: BaseRecommendClient
) => WithRecommendMethods<BaseRecommendClient>['getTrendingItems'];

export const getTrendingItems: GetTrendingItems = base => {
return (queries: readonly TrendingItemsQuery[], requestOptions) => {
const requests: readonly TrendingItemsQuery[] = queries.map(query => ({
...query,
model: 'trending-items',
// The `threshold` param is required by the endpoint to make it easier
// to provide a default value later, so we default it in the client
// so that users don't have to provide a value.
threshold: query.threshold || 0,
}));

return base.transporter.read(
{
method: MethodEnum.Post,
path: '1/indexes/*/recommendations',
data: {
requests,
},
cacheable: true,
},
requestOptions
);
};
};
2 changes: 2 additions & 0 deletions packages/recommend/src/methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
export * from './getFrequentlyBoughtTogether';
export * from './getRecommendations';
export * from './getRelatedProducts';
export * from './getTrendingFacets';
export * from './getTrendingItems';
5 changes: 3 additions & 2 deletions packages/recommend/src/types/FrequentlyBoughtTogetherQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import { RecommendationsQuery } from './RecommendationsQuery';

export type FrequentlyBoughtTogetherQuery = Omit<
RecommendationsQuery,
'model' | 'fallbackParameters'
>;
'model' | 'fallbackParameters' | 'facetName' | 'facetValue'
> &
Required<Pick<RecommendationsQuery, 'objectID'>>;
3 changes: 2 additions & 1 deletion packages/recommend/src/types/RecommendModel.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type RecommendModel = 'related-products' | 'bought-together';
export type RecommendModel = 'related-products' | 'bought-together' | TrendingModel;
export type TrendingModel = 'trending-items' | 'trending-facets';
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export type RecommendModel = 'related-products' | 'bought-together' | TrendingModel;
export type TrendingModel = 'trending-items' | 'trending-facets';
export type TrendingModel = 'trending-items' | 'trending-facets';
export type RecommendModel = 'related-products' | 'bought-together' | TrendingModel;

Copy link
Member

Choose a reason for hiding this comment

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

Sorry D: #1399

12 changes: 11 additions & 1 deletion packages/recommend/src/types/RecommendationsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type RecommendationsQuery = {
/**
* The `objectID` of the item to get recommendations for.
*/
readonly objectID: string;
readonly objectID?: string;
Copy link
Author

Choose a reason for hiding this comment

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

I didn't want to break the existing implementation for related-products and bought-together. So I put it optional but it is still mandatory for these models :/

Copy link
Contributor

Choose a reason for hiding this comment

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

it should be required, but omitted from the new items then probably, as how facetName is omitted for fbt

Copy link
Member

Choose a reason for hiding this comment

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

It is omitted actually, how come it's still needed?

Copy link
Author

@SophieManley03 SophieManley03 Feb 28, 2022

Choose a reason for hiding this comment

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

the problem if I do that is with getRecommendations . getRecommendation is used for every get method of Recommend and the objectID will be mandatory even for trends

Copy link
Member

Choose a reason for hiding this comment

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

Can we still remodel the code so that types are accurate?

Copy link
Author

@SophieManley03 SophieManley03 Mar 7, 2022

Choose a reason for hiding this comment

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

So after a third thought, I wanted to try to make it optional here but required for RP and FBT using Required in their own Query type.
I did that to be able to use getRecommendations for all models.

Copy link
Author

Choose a reason for hiding this comment

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

Which bring me to a question after a discussion with @PLNech:
What is the purpose of getRecommendation? Does it make sense to make it retrieve recommendations for all kind of model? @francoischalifour maybe you'll know?

Copy link
Member

Choose a reason for hiding this comment

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

Put differently: was getRecommendation a hack while we defined the actual model names before GA, or is it a feature designed to allow using not-yet-public new models?

  • If the former, then it might be time we drop this generic method (which brings subpar "common denominator" DX compared to specialized methods) and only implement Trending as a new, fully-typed method
  • If the latter, we'll need to find a generic implementation that is least disruptive to current users.

Copy link
Member

Choose a reason for hiding this comment

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

getRecommendations() is useful for the situation we're in right now: users can already use the trending model, without it being officially supported with a method in the client.

We didn't have a team to maintain this Recommend client initially so we provided it as an "escape hatch" if we're not reactive to support new models in the client.

getRecommendations() was just the very initial implementation that satisfied us at the time, feel free to change it as you need. And if it's not helpful for the new model, don't feel like you need to use it. (In the future, we can also deprecate it.)

Copy link

Choose a reason for hiding this comment

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

I'll ask the opposite question then: what's the purpose of having a specialized method for each model rather than just using getRecommendations()? It's basically a multi-query endpoint, and I think it has its uses (like making a single call to retrieve both trending items and trending facets for example). While calling getRelatedProducts() as a multi-query is strange IMO.


/**
* Threshold for the recommendations confidence score (between 0 and 100). Only recommendations with a greater score are returned.
Expand All @@ -38,4 +38,14 @@ export type RecommendationsQuery = {
* Additional filters to use as fallback when there aren’t enough recommendations.
*/
readonly fallbackParameters?: RecommendSearchOptions;

/**
* Used for trending model
*/
readonly facetName?: string;

/**
* Used for trending model
*/
readonly facetValue?: string;
};
6 changes: 5 additions & 1 deletion packages/recommend/src/types/RelatedProductsQuery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { RecommendationsQuery } from './RecommendationsQuery';

export type RelatedProductsQuery = Omit<RecommendationsQuery, 'model'>;
export type RelatedProductsQuery = Omit<
RecommendationsQuery,
'model' | 'facetName' | 'facetValue'
> &
Required<Pick<RecommendationsQuery, 'objectID'>>;
4 changes: 4 additions & 0 deletions packages/recommend/src/types/TrendingFacetsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { RecommendationsQuery } from './RecommendationsQuery';

export type TrendingFacetsQuery = Omit<RecommendationsQuery, 'model' | 'facetValue' | 'objectID'> &
Required<Pick<RecommendationsQuery, 'facetName'>>;
3 changes: 3 additions & 0 deletions packages/recommend/src/types/TrendingItemsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { RecommendationsQuery } from './RecommendationsQuery';

export type TrendingItemsQuery = Omit<RecommendationsQuery, 'model' | 'objectID'>;
18 changes: 18 additions & 0 deletions packages/recommend/src/types/WithRecommendMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { RequestOptions } from '@algolia/transporter';
import { FrequentlyBoughtTogetherQuery } from './FrequentlyBoughtTogetherQuery';
import { RecommendationsQuery } from './RecommendationsQuery';
import { RelatedProductsQuery } from './RelatedProductsQuery';
import { TrendingFacetsQuery } from './TrendingFacetsQuery';
import { TrendingItemsQuery } from './TrendingItemsQuery';

export type WithRecommendMethods<TType> = TType & {
/**
Expand All @@ -29,4 +31,20 @@ export type WithRecommendMethods<TType> = TType & {
queries: readonly FrequentlyBoughtTogetherQuery[],
requestOptions?: RequestOptions & SearchOptions
) => Readonly<Promise<MultipleQueriesResponse<TObject>>>;

/**
* Returns trending items
*/
readonly getTrendingItems: <TObject>(
queries: readonly TrendingItemsQuery[],
requestOptions?: RequestOptions & SearchOptions
) => Readonly<Promise<MultipleQueriesResponse<TObject>>>;

/**
* Returns trending items per facet
*/
readonly getTrendingFacets: <TObject>(
queries: readonly TrendingFacetsQuery[],
requestOptions?: RequestOptions & SearchOptions
) => Readonly<Promise<MultipleQueriesResponse<TObject>>>;
};
2 changes: 2 additions & 0 deletions packages/recommend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export * from './RecommendOptions';
export * from './RecommendSearchOptions';
export * from './RecommendationsQuery';
export * from './RelatedProductsQuery';
export * from './TrendingFacetsQuery';
export * from './TrendingItemsQuery';
export * from './WithRecommendMethods';