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

Recommend Request Dedupe and Merge #1170

Merged
merged 3 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 9 additions & 3 deletions docs/INTEGRATION_RECOMMENDATIONS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
## Recommendations Integration
It is recommended to utilize the [`RecommendationInstantiator`](https://github.com/searchspring/snap/blob/main/packages/snap-preact/src/Instantiators/README.md) for integration of product recommendations (standard when using Snap object).

Changes to the recommendation integration scripts were made in Snap `v0.60.0`. Legacy Recommmendation Integrations docs can still be found [`here`](https://github.com/searchspring/snap/blob/main/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md)

It is recommended to utilize the [`RecommendationInstantiator`](https://github.com/searchspring/snap/blob/main/packages/snap-preact/src/Instantiators/README.md) for integration of product recommendations. This method allows recommendations to be placed anywhere on the page with a single script block (requires the `bundle.js` script also).
Recommendations script blocks can be placed anywhere on the page and will automatically target and batch requests for all profiles specified in the block (requires the `bundle.js` script also). Batching profiles is important for deduplication of recommended products (see more below).

The block below uses the `recently-viewed` profile which would typically be setup to display the last products viewed by the shopper. Profiles must be setup in the Searchspring Management Console (SMC) and have associated Snap templates selected.

```html
<script type="searchspring/recommendations">
Expand All @@ -21,13 +24,15 @@ It is recommended to utilize the [`RecommendationInstantiator`](https://github.c
}
];
</script>

<div class="ss__recs__recently-viewed"><!-- recommendations will render here --></div>
```

The `RecommendationInstantiator` will look for these elements on the page and attempt to inject components based on the `profiles` specified. In the example above, the profile specified is the `recently-viewed` profile, and is set to render inside the selector `.ss__recs__recently-viewed`, this profile would typically be setup to display the last products viewed by the shopper. These profiles must be setup in the Searchspring Management Console (SMC) and have associated Snap templates selected.
The `RecommendationInstantiator` will look for these script blocks on the page and attempt to inject components based on the `selector` specified in each profile. In the example above, the profile specified is the `recently-viewed` profile, and is set to render inside the `.ss__recs__recently-viewed` element just below the script block. The targeted element could exist anywhere on the page - but it is recommended to group elements with script blocks whenever possible (for easy integration identification). The component to render into the targeted `selector` is setup within the `RecommendationInstantiator` configuration.


## Recommendation Context Variables
Context variables are applied to individual recommendation profiles similar to how they are done on the integration script tag. Variables here may be required depending on the profile placement, and can be used to alter the results displayed by our recommendations.
Context variables are set within the script blocks and can be used to set either global or per profile (profile specific) functionality. Variables are used to alter the results displayed by our recommendations and may be required depending on the profile placements in use.

### Globals Variables
| Option | Value | Placement | Description | Required
Expand All @@ -47,6 +52,7 @@ Context variables are applied to individual recommendation profiles similar to h
| options.siteId | global siteId overwrite | all | optional global siteId overwrite | |
| options.categories | array of category path strings | all | optional category identifiers used in category trending recommendation profiles | |
| options.brands | array of brand strings | all | optional brand identifiers used in brand trending recommendation profiles | |
| options.blockedItems | array of strings | all | SKU values to identify which products to exclude from the profile response | |
| options.branch | template branch overwrite | all | optional branch overwrite for recommendations template (advanced usage) | |
| options.dedupe | boolean (default: `true`) | all | dedupe products across all profiles in the batch | |
| options.query | string | dynamic custom | query to search | |
Expand Down
199 changes: 199 additions & 0 deletions packages/snap-client/src/Client/apis/Recommend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,113 @@ describe('Recommend Api', () => {
requestMock.mockReset();
});

it('batchRecommendations deduplicates certain global request parameters', async () => {
const api = new RecommendAPI(new ApiConfiguration(apiConfig));

//now consts try a post
const POSTParams = {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},

body: JSON.stringify({
profiles: [
{ tag: 'profile1' },
{ tag: 'profile2', filters: [{ field: 'size', type: '=', values: ['small'] }] },
{ tag: 'profile3', blockedItems: ['sku3p'] },
],
siteId: '8uyt2m',
products: ['product1', 'product2', 'product3', 'product4'],
blockedItems: ['sku1', 'sku3', 'sku4', 'sku2'],
filters: [
{ field: 'color', type: '=', values: ['blue'] },
{ field: 'price', type: '>=', values: [0] },
{ field: 'price', type: '<=', values: [20] },
{ field: 'price', type: '<=', values: [40] },
{ field: 'color', type: '=', values: ['green'] },
],
}),
};

const POSTRequestUrl = 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend';

const POSTRequestMock = jest
.spyOn(global.window, 'fetch')
.mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve({}) } as Response));

// profile 1
api.batchRecommendations({
tag: 'profile1',
siteId: '8uyt2m',
products: ['product1'],
blockedItems: ['sku1'],
filters: [
{
type: 'value',
field: 'color',
value: 'blue',
},
{
type: 'range',
field: 'price',
value: { low: 0, high: 20 },
},
],
batched: true,
});

// profile 2
api.batchRecommendations({
tag: 'profile2',
profile: {
filters: [
{
type: 'value',
field: 'size',
value: 'small',
},
],
},
siteId: '8uyt2m',
products: ['product2', 'product3'],
blockedItems: ['sku3', 'sku4'],
filters: [
{
type: 'range',
field: 'price',
value: { low: 0, high: 40 },
},
],
batched: true,
});

// profile 3
api.batchRecommendations({
tag: 'profile3',
profile: {
blockedItems: ['sku3p'],
},
siteId: '8uyt2m',
product: 'product4',
blockedItems: ['sku2', 'sku3'],
filters: [
{
type: 'value',
field: 'color',
value: 'green',
},
],
batched: true,
});

//add delay for paramBatch.timeout
await wait(250);

expect(POSTRequestMock).toHaveBeenCalledWith(POSTRequestUrl, POSTParams);
POSTRequestMock.mockReset();
});

it('batchRecommendations handles POST requests', async () => {
const api = new RecommendAPI(new ApiConfiguration(apiConfig));

Expand All @@ -643,6 +750,7 @@ describe('Recommend Api', () => {
}),
siteId: '8uyt2m',
products: ['marnie-runner-2-7x10'],
blockedItems: ['sku1', 'sku2', 'sku3'],
filters: [
{ field: 'color', type: '=', values: ['blue'] },
{ field: 'price', type: '>=', values: [0] },
Expand Down Expand Up @@ -678,6 +786,97 @@ describe('Recommend Api', () => {
api.batchRecommendations({
tag: i.toString(),
...batchParams,
blockedItems: ['sku1', 'sku2', 'sku3'],
filters: [
{
type: 'value',
field: 'color',
value: 'blue',
},
{
type: 'range',
field: 'price',
value: { low: 0, high: 20 },
},
],
batched: true,
});
}

//add delay for paramBatch.timeout
await wait(250);

expect(POSTRequestMock).toHaveBeenCalledWith(POSTRequestUrl, POSTParams);
POSTRequestMock.mockReset();
});

it('batchRecommendations handles POST requests with profile specific request parameters', async () => {
const api = new RecommendAPI(new ApiConfiguration(apiConfig));

//now consts try a post
const POSTParams = {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},

body: JSON.stringify({
profiles: Array.from({ length: 100 }, (item, index) => {
return {
tag: index.toString(),
blockedItems: ['skuBlocked', `sku${index}`],
filters: [{ field: 'color', type: '=', values: ['red'] }],
};
}),
siteId: '8uyt2m',
products: ['marnie-runner-2-7x10'],
blockedItems: ['sku1', 'sku2', 'sku3'],
filters: [
{ field: 'color', type: '=', values: ['blue'] },
{ field: 'price', type: '>=', values: [0] },
{ field: 'price', type: '<=', values: [20] },
],
lastViewed: [
'marnie-runner-2-7x10',
'ruby-runner-2-7x10',
'abbie-runner-2-7x10',
'riley-4x6',
'joely-5x8',
'helena-4x6',
'kwame-4x6',
'sadie-4x6',
'candice-runner-2-7x10',
'esmeray-4x6',
'camilla-230x160',
'candice-4x6',
'sahara-4x6',
'dayna-4x6',
'moema-4x6',
],
}),
};

const POSTRequestUrl = 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend';

const POSTRequestMock = jest
.spyOn(global.window, 'fetch')
.mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve({}) } as Response));

for (let i = 0; i < 100; i++) {
api.batchRecommendations({
tag: i.toString(),
profile: {
blockedItems: ['skuBlocked', `sku${i}`],
filters: [
{
type: 'value',
field: 'color',
value: 'red',
},
],
},
...batchParams,
blockedItems: ['sku1', 'sku2', 'sku3'],
filters: [
{
type: 'value',
Expand Down
14 changes: 11 additions & 3 deletions packages/snap-client/src/Client/apis/Recommend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,21 @@ export class RecommendAPI extends API {

// parameters used globally
const { products, blockedItems, filters, test, cart, lastViewed, shopper } = entry.request;

// merge and de-dupe global array fields
const dedupedProducts = Array.from(new Set((batch.request.products || []).concat(products || [])));
const dedupedBlockedItems = Array.from(new Set((batch.request.blockedItems || []).concat(blockedItems || [])));
const dedupedFilters = Array.from(
new Set((batch.request.filters || []).concat(transformRecommendationFiltersPost(filters) || []).map((filter) => JSON.stringify(filter)))
).map((stringyFilter) => JSON.parse(stringyFilter));

batch.request = {
...batch.request,
...defined({
siteId: entry.request.profile?.siteId || entry.request.siteId,
products,
blockedItems,
filters: transformRecommendationFiltersPost(filters),
products: dedupedProducts.length ? dedupedProducts : undefined,
blockedItems: dedupedBlockedItems.length ? dedupedBlockedItems : undefined,
filters: dedupedFilters.length ? dedupedFilters : undefined,
test,
cart,
lastViewed,
Expand Down
Loading