@reactionary/source 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/src/client/client-builder.ts +8 -0
- package/core/src/client/client.ts +4 -0
- package/core/src/providers/index.ts +2 -0
- package/core/src/providers/product-associations.provider.ts +49 -0
- package/core/src/providers/product-recommendations.provider.ts +150 -0
- package/core/src/schemas/capabilities.schema.ts +2 -0
- package/core/src/schemas/models/identifiers.model.ts +8 -0
- package/core/src/schemas/models/index.ts +1 -0
- package/core/src/schemas/models/product-recommendations.model.ts +10 -0
- package/core/src/schemas/queries/index.ts +2 -0
- package/core/src/schemas/queries/product-associations.query.ts +23 -0
- package/core/src/schemas/queries/product-recommendations.query.ts +72 -0
- package/documentation/docs/7-marketing.md +95 -0
- package/examples/node/package.json +6 -6
- package/examples/node/src/capabilities/product-recommendations.spec.ts +140 -0
- package/examples/node/src/utils.ts +3 -0
- package/package.json +2 -2
- package/providers/algolia/package.json +1 -1
- package/providers/algolia/src/core/initialize.ts +6 -0
- package/providers/algolia/src/index.ts +1 -0
- package/providers/algolia/src/providers/index.ts +2 -1
- package/providers/algolia/src/providers/product-recommendations.provider.ts +236 -0
- package/providers/algolia/src/schema/capabilities.schema.ts +2 -1
- package/providers/algolia/src/schema/product-recommendation.schema.ts +9 -0
- package/providers/medusa/src/core/initialize.ts +6 -0
- package/providers/medusa/src/index.ts +1 -0
- package/providers/medusa/src/providers/product-recommendations.provider.ts +117 -0
- package/providers/medusa/src/schema/capabilities.schema.ts +1 -0
- package/providers/meilisearch/src/core/initialize.ts +6 -0
- package/providers/meilisearch/src/index.ts +1 -0
- package/providers/meilisearch/src/providers/index.ts +1 -0
- package/providers/meilisearch/src/providers/product-recommendations.provider.ts +89 -0
- package/providers/meilisearch/src/schema/capabilities.schema.ts +1 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RequestContextSchema,
|
|
7
7
|
type RequestContext,
|
|
8
8
|
} from '../schemas/session.schema.js';
|
|
9
|
+
import { MulticastProductRecommendationsProvider, type ProductRecommendationsProvider } from '../providers/product-recommendations.provider.js';
|
|
9
10
|
|
|
10
11
|
type CapabilityFactory<T> = (cache: Cache, context: RequestContext) => T;
|
|
11
12
|
|
|
@@ -52,6 +53,8 @@ export class ClientBuilder<TClient = Client> {
|
|
|
52
53
|
|
|
53
54
|
const mergedAnalytics: AnalyticsProvider[] = [];
|
|
54
55
|
|
|
56
|
+
const mergedProductRecommendationsProviders: ProductRecommendationsProvider[] = [];
|
|
57
|
+
|
|
55
58
|
for (const factory of this.factories) {
|
|
56
59
|
const provider = factory(sharedCache, this.context);
|
|
57
60
|
client = {
|
|
@@ -62,12 +65,17 @@ export class ClientBuilder<TClient = Client> {
|
|
|
62
65
|
if (provider.analytics) {
|
|
63
66
|
mergedAnalytics.push(provider.analytics);
|
|
64
67
|
}
|
|
68
|
+
|
|
69
|
+
if (provider.productRecommendations) {
|
|
70
|
+
mergedProductRecommendationsProviders.push(provider.productRecommendations);
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
// Add cache to complete the client
|
|
68
75
|
const completeClient = {
|
|
69
76
|
...client,
|
|
70
77
|
analytics: new MulticastAnalyticsProvider(sharedCache, this.context, mergedAnalytics),
|
|
78
|
+
productRecommendations: new MulticastProductRecommendationsProvider(sharedCache, this.context, mergedProductRecommendationsProviders),
|
|
71
79
|
cache: sharedCache,
|
|
72
80
|
} as TClient & { cache: Cache };
|
|
73
81
|
|
|
@@ -8,10 +8,14 @@ import type { Cache } from "../cache/cache.interface.js";
|
|
|
8
8
|
import type { CategoryProvider } from "../providers/category.provider.js";
|
|
9
9
|
import type { AnalyticsProvider, CheckoutProvider, OrderProvider, ProfileProvider, StoreProvider } from "../providers/index.js";
|
|
10
10
|
import type { OrderSearchProvider } from "../providers/order-search.provider.js";
|
|
11
|
+
import type { ProductRecommendationsProvider } from "../providers/product-recommendations.provider.js";
|
|
12
|
+
import type { ProductAssociationsProvider } from "../providers/product-associations.provider.js";
|
|
11
13
|
|
|
12
14
|
export interface Client {
|
|
13
15
|
product: ProductProvider,
|
|
14
16
|
productSearch: ProductSearchProvider,
|
|
17
|
+
productRecommendations: ProductRecommendationsProvider,
|
|
18
|
+
productAssociations: ProductAssociationsProvider,
|
|
15
19
|
identity: IdentityProvider,
|
|
16
20
|
cache: Cache,
|
|
17
21
|
cart: CartProvider,
|
|
@@ -9,6 +9,8 @@ export * from './price.provider.js';
|
|
|
9
9
|
export * from './product.provider.js';
|
|
10
10
|
export * from './profile.provider.js';
|
|
11
11
|
export * from './product-search.provider.js';
|
|
12
|
+
export * from './product-recommendations.provider.js';
|
|
13
|
+
export * from './product-associations.provider.js';
|
|
12
14
|
export * from './store.provider.js';
|
|
13
15
|
export * from './order.provider.js'
|
|
14
16
|
export * from './order-search.provider.js'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ProductIdentifier, ProductVariantIdentifier } from "../schemas/index.js";
|
|
2
|
+
import type { ProductAssociationsGetAccessoriesQuery, ProductAssociationsGetSparepartsQuery, ProductAssociationsGetReplacementsQuery } from "../schemas/queries/product-associations.query.js";
|
|
3
|
+
import { BaseProvider } from "./base.provider.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The product association provider is responsible for providing evidence based associations between products, such as
|
|
8
|
+
* accessories, spareparts, and replacements. These associations are typically used to provide recommendations to customers on the product detail page, but can also be used in other contexts such as the cart or post-purchase, but
|
|
9
|
+
* do not carry any personalization concept to them.
|
|
10
|
+
*/
|
|
11
|
+
export abstract class ProductAssociationsProvider extends BaseProvider {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns a list of product identifiers which are accessories to the given product.
|
|
15
|
+
* Accessories in are products in their own right, but are commonly purchased alongside or recommended as complementary to the main product. Examples of accessories include:
|
|
16
|
+
* - A phone case for a smartphone
|
|
17
|
+
* - A camera bag for a camera
|
|
18
|
+
*
|
|
19
|
+
*
|
|
20
|
+
* Usecase:
|
|
21
|
+
* - PDP: Accessories for this product
|
|
22
|
+
*/
|
|
23
|
+
public abstract getAccessories(query: ProductAssociationsGetAccessoriesQuery): Promise<ProductVariantIdentifier[]>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns a list of product identifiers which are spareparts to the given product.
|
|
27
|
+
* Spareparts are products which are necessary for the use of the main product, but are not typically purchased alongside it. Examples of spareparts include:
|
|
28
|
+
*
|
|
29
|
+
* Usecase:
|
|
30
|
+
* - PDP: Accessories for this product
|
|
31
|
+
*/
|
|
32
|
+
public abstract getSpareparts(query: ProductAssociationsGetSparepartsQuery): Promise<ProductVariantIdentifier[]>;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This product is replaced by these equivalent or newer products
|
|
37
|
+
* @param query
|
|
38
|
+
*/
|
|
39
|
+
public abstract getReplacements(query: ProductAssociationsGetReplacementsQuery): Promise<ProductVariantIdentifier[]>;
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
getResourceName(): string {
|
|
43
|
+
return 'product-associations';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Cache } from '../cache/cache.interface.js';
|
|
2
|
+
import { Reactionary } from "../decorators/reactionary.decorator.js";
|
|
3
|
+
import { success, type ProductRecommendation, type ProductRecommendationsByCollectionQuery, type RequestContext, type Result } from "../schemas/index.js";
|
|
4
|
+
import { ProductRecommendationsQuerySchema, type ProductRecommendationAlgorithmAlsoViewedProductsQuery, type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery, type ProductRecommendationAlgorithmPopuplarProductsQuery, type ProductRecommendationAlgorithmRelatedProductsQuery, type ProductRecommendationAlgorithmSimilarProductsQuery, type ProductRecommendationAlgorithmTopPicksProductsQuery, type ProductRecommendationAlgorithmTrendingInCategoryQuery, type ProductRecommendationsQuery } from "../schemas/queries/product-recommendations.query.js";
|
|
5
|
+
import { BaseProvider } from "./base.provider.js";
|
|
6
|
+
|
|
7
|
+
export abstract class ProductRecommendationsProvider extends BaseProvider {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* returns a list of recommended products, based on the selected algorithm and the provided query parameters. The recommendations should be relevant to the product specified in the query, and can be personalized based on the customer segments or contexts provided. The provider should return a list of product variant identifiers that are recommended for the given product, which can then be used to fetch the full product details from the product provider if needed.
|
|
11
|
+
* *
|
|
12
|
+
* Usecase:
|
|
13
|
+
* - PDP - "Customers who viewed this product also viewed"
|
|
14
|
+
* - Cart - "You might also like"
|
|
15
|
+
* - Post-purchase - "Customers who bought this product also bought"
|
|
16
|
+
* - Article page: "Products related to the product mentioned in this article"
|
|
17
|
+
* @param query
|
|
18
|
+
*/
|
|
19
|
+
public async getRecommendations(query: ProductRecommendationsQuery): Promise<Result<ProductRecommendation[]>> {
|
|
20
|
+
if (query.algorithm === 'frequentlyBoughtTogether') {
|
|
21
|
+
return success(await this.getFrequentlyBoughtTogetherRecommendations(query));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (query.algorithm === 'similar') {
|
|
25
|
+
return success(await this.getSimilarProductsRecommendations(query));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (query.algorithm === 'related') {
|
|
29
|
+
return success(await this.getRelatedProductsRecommendations(query));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (query.algorithm === 'trendingInCategory') {
|
|
33
|
+
return success(await this.getTrendingInCategoryRecommendations(query));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (query.algorithm === 'popular') {
|
|
37
|
+
return success(await this.getPopularProductsRecommendations(query));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (query.algorithm === 'topPicks') {
|
|
41
|
+
return success(await this.getTopPicksProductsRecommendations(query));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (query.algorithm === 'alsoViewed') {
|
|
45
|
+
return success(await this.getAlsoViewedProductsRecommendations(query));
|
|
46
|
+
}
|
|
47
|
+
return success([]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async getCollection(query: ProductRecommendationsByCollectionQuery): Promise<Result<ProductRecommendation[]>> {
|
|
51
|
+
return success([]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected async getFrequentlyBoughtTogetherRecommendations(query: ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery): Promise<ProductRecommendation[]> {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected async getSimilarProductsRecommendations(query: ProductRecommendationAlgorithmSimilarProductsQuery): Promise<ProductRecommendation[]> {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected async getTrendingInCategoryRecommendations(query: ProductRecommendationAlgorithmTrendingInCategoryQuery): Promise<ProductRecommendation[]> {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected async getRelatedProductsRecommendations(query: ProductRecommendationAlgorithmRelatedProductsQuery): Promise<ProductRecommendation[]> {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected async getPopularProductsRecommendations(query: ProductRecommendationAlgorithmPopuplarProductsQuery): Promise<ProductRecommendation[]> {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected async getTopPicksProductsRecommendations(query: ProductRecommendationAlgorithmTopPicksProductsQuery): Promise<ProductRecommendation[]> {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected async getAlsoViewedProductsRecommendations(query: ProductRecommendationAlgorithmAlsoViewedProductsQuery): Promise<ProductRecommendation[]> {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
protected override getResourceName(): string {
|
|
84
|
+
return 'product-recommendations';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
export class MulticastProductRecommendationsProvider extends ProductRecommendationsProvider {
|
|
93
|
+
protected providers: Array<ProductRecommendationsProvider>;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
cache: Cache,
|
|
97
|
+
requestContext: RequestContext,
|
|
98
|
+
providers: Array<ProductRecommendationsProvider>
|
|
99
|
+
) {
|
|
100
|
+
super(cache, requestContext);
|
|
101
|
+
|
|
102
|
+
this.providers = providers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Reactionary({
|
|
106
|
+
inputSchema: ProductRecommendationsQuerySchema,
|
|
107
|
+
})
|
|
108
|
+
public override async getRecommendations(query: ProductRecommendationsQuery): Promise<Result<ProductRecommendation[]>> {
|
|
109
|
+
const output = [];
|
|
110
|
+
for (const provider of this.providers) {
|
|
111
|
+
const providerOutput = await provider.getRecommendations(query);
|
|
112
|
+
if (providerOutput.success) {
|
|
113
|
+
|
|
114
|
+
output.push(...providerOutput.value);
|
|
115
|
+
} else {
|
|
116
|
+
// For other types of errors, we might want to log them or handle them differently
|
|
117
|
+
console.error(`Error from provider ${provider.constructor.name}:`, providerOutput.error);
|
|
118
|
+
return providerOutput
|
|
119
|
+
}
|
|
120
|
+
if (output.length >= query.numberOfRecommendations) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return success(output.slice(0, query.numberOfRecommendations));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public override async getCollection(query: ProductRecommendationsByCollectionQuery): Promise<Result<ProductRecommendation[]>> {
|
|
128
|
+
const output = [];
|
|
129
|
+
for (const provider of this.providers) {
|
|
130
|
+
const providerOutput = await provider.getCollection(query);
|
|
131
|
+
if (providerOutput.success) {
|
|
132
|
+
output.push(...providerOutput.value);
|
|
133
|
+
} else {
|
|
134
|
+
if (providerOutput.error.type === 'NotFound') {
|
|
135
|
+
// If the error is a NotFound error, we can ignore it and continue to the next provider
|
|
136
|
+
continue;
|
|
137
|
+
} else {
|
|
138
|
+
// For other types of errors, we might want to log them or handle them differently
|
|
139
|
+
console.error(`Error from provider ${provider.constructor.name}:`, providerOutput.error);
|
|
140
|
+
return providerOutput;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (output.length >= query.numberOfRecommendations) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return success(output.slice(0, query.numberOfRecommendations));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
}
|
|
@@ -5,6 +5,8 @@ import type { Client } from '../client/client.js';
|
|
|
5
5
|
export const CapabilitiesSchema = z.looseObject({
|
|
6
6
|
product: z.boolean(),
|
|
7
7
|
productSearch: z.boolean(),
|
|
8
|
+
productAssociations: z.boolean(),
|
|
9
|
+
productRecommendations: z.boolean(),
|
|
8
10
|
analytics: z.boolean(),
|
|
9
11
|
identity: z.boolean(),
|
|
10
12
|
cart: z.boolean(),
|
|
@@ -118,6 +118,11 @@ export const PickupPointIdentifierSchema = z.looseObject({
|
|
|
118
118
|
key: z.string(),
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
export const ProductRecommendationIdentifierSchema = z.looseObject({
|
|
122
|
+
key: z.string(),
|
|
123
|
+
algorithm: z.string(),
|
|
124
|
+
});
|
|
125
|
+
|
|
121
126
|
|
|
122
127
|
export const ProductSearchIdentifierSchema = z.looseObject({
|
|
123
128
|
term: z.string().describe('The search term used to find products.'),
|
|
@@ -141,6 +146,7 @@ export const OrderSearchIdentifierSchema = z.looseObject({
|
|
|
141
146
|
paginationOptions: PaginationOptionsSchema.describe('Pagination options for the search results.'),
|
|
142
147
|
});
|
|
143
148
|
|
|
149
|
+
|
|
144
150
|
export type OrderSearchIdentifier = InferType<typeof OrderSearchIdentifierSchema>;
|
|
145
151
|
export type ProductIdentifier = InferType<typeof ProductIdentifierSchema>;
|
|
146
152
|
export type ProductVariantIdentifier = InferType<typeof ProductVariantIdentifierSchema>;
|
|
@@ -178,6 +184,7 @@ export type ProductOptionIdentifier = InferType<typeof ProductOptionIdentifierSc
|
|
|
178
184
|
export type ProductOptionValueIdentifier = InferType<typeof ProductOptionValueIdentifierSchema>;
|
|
179
185
|
export type ProductAttributeIdentifier = InferType<typeof ProductAttributeIdentifierSchema>;
|
|
180
186
|
export type ProductAttributeValueIdentifier = InferType<typeof ProductAttributeValueIdentifierSchema>;
|
|
187
|
+
export type ProductRecommendationIdentifier = InferType<typeof ProductRecommendationIdentifierSchema>;
|
|
181
188
|
|
|
182
189
|
export type IdentifierType =
|
|
183
190
|
| ProductIdentifier
|
|
@@ -191,6 +198,7 @@ export type IdentifierType =
|
|
|
191
198
|
| CategoryIdentifier
|
|
192
199
|
| WebStoreIdentifier
|
|
193
200
|
| InventoryIdentifier
|
|
201
|
+
| ProductRecommendationIdentifier
|
|
194
202
|
| FulfillmentCenterIdentifier
|
|
195
203
|
| IdentityIdentifier
|
|
196
204
|
| ShippingMethodIdentifier
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { ProductIdentifierSchema, ProductRecommendationIdentifierSchema, ProductVariantIdentifierSchema } from "./identifiers.model.js";
|
|
3
|
+
|
|
4
|
+
export const ProductRecommendationSchema = z.looseObject({
|
|
5
|
+
recommendationIdentifier: ProductRecommendationIdentifierSchema.describe('The identifier for the product recommendation, which includes a key and an algorithm and any other vendor specific/instance specific data '),
|
|
6
|
+
product: ProductIdentifierSchema.describe('The identifier for the recommended product.'),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export type ProductRecommendation = z.infer<typeof ProductRecommendationSchema>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { BaseQuerySchema } from "./base.query.js";
|
|
3
|
+
import { ProductIdentifierSchema, ProductVariantIdentifierSchema } from "../models/identifiers.model.js";
|
|
4
|
+
import { CartItemSchema } from "../models/cart.model.js";
|
|
5
|
+
|
|
6
|
+
export const ProductAssociationsGetAccessoriesQuerySchema = BaseQuerySchema.extend({
|
|
7
|
+
forProductVariant: ProductVariantIdentifierSchema.describe('The product variant identifier for which to get accessory recommendations. The provider should return recommendations that are relevant to this product, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
|
|
8
|
+
numberOfAccessories: z.number().min(1).max(12).describe('The number of accessory recommendations requested. The provider may return fewer than this number, but should not return more.'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const ProductAssociationsGetSparepartsQuerySchema = BaseQuerySchema.extend({
|
|
12
|
+
forProductVariant: ProductVariantIdentifierSchema.describe('The product variant identifier for which to get similar item recommendations. The provider should return recommendations that are relevant to this product, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const ProductAssociationsGetReplacementsQuerySchema = BaseQuerySchema.extend({
|
|
16
|
+
forProductVariant: ProductVariantIdentifierSchema.describe('The product variant identifier for which to get replacement recommendations. The provider should return recommendations that are relevant to this product, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export type ProductAssociationsGetAccessoriesQuery = z.infer<typeof ProductAssociationsGetAccessoriesQuerySchema>;
|
|
22
|
+
export type ProductAssociationsGetSparepartsQuery = z.infer<typeof ProductAssociationsGetSparepartsQuerySchema>;
|
|
23
|
+
export type ProductAssociationsGetReplacementsQuery = z.infer<typeof ProductAssociationsGetReplacementsQuerySchema>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type { InferType } from "../../zod-utils.js";
|
|
3
|
+
import { CategoryIdentifierSchema, ProductIdentifierSchema } from "../models/identifiers.model.js";
|
|
4
|
+
import { BaseQuerySchema } from "./base.query.js";
|
|
5
|
+
|
|
6
|
+
export const ProductRecommendationBaseQuerySchema = BaseQuerySchema.extend({
|
|
7
|
+
numberOfRecommendations: z.number().min(1).max(12).describe('The number of recommendations requested. The provider may return fewer than this number, but should not return more.'),
|
|
8
|
+
labels: z.array(z.string()).optional().describe('The customer segments, quirks, chirps or other labels to which the recommendations can optimize themselves to be relevant. This can be used by the provider to personalize the recommendations based on the preferences and behaviors of users in these segments.'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const ProductRecommendationsByCollectionQuerySchema = ProductRecommendationBaseQuerySchema.extend({
|
|
12
|
+
collectionName: z.string().describe('The name of the collection for which to get product recommendations. This is to access either manually curated lists, or interface marketing rules engines that define zones by name'),
|
|
13
|
+
sourceProduct: z.array(ProductIdentifierSchema).optional().describe('The products on screen or in the context you are asking for the recommendations. Could be all the variants from the current cart (resolved into their products), or just the variant of the PDP, or the 4 first products of a category.'),
|
|
14
|
+
sourceCategory: CategoryIdentifierSchema.optional().describe('The category identifier to use as a seed for the recommendations. The provider should return recommendations that are relevant to this category, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences. This is optional, as the collection may already be curated to be relevant to a specific product or category, but it can be used by the provider to further personalize the recommendations based on the preferences and behaviors of users who have interacted with this category.'),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const ProductRecommendationProductBasedBaseQuerySchema = ProductRecommendationBaseQuerySchema.extend({
|
|
18
|
+
sourceProduct: ProductIdentifierSchema.describe('The product identifiers for which to get recommendations. The provider should return recommendations that are relevant to these products, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
|
|
22
|
+
algorithm: z.literal('frequentlyBoughtTogether').describe('The provider should return recommendations based on products that are frequently bought together with the source products. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const ProductRecommendationAlgorithmSimilarProductsQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
|
|
26
|
+
algorithm: z.literal('similar').describe('The provider should return recommendations based on products that are similar to the source products either visually or data wise'),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const ProductRecommendationAlgorithmRelatedProductsQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
|
|
30
|
+
algorithm: z.literal('related').describe('The provider should return recommendations based on products that are related to the source products. '),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const ProductRecommendationAlgorithmTrendingInCategoryQuerySchema = ProductRecommendationBaseQuerySchema.extend({
|
|
34
|
+
algorithm: z.literal('trendingInCategory').describe('The provider should return recommendations based on products that are trending in the specified category. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.'),
|
|
35
|
+
sourceCategory: CategoryIdentifierSchema.describe('The category identifier for which to get trending product recommendations. The provider should return recommendations that are relevant to this category, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// unsure if we need both Popular and TopPicks, as they are quite similar. Maybe we can merge them into one algorithm with a parameter to specify the type of popularity? For now, I'll keep them separate for clarity.
|
|
40
|
+
export const ProductRecommendationAlgorithmPopuplarProductsQuerySchema = ProductRecommendationBaseQuerySchema.extend({
|
|
41
|
+
algorithm: z.literal('popular').describe('The provider should return recommendations based on products that are popular among users. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const ProductRecommendationAlgorithmTopPicksProductsQuerySchema = ProductRecommendationBaseQuerySchema.extend({
|
|
45
|
+
algorithm: z.literal('topPicks').describe('The provider should return recommendations based on products that are top picks among users. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.') ,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const ProductRecommendationAlgorithmAlsoViewedProductsQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
|
|
49
|
+
algorithm: z.literal('alsoViewed').describe('The provider should return recommendations based on products that are also viewed by users. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.') ,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export const ProductRecommendationsQuerySchema = z.discriminatedUnion('algorithm', [
|
|
55
|
+
ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuerySchema,
|
|
56
|
+
ProductRecommendationAlgorithmTrendingInCategoryQuerySchema,
|
|
57
|
+
ProductRecommendationAlgorithmSimilarProductsQuerySchema,
|
|
58
|
+
ProductRecommendationAlgorithmRelatedProductsQuerySchema,
|
|
59
|
+
ProductRecommendationAlgorithmPopuplarProductsQuerySchema,
|
|
60
|
+
ProductRecommendationAlgorithmTopPicksProductsQuerySchema,
|
|
61
|
+
ProductRecommendationAlgorithmAlsoViewedProductsQuerySchema
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export type ProductRecommendationsQuery = InferType<typeof ProductRecommendationsQuerySchema>;
|
|
65
|
+
export type ProductRecommendationsByCollectionQuery = InferType<typeof ProductRecommendationsByCollectionQuerySchema>;
|
|
66
|
+
export type ProductRecommendationAlgorithmTopPicksProductsQuery = InferType<typeof ProductRecommendationAlgorithmTopPicksProductsQuerySchema>;
|
|
67
|
+
export type ProductRecommendationAlgorithmPopuplarProductsQuery = InferType<typeof ProductRecommendationAlgorithmPopuplarProductsQuerySchema>;
|
|
68
|
+
export type ProductRecommendationAlgorithmTrendingInCategoryQuery = InferType<typeof ProductRecommendationAlgorithmTrendingInCategoryQuerySchema>;
|
|
69
|
+
export type ProductRecommendationAlgorithmRelatedProductsQuery = InferType<typeof ProductRecommendationAlgorithmRelatedProductsQuerySchema>;
|
|
70
|
+
export type ProductRecommendationAlgorithmSimilarProductsQuery= InferType<typeof ProductRecommendationAlgorithmSimilarProductsQuerySchema>;
|
|
71
|
+
export type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery = InferType<typeof ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuerySchema>;
|
|
72
|
+
export type ProductRecommendationAlgorithmAlsoViewedProductsQuery = InferType<typeof ProductRecommendationAlgorithmAlsoViewedProductsQuerySchema>;
|
|
@@ -1,3 +1,98 @@
|
|
|
1
1
|
# Adding the personal touch
|
|
2
2
|
|
|
3
|
+
Reactionary supports many mechanisms by which to make the customer journey unique to a customer. The main mechanism is via the `ProductRecommendationsProvider` which offers 7 unique ways to find some relevant products to display for the user.
|
|
4
|
+
|
|
5
|
+
It is assumed, that any basic-direct-assign logic (ie editor has picked 4 products to show) is already handled at the CMS level. The Reactionary provider is for the usecase where you want a dynamic result back, based on the users behavior, tags, or whatever you have.
|
|
6
|
+
|
|
7
|
+
The supported algorithms are
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
- Frequently-Bought-Together (Product based)
|
|
11
|
+
- Trending-In-Category (Category based)
|
|
12
|
+
- Similar (Product)
|
|
13
|
+
- Related-Items (Product)
|
|
14
|
+
- Popular-Products (User)
|
|
15
|
+
- Top-Picks-For-You (User)
|
|
16
|
+
- Also-Viewed (Product, User)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Reactionary supports having multiple providers of product recommendations, each of which will be asked in order, until the result is fully populated.
|
|
20
|
+
Every provider might not support every algorithm though.
|
|
21
|
+
|
|
22
|
+
But, the point of this is, that you can either hardcode something on the PDP
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const recommendations = client.productRecommendations.getRecommendations({
|
|
26
|
+
algorithm: 'similar',
|
|
27
|
+
numberOfRecommendations: 8,
|
|
28
|
+
labels: [
|
|
29
|
+
(
|
|
30
|
+
isLoggedIn: 'Registered' : 'Guest',
|
|
31
|
+
registeredThisSession? 'FirstSession': 'ReturningCustomer',
|
|
32
|
+
customerSegments.map(x => x.name),
|
|
33
|
+
new Date().getHour() > 12? "Afternoon": "Morning",
|
|
34
|
+
getTemperatureAround(context.clientIp) < 20: 'Cold': 'Warm'
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (recommendations.success) {
|
|
40
|
+
// we can now resolve them
|
|
41
|
+
const allProducts = Promise.all(
|
|
42
|
+
recommendations.value.products.map(x => client.product.getByID(x)).map(x => x.successs? x.value : null )).filter(y => !!y);
|
|
43
|
+
// and draw them...
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Instead of using the full `client.product` you could have resolved it via the `.productSearch` to get a smaller data-footprint. However, since you are hopefully just about to navigate to one of these pages, it might be better for overall cache performance to use the full data model.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Collections
|
|
53
|
+
|
|
54
|
+
Some systems offer named access to a potentially rules based selection of products. An example is the HCL Commerce espot system, where your frontpage migth have a spot called "Newest arrivals", and behind the scenes some rules engine decides what to show there.
|
|
55
|
+
|
|
56
|
+
It can also be simply the name of a CMS entity, or other system specific functionality.
|
|
57
|
+
|
|
58
|
+
You can call that as well here:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const recommendations = client.productRecommendations.getCollection({
|
|
62
|
+
collectionName: 'newest-arrivals',
|
|
63
|
+
numberOfRecommendations: 8,
|
|
64
|
+
labels: [
|
|
65
|
+
(
|
|
66
|
+
isLoggedIn: 'Registered' : 'Guest',
|
|
67
|
+
registeredThisSession? 'FirstSession': 'ReturningCustomer',
|
|
68
|
+
customerSegments.map(x => x.name),
|
|
69
|
+
new Date().getHour() > 12? "Afternoon": "Morning",
|
|
70
|
+
getTemperatureAround(context.clientIp) < 20: 'Cold': 'Warm'
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (recommendations.success) {
|
|
76
|
+
// we can now resolve them
|
|
77
|
+
const allProducts = Promise.all(
|
|
78
|
+
recommendations.value.products.map(x => client.product.getByID(x)).map(x => x.successs? x.value : null )).filter(y => !!y);
|
|
79
|
+
// and draw them...
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Configuration
|
|
84
|
+
Since not every system can provide every recommendation algorithm, Reactionary supports enabling this capability on multiple providers.
|
|
85
|
+
|
|
86
|
+
So if you have both algolia and medusa, you can set `productRecommendations: true` on both, and the system will then ask each system in turn until the required number of results have been gathered.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Design decisions
|
|
91
|
+
Since not all systems can deliver full product data back, we are initially only returning the IDs of the products, so you have to use the `product` provider to resolve it into something displayable.
|
|
92
|
+
|
|
93
|
+
Later, we might add a discriminator, allowing the same bare-bones/tile-ready data to be returned from this, as is returned from the product search.
|
|
94
|
+
|
|
95
|
+
It was also decided to have the source and target unit be a product (Forest Ranger T-Shirt), and not the individual sku (Forest Ranger T-Shirt, Yellow, Size-32).
|
|
96
|
+
|
|
97
|
+
This is from emperical studies showign that you will get more results from the toplevel product, than from a rarely used SKU.
|
|
3
98
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/examples-node",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "src/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@reactionary/core": "0.3.
|
|
8
|
-
"@reactionary/provider-commercetools": "0.3.
|
|
9
|
-
"@reactionary/provider-algolia": "0.3.
|
|
10
|
-
"@reactionary/provider-medusa": "0.3.
|
|
11
|
-
"@reactionary/provider-meilisearch": "0.3.
|
|
7
|
+
"@reactionary/core": "0.3.4",
|
|
8
|
+
"@reactionary/provider-commercetools": "0.3.4",
|
|
9
|
+
"@reactionary/provider-algolia": "0.3.4",
|
|
10
|
+
"@reactionary/provider-medusa": "0.3.4",
|
|
11
|
+
"@reactionary/provider-meilisearch": "0.3.4"
|
|
12
12
|
},
|
|
13
13
|
"type": "module"
|
|
14
14
|
}
|