@reactionary/provider-algolia 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -38,11 +38,19 @@ You can have more, for use with facets, and additional searchable fields, but th
38
38
 
39
39
  The `objectID` corrosponds to your productIdentifier, and `variantID` should match your SKU
40
40
 
41
+ ## Analytics
41
42
 
42
- ## Building
43
+ The Algolia analytics provider maps the following tracked event types to data tracked in Algolia:
43
44
 
44
- Run `nx build provider-algolia` to build the library.
45
+ - AnalyticsMutationProductSummaryViewEvent => ViewedObjectIDs
46
+ - AnalyticsMutationProductSummaryClickEvent => ClickedObjectIDsAfterSearch / ClickedObjectIDs
47
+ - AnalyticsMutationProductAddToCartEvent => AddedToCartObjectIDsAfterSearch / AddedToCartObjectIDs
48
+ - AnalyticsMutationPurchaseEvent => PurchasedObjectIDs
45
49
 
46
- ## Running unit tests
50
+ The `AfterSearch` variants are (with the exception of purchase) preferred by the provider in the cases where Algolia is the source of the events. For search or recommendation this would typically be the case, but not necesarily for users arriving on a PDP as a direct target from a search or a link.
47
51
 
48
- Run `nx test provider-algolia` to execute the unit tests via [Jest](https://jestjs.io).
52
+ Note that we do not map `PurchasedObjectIDsAfterSearch` as it would require us to persist the search query ID that lead to the add-to-cart occuring on the cart items. This currently seems like an excess burden to impose on the cart interface.
53
+
54
+ The `ConvertedObjectIDs` and `ConvertedObjectIDsAfterSearch` are not mapped as they seem superfluous by all accounts in a product-purchase based flow. They could likely be used for other types of conversions in a more general setup, such as a customer finishing reading an article.
55
+
56
+ Finally the events that are related to filtering are not mapped, as they are by all accounts deprecated and no longer influence any of the recommendation or personalization features.
@@ -1,14 +1,18 @@
1
- import { AlgoliaSearchProvider } from "../providers/product-search.provider.js";
1
+ import { AlgoliaProductSearchProvider } from "../providers/product-search.provider.js";
2
2
  import { AlgoliaAnalyticsProvider } from "../providers/analytics.provider.js";
3
+ import { AlgoliaProductRecommendationsProvider } from "../providers/product-recommendations.provider.js";
3
4
  function withAlgoliaCapabilities(configuration, capabilities) {
4
5
  return (cache, context) => {
5
6
  const client = {};
6
7
  if (capabilities.productSearch) {
7
- client.productSearch = new AlgoliaSearchProvider(configuration, cache, context);
8
+ client.productSearch = new AlgoliaProductSearchProvider(cache, context, configuration);
8
9
  }
9
10
  if (capabilities.analytics) {
10
11
  client.analytics = new AlgoliaAnalyticsProvider(cache, context, configuration);
11
12
  }
13
+ if (capabilities.productRecommendations) {
14
+ client.productRecommendations = new AlgoliaProductRecommendationsProvider(configuration, cache, context);
15
+ }
12
16
  return client;
13
17
  };
14
18
  }
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./core/initialize.js";
2
2
  export * from "./providers/product-search.provider.js";
3
+ export * from "./providers/product-recommendations.provider.js";
3
4
  export * from "./schema/configuration.schema.js";
4
5
  export * from "./schema/capabilities.schema.js";
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@reactionary/provider-algolia",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
- "@reactionary/core": "0.3.1",
7
+ "@reactionary/core": "0.3.3",
8
8
  "algoliasearch": "^5.23.4",
9
9
  "zod": "4.1.9"
10
10
  },
@@ -2,13 +2,13 @@ import {
2
2
  AnalyticsProvider
3
3
  } from "@reactionary/core";
4
4
  import {
5
- insightsClient
5
+ algoliasearch
6
6
  } from "algoliasearch";
7
7
  class AlgoliaAnalyticsProvider extends AnalyticsProvider {
8
8
  constructor(cache, requestContext, config) {
9
9
  super(cache, requestContext);
10
10
  this.config = config;
11
- this.client = insightsClient(this.config.appId, this.config.apiKey);
11
+ this.client = algoliasearch(this.config.appId, this.config.apiKey).initInsights({});
12
12
  }
13
13
  async processProductAddToCart(event) {
14
14
  if (event.source && event.source.type === "search") {
@@ -21,7 +21,7 @@ class AlgoliaAnalyticsProvider extends AnalyticsProvider {
21
21
  userToken: this.context.session.identityContext.personalizationKey,
22
22
  queryID: event.source.identifier.key
23
23
  };
24
- this.client.pushEvents({
24
+ const response = await this.client.pushEvents({
25
25
  events: [algoliaEvent]
26
26
  });
27
27
  }
@@ -37,7 +37,7 @@ class AlgoliaAnalyticsProvider extends AnalyticsProvider {
37
37
  positions: [event.position],
38
38
  queryID: event.source.identifier.key
39
39
  };
40
- this.client.pushEvents({
40
+ const response = await this.client.pushEvents({
41
41
  events: [algoliaEvent]
42
42
  });
43
43
  }
@@ -51,7 +51,7 @@ class AlgoliaAnalyticsProvider extends AnalyticsProvider {
51
51
  objectIDs: event.products.map((x) => x.key),
52
52
  userToken: this.context.session.identityContext.personalizationKey
53
53
  };
54
- this.client.pushEvents({
54
+ const response = await this.client.pushEvents({
55
55
  events: [algoliaEvent]
56
56
  });
57
57
  }
@@ -62,10 +62,10 @@ class AlgoliaAnalyticsProvider extends AnalyticsProvider {
62
62
  eventType: "conversion",
63
63
  eventSubtype: "purchase",
64
64
  index: this.config.indexName,
65
- objectIDs: event.order.items.map((x) => x.identifier.key),
65
+ objectIDs: event.order.items.map((x) => x.variant.sku),
66
66
  userToken: this.context.session.identityContext.personalizationKey
67
67
  };
68
- this.client.pushEvents({
68
+ const response = await this.client.pushEvents({
69
69
  events: [algoliaEvent]
70
70
  });
71
71
  }
@@ -1,2 +1,3 @@
1
1
  export * from "./analytics.provider.js";
2
2
  export * from "./product-search.provider.js";
3
+ export * from "./product-recommendations.provider.js";
@@ -0,0 +1,174 @@
1
+ import {
2
+ ProductRecommendationsProvider
3
+ } from "@reactionary/core";
4
+ import { recommendClient } from "algoliasearch";
5
+ class AlgoliaProductRecommendationsProvider extends ProductRecommendationsProvider {
6
+ constructor(config, cache, context) {
7
+ super(cache, context);
8
+ this.config = config;
9
+ }
10
+ getRecommendClient() {
11
+ return recommendClient(this.config.appId, this.config.apiKey);
12
+ }
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ getRecommendationThreshold(_algorithm) {
15
+ return 10;
16
+ }
17
+ getQueryParametersForRecommendations(algorithm) {
18
+ return {
19
+ userToken: this.context.session.identityContext?.personalizationKey || "anonymous",
20
+ analytics: true,
21
+ analyticsTags: ["reactionary", algorithm],
22
+ clickAnalytics: true
23
+ };
24
+ }
25
+ /**
26
+ * Get frequently bought together recommendations using Algolia Recommend
27
+ */
28
+ async getFrequentlyBoughtTogetherRecommendations(query) {
29
+ const client = this.getRecommendClient();
30
+ try {
31
+ const response = await client.getRecommendations({
32
+ requests: [
33
+ {
34
+ indexName: this.config.indexName,
35
+ model: "bought-together",
36
+ objectID: query.sourceProduct.key,
37
+ maxRecommendations: query.numberOfRecommendations,
38
+ threshold: this.getRecommendationThreshold("bought-together"),
39
+ queryParameters: this.getQueryParametersForRecommendations("bought-together")
40
+ }
41
+ ]
42
+ });
43
+ const result = [];
44
+ if (response.results) {
45
+ for (const res of response.results) {
46
+ result.push(...this.parseRecommendation(res, query));
47
+ }
48
+ }
49
+ return result;
50
+ } catch (error) {
51
+ console.error("Error fetching frequently bought together recommendations:", error);
52
+ return [];
53
+ }
54
+ }
55
+ /**
56
+ * Get similar product recommendations using Algolia Recommend
57
+ */
58
+ async getSimilarProductsRecommendations(query) {
59
+ const client = this.getRecommendClient();
60
+ try {
61
+ const response = await client.getRecommendations({
62
+ requests: [
63
+ {
64
+ indexName: this.config.indexName,
65
+ model: "looking-similar",
66
+ objectID: query.sourceProduct.key,
67
+ maxRecommendations: query.numberOfRecommendations,
68
+ threshold: this.getRecommendationThreshold("looking-similar"),
69
+ queryParameters: this.getQueryParametersForRecommendations("looking-similar")
70
+ }
71
+ ]
72
+ });
73
+ const result = [];
74
+ if (response.results) {
75
+ for (const res of response.results) {
76
+ result.push(...this.parseRecommendation(res, query));
77
+ }
78
+ }
79
+ return result;
80
+ } catch (error) {
81
+ console.error("Error fetching similar product recommendations:", error);
82
+ return [];
83
+ }
84
+ }
85
+ /**
86
+ * Get related product recommendations using Algolia Recommend
87
+ */
88
+ async getRelatedProductsRecommendations(query) {
89
+ const client = this.getRecommendClient();
90
+ try {
91
+ const response = await client.getRecommendations({
92
+ requests: [
93
+ {
94
+ indexName: this.config.indexName,
95
+ model: "related-products",
96
+ objectID: query.sourceProduct.key,
97
+ maxRecommendations: query.numberOfRecommendations,
98
+ threshold: this.getRecommendationThreshold("related-products"),
99
+ queryParameters: this.getQueryParametersForRecommendations("related-products")
100
+ }
101
+ ]
102
+ });
103
+ const result = [];
104
+ if (response.results) {
105
+ for (const res of response.results) {
106
+ result.push(...this.parseRecommendation(res, query));
107
+ }
108
+ }
109
+ return result;
110
+ } catch (error) {
111
+ console.error("Error fetching related product recommendations:", error);
112
+ return [];
113
+ }
114
+ }
115
+ /**
116
+ * Get trending in category recommendations using Algolia Recommend
117
+ */
118
+ async getTrendingInCategoryRecommendations(query) {
119
+ const client = this.getRecommendClient();
120
+ try {
121
+ const response = await client.getRecommendations({
122
+ requests: [
123
+ {
124
+ indexName: this.config.indexName,
125
+ model: "trending-items",
126
+ facetName: "categories",
127
+ facetValue: query.sourceCategory.key,
128
+ maxRecommendations: query.numberOfRecommendations,
129
+ threshold: this.getRecommendationThreshold("trending-items"),
130
+ queryParameters: this.getQueryParametersForRecommendations("trending-items")
131
+ }
132
+ ]
133
+ });
134
+ const result = [];
135
+ if (response.results) {
136
+ for (const res of response.results) {
137
+ result.push(...this.parseRecommendation(res, query));
138
+ }
139
+ }
140
+ return result;
141
+ } catch (error) {
142
+ console.error("Error fetching trending in category recommendations:", error);
143
+ return [];
144
+ }
145
+ }
146
+ parseRecommendation(res, query) {
147
+ const result = [];
148
+ for (const hit of res.hits) {
149
+ const recommendationIdentifier = {
150
+ key: res.queryID || "x",
151
+ algorithm: query.algorithm,
152
+ abTestID: res.abTestID,
153
+ abTestVariantID: res.abTestVariantID
154
+ };
155
+ const recommendation = this.parseSingle(hit, recommendationIdentifier);
156
+ result.push(recommendation);
157
+ }
158
+ return result;
159
+ }
160
+ /**
161
+ * Maps Algolia recommendation results to ProductRecommendation format
162
+ */
163
+ parseSingle(hit, recommendationIdentifier) {
164
+ return {
165
+ recommendationIdentifier,
166
+ product: {
167
+ key: hit.objectID
168
+ }
169
+ };
170
+ }
171
+ }
172
+ export {
173
+ AlgoliaProductRecommendationsProvider
174
+ };
@@ -23,8 +23,8 @@ import {
23
23
  success
24
24
  } from "@reactionary/core";
25
25
  import { algoliasearch } from "algoliasearch";
26
- class AlgoliaSearchProvider extends ProductSearchProvider {
27
- constructor(config, cache, context) {
26
+ class AlgoliaProductSearchProvider extends ProductSearchProvider {
27
+ constructor(cache, context, config) {
28
28
  super(cache, context);
29
29
  this.config = config;
30
30
  }
@@ -136,7 +136,9 @@ class AlgoliaSearchProvider extends ProductSearchProvider {
136
136
  term: query.search.term,
137
137
  facets: query.search.facets,
138
138
  filters: query.search.filters,
139
- paginationOptions: query.search.paginationOptions
139
+ paginationOptions: query.search.paginationOptions,
140
+ index: body.index || "",
141
+ key: body.queryID || ""
140
142
  },
141
143
  pageNumber: (body.page || 0) + 1,
142
144
  pageSize: body.hitsPerPage || 0,
@@ -177,7 +179,7 @@ __decorateClass([
177
179
  inputSchema: ProductSearchQueryByTermSchema,
178
180
  outputSchema: ProductSearchResultSchema
179
181
  })
180
- ], AlgoliaSearchProvider.prototype, "queryByTerm", 1);
182
+ ], AlgoliaProductSearchProvider.prototype, "queryByTerm", 1);
181
183
  export {
182
- AlgoliaSearchProvider
184
+ AlgoliaProductSearchProvider
183
185
  };
@@ -1,7 +1,8 @@
1
1
  import { CapabilitiesSchema } from "@reactionary/core";
2
2
  const AlgoliaCapabilitiesSchema = CapabilitiesSchema.pick({
3
3
  productSearch: true,
4
- analytics: true
4
+ analytics: true,
5
+ productRecommendations: true
5
6
  }).partial();
6
7
  export {
7
8
  AlgoliaCapabilitiesSchema
@@ -0,0 +1,9 @@
1
+ import { ProductRecommendationIdentifierSchema } from "@reactionary/core";
2
+ import z from "zod";
3
+ const AlgoliaProductSearchIdentifierSchema = ProductRecommendationIdentifierSchema.extend({
4
+ abTestID: z.number().optional(),
5
+ abTestVariantID: z.number().optional()
6
+ });
7
+ export {
8
+ AlgoliaProductSearchIdentifierSchema
9
+ };
package/src/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './core/initialize.js';
2
2
  export * from './providers/product-search.provider.js';
3
+ export * from './providers/product-recommendations.provider.js';
3
4
  export * from './schema/configuration.schema.js';
4
5
  export * from './schema/capabilities.schema.js';
@@ -1,2 +1,3 @@
1
1
  export * from './analytics.provider.js';
2
2
  export * from './product-search.provider.js';
3
+ export * from './product-recommendations.provider.js';
@@ -0,0 +1,58 @@
1
+ import { type Cache, ProductRecommendationsProvider, type ProductRecommendation, type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery, type ProductRecommendationAlgorithmSimilarProductsQuery, type ProductRecommendationAlgorithmRelatedProductsQuery, type ProductRecommendationAlgorithmTrendingInCategoryQuery, type RequestContext, type ProductRecommendationsQuery } from '@reactionary/core';
2
+ import { type RecommendationsResults, type RecommendClient, type RecommendSearchParams } from 'algoliasearch';
3
+ import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
4
+ import type { AlgoliaProductRecommendationIdentifier } from '../schema/product-recommendation.schema.js';
5
+ interface AlgoliaRecommendHit {
6
+ objectID: string;
7
+ sku?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * AlgoliaProductRecommendationsProvider
12
+ *
13
+ * Provides product recommendations using Algolia's Recommend API.
14
+ * Supports frequentlyBoughtTogether, similar, related, and trendingInCategory algorithms.
15
+ *
16
+ * Note: This requires Algolia Recommend to be enabled and AI models to be trained.
17
+ * See: https://www.algolia.com/doc/guides/algolia-recommend/overview/
18
+ */
19
+ export declare class AlgoliaProductRecommendationsProvider extends ProductRecommendationsProvider {
20
+ protected config: AlgoliaConfiguration;
21
+ constructor(config: AlgoliaConfiguration, cache: Cache, context: RequestContext);
22
+ protected getRecommendClient(): RecommendClient;
23
+ protected getRecommendationThreshold(_algorithm: string): number;
24
+ protected getQueryParametersForRecommendations(algorithm: string): RecommendSearchParams;
25
+ /**
26
+ * Get frequently bought together recommendations using Algolia Recommend
27
+ */
28
+ protected getFrequentlyBoughtTogetherRecommendations(query: ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery): Promise<ProductRecommendation[]>;
29
+ /**
30
+ * Get similar product recommendations using Algolia Recommend
31
+ */
32
+ protected getSimilarProductsRecommendations(query: ProductRecommendationAlgorithmSimilarProductsQuery): Promise<ProductRecommendation[]>;
33
+ /**
34
+ * Get related product recommendations using Algolia Recommend
35
+ */
36
+ protected getRelatedProductsRecommendations(query: ProductRecommendationAlgorithmRelatedProductsQuery): Promise<ProductRecommendation[]>;
37
+ /**
38
+ * Get trending in category recommendations using Algolia Recommend
39
+ */
40
+ protected getTrendingInCategoryRecommendations(query: ProductRecommendationAlgorithmTrendingInCategoryQuery): Promise<ProductRecommendation[]>;
41
+ protected parseRecommendation(res: RecommendationsResults, query: ProductRecommendationsQuery): {
42
+ [x: string]: unknown;
43
+ recommendationIdentifier: {
44
+ [x: string]: unknown;
45
+ key: string;
46
+ algorithm: string;
47
+ };
48
+ product: {
49
+ [x: string]: unknown;
50
+ key: string;
51
+ };
52
+ }[];
53
+ /**
54
+ * Maps Algolia recommendation results to ProductRecommendation format
55
+ */
56
+ protected parseSingle(hit: AlgoliaRecommendHit, recommendationIdentifier: AlgoliaProductRecommendationIdentifier): ProductRecommendation;
57
+ }
58
+ export {};
@@ -11,9 +11,9 @@ interface AlgoliaNativeRecord {
11
11
  name?: string;
12
12
  variants: Array<AlgoliaNativeVariant>;
13
13
  }
14
- export declare class AlgoliaSearchProvider extends ProductSearchProvider {
14
+ export declare class AlgoliaProductSearchProvider extends ProductSearchProvider {
15
15
  protected config: AlgoliaConfiguration;
16
- constructor(config: AlgoliaConfiguration, cache: Cache, context: RequestContext);
16
+ constructor(cache: Cache, context: RequestContext, config: AlgoliaConfiguration);
17
17
  queryByTerm(payload: ProductSearchQueryByTerm): Promise<Result<ProductSearchResult>>;
18
18
  createCategoryNavigationFilter(payload: ProductSearchQueryCreateNavigationFilter): Promise<Result<FacetValueIdentifier>>;
19
19
  protected parseSingle(body: AlgoliaNativeRecord): {
@@ -39,6 +39,8 @@ export declare class AlgoliaSearchProvider extends ProductSearchProvider {
39
39
  pageNumber: number;
40
40
  pageSize: number;
41
41
  };
42
+ index: string;
43
+ key: string;
42
44
  };
43
45
  pageNumber: number;
44
46
  pageSize: number;
@@ -2,5 +2,6 @@ import type { z } from 'zod';
2
2
  export declare const AlgoliaCapabilitiesSchema: z.ZodObject<{
3
3
  analytics: z.ZodOptional<z.ZodBoolean>;
4
4
  productSearch: z.ZodOptional<z.ZodBoolean>;
5
+ productRecommendations: z.ZodOptional<z.ZodBoolean>;
5
6
  }, z.core.$loose>;
6
7
  export type AlgoliaCapabilities = z.infer<typeof AlgoliaCapabilitiesSchema>;
@@ -0,0 +1,8 @@
1
+ import z from "zod";
2
+ export declare const AlgoliaProductSearchIdentifierSchema: z.ZodObject<{
3
+ key: z.ZodString;
4
+ algorithm: z.ZodString;
5
+ abTestID: z.ZodOptional<z.ZodNumber>;
6
+ abTestVariantID: z.ZodOptional<z.ZodNumber>;
7
+ }, z.z.core.$loose>;
8
+ export type AlgoliaProductRecommendationIdentifier = z.infer<typeof AlgoliaProductSearchIdentifierSchema>;