@reactionary/source 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.
Files changed (46) hide show
  1. package/core/src/client/client-builder.ts +8 -0
  2. package/core/src/client/client.ts +4 -0
  3. package/core/src/initialization.ts +4 -1
  4. package/core/src/providers/identity.provider.ts +5 -0
  5. package/core/src/providers/index.ts +2 -0
  6. package/core/src/providers/product-associations.provider.ts +49 -0
  7. package/core/src/providers/product-recommendations.provider.ts +150 -0
  8. package/core/src/schemas/capabilities.schema.ts +2 -0
  9. package/core/src/schemas/models/identifiers.model.ts +8 -0
  10. package/core/src/schemas/models/index.ts +1 -0
  11. package/core/src/schemas/models/product-recommendations.model.ts +10 -0
  12. package/core/src/schemas/queries/index.ts +2 -0
  13. package/core/src/schemas/queries/product-associations.query.ts +23 -0
  14. package/core/src/schemas/queries/product-recommendations.query.ts +72 -0
  15. package/core/src/schemas/session.schema.ts +2 -1
  16. package/documentation/docs/7-marketing.md +95 -0
  17. package/documentation/docs/8-tracking.md +1 -1
  18. package/examples/node/package.json +6 -6
  19. package/examples/node/src/basic/client-creation.spec.ts +2 -2
  20. package/examples/node/src/capabilities/product-recommendations.spec.ts +96 -0
  21. package/examples/node/src/utils.ts +3 -0
  22. package/package.json +1 -1
  23. package/providers/algolia/README.md +12 -4
  24. package/providers/algolia/src/core/initialize.ts +8 -2
  25. package/providers/algolia/src/index.ts +1 -0
  26. package/providers/algolia/src/providers/analytics.provider.ts +9 -7
  27. package/providers/algolia/src/providers/index.ts +2 -1
  28. package/providers/algolia/src/providers/product-recommendations.provider.ts +234 -0
  29. package/providers/algolia/src/providers/product-search.provider.ts +5 -4
  30. package/providers/algolia/src/schema/capabilities.schema.ts +2 -1
  31. package/providers/algolia/src/schema/product-recommendation.schema.ts +9 -0
  32. package/providers/algolia/src/test/analytics.spec.ts +138 -0
  33. package/providers/commercetools/src/providers/identity.provider.ts +8 -1
  34. package/providers/commercetools/src/test/caching.spec.ts +3 -3
  35. package/providers/commercetools/src/test/identity.spec.ts +2 -2
  36. package/providers/google-analytics/package.json +0 -6
  37. package/providers/medusa/src/core/initialize.ts +6 -0
  38. package/providers/medusa/src/index.ts +1 -0
  39. package/providers/medusa/src/providers/identity.provider.ts +34 -10
  40. package/providers/medusa/src/providers/product-recommendations.provider.ts +117 -0
  41. package/providers/medusa/src/schema/capabilities.schema.ts +1 -0
  42. package/providers/meilisearch/src/core/initialize.ts +6 -0
  43. package/providers/meilisearch/src/index.ts +1 -0
  44. package/providers/meilisearch/src/providers/index.ts +1 -0
  45. package/providers/meilisearch/src/providers/product-recommendations.provider.ts +89 -0
  46. package/providers/meilisearch/src/schema/capabilities.schema.ts +1 -0
@@ -0,0 +1,96 @@
1
+ import 'dotenv/config';
2
+ import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { createClient, PrimaryProvider } from '../utils.js';
4
+ import type { ProductSearchQueryCreateNavigationFilter } from '@reactionary/core';
5
+
6
+ const testData = {
7
+ product: {
8
+ id: 'product_10959528',
9
+ name: 'Manhattan 170703 cable accessory Cable kit',
10
+ image: 'https://images.icecat.biz/img/norm/high/10959528-2837.jpg',
11
+ sku: '0766623170703',
12
+ slug: 'manhattan-170703-cable-accessory-cable-kit-10959528',
13
+ },
14
+ productWithMultiVariants: {
15
+ slug: 'hp-gk859aa-mouse-office-bluetooth-laser-1600-dpi-1377612',
16
+ },
17
+ };
18
+
19
+ describe.each([PrimaryProvider.MEDUSA])(
20
+ 'Product Recommendations - Collections - %s',
21
+ (provider) => {
22
+ let client: ReturnType<typeof createClient>;
23
+
24
+ beforeEach(() => {
25
+ client = createClient(provider);
26
+ });
27
+
28
+ it('should be able to return a list of products for a collection', async () => {
29
+ const result = await client.productRecommendations.getCollection({
30
+ collectionName: 'newest-arrivals',
31
+ numberOfRecommendations: 10,
32
+ });
33
+
34
+ if (!result.success) {
35
+ assert.fail(JSON.stringify(result.error));
36
+ }
37
+
38
+ expect(result.value.length).toBeGreaterThan(0);
39
+ });
40
+
41
+ it('should return an empty result for an unknown collection', async () => {
42
+ const result = await client.productRecommendations.getCollection({
43
+ collectionName: 'Unknown Collection',
44
+ numberOfRecommendations: 10,
45
+ });
46
+
47
+ if (!result.success) {
48
+ assert.fail(JSON.stringify(result.error));
49
+ }
50
+
51
+ expect(result.value.length).toBe(0);
52
+ });
53
+ });
54
+
55
+
56
+ describe.each([PrimaryProvider.MEILISEARCH])(
57
+ 'Product Recommendations - Similar - %s',
58
+ (provider) => {
59
+ let client: ReturnType<typeof createClient>;
60
+
61
+ beforeEach(() => {
62
+ client = createClient(provider);
63
+ });
64
+
65
+ it('should be able to return a list of products for recommendation - Similar ', async () => {
66
+ const result = await client.productRecommendations.getRecommendations({
67
+ algorithm: 'similar',
68
+ sourceProduct: {
69
+ key: testData.product.id,
70
+ },
71
+ numberOfRecommendations: 10,
72
+ });
73
+
74
+ if (!result.success) {
75
+ assert.fail(JSON.stringify(result.error));
76
+ }
77
+
78
+ expect(result.value.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ it('should return an empty result for an unknown sku', async () => {
82
+ const result = await client.productRecommendations.getRecommendations({
83
+ algorithm: 'similar',
84
+ sourceProduct: {
85
+ key: 'unknown-product-id',
86
+ },
87
+ numberOfRecommendations: 10,
88
+ });
89
+
90
+ if (!result.success) {
91
+ assert.fail(JSON.stringify(result.error));
92
+ }
93
+
94
+ expect(result.value.length).toBe(0);
95
+ });
96
+ });
@@ -91,6 +91,7 @@ export function createClient(provider: PrimaryProvider) {
91
91
  order: true,
92
92
  price: true,
93
93
  productSearch: true,
94
+ productRecommendations: true,
94
95
  orderSearch: true,
95
96
  store: true,
96
97
  profile: true
@@ -124,6 +125,7 @@ export function createClient(provider: PrimaryProvider) {
124
125
  builder = builder.withCapability(
125
126
  withAlgoliaCapabilities(getAlgoliaTestConfiguration(), {
126
127
  productSearch: true,
128
+ productRecommendations: true,
127
129
  })
128
130
  );
129
131
  }
@@ -133,6 +135,7 @@ export function createClient(provider: PrimaryProvider) {
133
135
  withMeilisearchCapabilities(getMeilisearchTestConfiguration(), {
134
136
  productSearch: true,
135
137
  orderSearch: true,
138
+ productRecommendations: true,
136
139
  }),
137
140
  );
138
141
  builder = builder.withCapability(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/source",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "dependencies": {
@@ -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,21 +1,27 @@
1
1
  import type { Cache, ClientFromCapabilities, RequestContext } from "@reactionary/core";
2
- import { AlgoliaSearchProvider } from "../providers/product-search.provider.js";
2
+ import { AlgoliaProductSearchProvider } from "../providers/product-search.provider.js";
3
3
  import type { AlgoliaCapabilities } from "../schema/capabilities.schema.js";
4
4
  import type { AlgoliaConfiguration } from "../schema/configuration.schema.js";
5
5
  import { AlgoliaAnalyticsProvider } from "../providers/analytics.provider.js";
6
+ import { AlgoliaProductRecommendationsProvider } from "../providers/product-recommendations.provider.js";
6
7
 
7
8
  export function withAlgoliaCapabilities<T extends AlgoliaCapabilities>(configuration: AlgoliaConfiguration, capabilities: T) {
8
9
  return (cache: Cache, context: RequestContext): ClientFromCapabilities<T> => {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
11
  const client: any = {};
10
12
 
11
13
  if (capabilities.productSearch) {
12
- client.productSearch = new AlgoliaSearchProvider(configuration, cache, context);
14
+ client.productSearch = new AlgoliaProductSearchProvider(cache, context, configuration);
13
15
  }
14
16
 
15
17
  if (capabilities.analytics) {
16
18
  client.analytics = new AlgoliaAnalyticsProvider(cache, context, configuration);
17
19
  }
18
20
 
21
+ if (capabilities.productRecommendations) {
22
+ client.productRecommendations = new AlgoliaProductRecommendationsProvider(configuration, cache, context);
23
+ }
24
+
19
25
  return client;
20
26
  };
21
27
  }
@@ -1,5 +1,6 @@
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
 
4
5
  export * from './schema/configuration.schema.js';
5
6
  export * from './schema/capabilities.schema.js';
@@ -8,12 +8,12 @@ import {
8
8
  type RequestContext,
9
9
  } from '@reactionary/core';
10
10
  import {
11
- insightsClient,
12
11
  type InsightsClient,
13
12
  type ViewedObjectIDs,
14
13
  type ClickedObjectIDsAfterSearch,
15
14
  type AddedToCartObjectIDsAfterSearch,
16
15
  type PurchasedObjectIDs,
16
+ algoliasearch,
17
17
  } from 'algoliasearch';
18
18
  import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
19
19
  import type { AlgoliaProductSearchIdentifier } from '../schema/search.schema.js';
@@ -30,7 +30,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
30
30
  super(cache, requestContext);
31
31
 
32
32
  this.config = config;
33
- this.client = insightsClient(this.config.appId, this.config.apiKey);
33
+ this.client = algoliasearch(this.config.appId, this.config.apiKey).initInsights({});
34
34
  }
35
35
 
36
36
  protected override async processProductAddToCart(
@@ -48,7 +48,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
48
48
  .key,
49
49
  } satisfies AddedToCartObjectIDsAfterSearch;
50
50
 
51
- this.client.pushEvents({
51
+ const response = await this.client.pushEvents({
52
52
  events: [algoliaEvent],
53
53
  });
54
54
  }
@@ -69,7 +69,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
69
69
  .key,
70
70
  } satisfies ClickedObjectIDsAfterSearch;
71
71
 
72
- this.client.pushEvents({
72
+ const response = await this.client.pushEvents({
73
73
  events: [algoliaEvent],
74
74
  });
75
75
  }
@@ -87,7 +87,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
87
87
  userToken: this.context.session.identityContext.personalizationKey,
88
88
  } satisfies ViewedObjectIDs;
89
89
 
90
- this.client.pushEvents({
90
+ const response = await this.client.pushEvents({
91
91
  events: [algoliaEvent],
92
92
  });
93
93
  }
@@ -96,16 +96,18 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
96
96
  protected override async processPurchase(
97
97
  event: AnalyticsMutationPurchaseEvent
98
98
  ): Promise<void> {
99
+ // TODO: Figure out how to handle the problem below. From the order we have the SKUs,
100
+ // but in Algolia we have the products indexed, and we can't really resolve it here...
99
101
  const algoliaEvent = {
100
102
  eventName: 'purchase',
101
103
  eventType: 'conversion',
102
104
  eventSubtype: 'purchase',
103
105
  index: this.config.indexName,
104
- objectIDs: event.order.items.map((x) => x.identifier.key),
106
+ objectIDs: event.order.items.map((x) => x.variant.sku),
105
107
  userToken: this.context.session.identityContext.personalizationKey,
106
108
  } satisfies PurchasedObjectIDs;
107
109
 
108
- this.client.pushEvents({
110
+ const response = await this.client.pushEvents({
109
111
  events: [algoliaEvent],
110
112
  });
111
113
  }
@@ -1,2 +1,3 @@
1
1
  export * from './analytics.provider.js';
2
- export * from './product-search.provider.js';
2
+ export * from './product-search.provider.js';
3
+ export * from './product-recommendations.provider.js';
@@ -0,0 +1,234 @@
1
+ import {
2
+ type Cache,
3
+ ProductRecommendationsProvider,
4
+ type ProductRecommendation,
5
+ type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery,
6
+ type ProductRecommendationAlgorithmSimilarProductsQuery,
7
+ type ProductRecommendationAlgorithmRelatedProductsQuery,
8
+ type ProductRecommendationAlgorithmTrendingInCategoryQuery,
9
+ type RequestContext,
10
+ type ProductRecommendationsQuery,
11
+ } from '@reactionary/core';
12
+ import { recommendClient, type BoughtTogetherQuery, type LookingSimilarQuery, type RecommendationsResults, type RecommendClient, type RecommendSearchParams, type RelatedQuery, type TrendingItemsQuery } from 'algoliasearch';
13
+ import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
14
+ import type { AlgoliaProductRecommendationIdentifier } from '../schema/product-recommendation.schema.js';
15
+
16
+ interface AlgoliaRecommendHit {
17
+ objectID: string;
18
+ sku?: string;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ /**
23
+ * AlgoliaProductRecommendationsProvider
24
+ *
25
+ * Provides product recommendations using Algolia's Recommend API.
26
+ * Supports frequentlyBoughtTogether, similar, related, and trendingInCategory algorithms.
27
+ *
28
+ * Note: This requires Algolia Recommend to be enabled and AI models to be trained.
29
+ * See: https://www.algolia.com/doc/guides/algolia-recommend/overview/
30
+ */
31
+
32
+
33
+
34
+
35
+ export class AlgoliaProductRecommendationsProvider extends ProductRecommendationsProvider {
36
+ protected config: AlgoliaConfiguration;
37
+
38
+ constructor(config: AlgoliaConfiguration, cache: Cache, context: RequestContext) {
39
+ super(cache, context);
40
+ this.config = config;
41
+ }
42
+
43
+ protected getRecommendClient(): RecommendClient {
44
+ return recommendClient(this.config.appId, this.config.apiKey);
45
+ }
46
+
47
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
48
+ protected getRecommendationThreshold(_algorithm: string): number {
49
+ // Default threshold can be customized per algorithm if needed
50
+ // The parameter is currently unused but kept for future algorithm-specific threshold customization
51
+ return 10;
52
+ }
53
+
54
+ protected getQueryParametersForRecommendations(algorithm: string): RecommendSearchParams {
55
+ return {
56
+ userToken: this.context.session.identityContext?.personalizationKey || 'anonymous',
57
+ analytics: true,
58
+ analyticsTags: ['reactionary', algorithm],
59
+ clickAnalytics: true
60
+ } satisfies RecommendSearchParams;
61
+ }
62
+
63
+ /**
64
+ * Get frequently bought together recommendations using Algolia Recommend
65
+ */
66
+ protected override async getFrequentlyBoughtTogetherRecommendations(
67
+ query: ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery
68
+ ): Promise<ProductRecommendation[]> {
69
+ const client = this.getRecommendClient();
70
+
71
+ try {
72
+ // Note: Algolia's Recommend API requires setting up AI Recommend models
73
+ // This implementation uses the getRecommendations method from the recommend client
74
+ const response = await client.getRecommendations({
75
+ requests: [
76
+ {
77
+ indexName: this.config.indexName,
78
+ model: 'bought-together',
79
+ objectID: query.sourceProduct.key,
80
+ maxRecommendations: query.numberOfRecommendations,
81
+ threshold: this.getRecommendationThreshold('bought-together'),
82
+ queryParameters: this.getQueryParametersForRecommendations('bought-together')
83
+ } satisfies BoughtTogetherQuery,
84
+ ],
85
+
86
+ });
87
+
88
+ const result = [];
89
+ if (response.results) {
90
+ for(const res of response.results) {
91
+ result.push(...this.parseRecommendation(res, query));
92
+ }
93
+ }
94
+ return result;
95
+ } catch (error) {
96
+ console.error('Error fetching frequently bought together recommendations:', error);
97
+ return [];
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Get similar product recommendations using Algolia Recommend
103
+ */
104
+ protected override async getSimilarProductsRecommendations(
105
+ query: ProductRecommendationAlgorithmSimilarProductsQuery
106
+ ): Promise<ProductRecommendation[]> {
107
+ const client = this.getRecommendClient();
108
+
109
+ try {
110
+ const response = await client.getRecommendations({
111
+ requests: [
112
+ {
113
+ indexName: this.config.indexName,
114
+ model: 'looking-similar',
115
+ objectID: query.sourceProduct.key,
116
+ maxRecommendations: query.numberOfRecommendations,
117
+ threshold: this.getRecommendationThreshold('looking-similar'),
118
+ queryParameters: this.getQueryParametersForRecommendations('looking-similar')
119
+ } satisfies LookingSimilarQuery
120
+ ],
121
+ });
122
+
123
+ const result = [];
124
+ if (response.results) {
125
+ for(const res of response.results) {
126
+ result.push(...this.parseRecommendation(res, query));
127
+ }
128
+ }
129
+ return result;
130
+ } catch (error) {
131
+ console.error('Error fetching similar product recommendations:', error);
132
+ return [];
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get related product recommendations using Algolia Recommend
138
+ */
139
+ protected override async getRelatedProductsRecommendations(
140
+ query: ProductRecommendationAlgorithmRelatedProductsQuery
141
+ ): Promise<ProductRecommendation[]> {
142
+ const client = this.getRecommendClient();
143
+
144
+ try {
145
+ const response = await client.getRecommendations({
146
+ requests: [
147
+ {
148
+ indexName: this.config.indexName,
149
+ model: 'related-products',
150
+ objectID: query.sourceProduct.key,
151
+ maxRecommendations: query.numberOfRecommendations,
152
+ threshold: this.getRecommendationThreshold('related-products'),
153
+ queryParameters: this.getQueryParametersForRecommendations('related-products')
154
+ } satisfies RelatedQuery,
155
+ ],
156
+ });
157
+
158
+ const result = [];
159
+ if (response.results) {
160
+ for(const res of response.results) {
161
+ result.push(...this.parseRecommendation(res, query));
162
+ }
163
+ }
164
+ return result;
165
+ } catch (error) {
166
+ console.error('Error fetching related product recommendations:', error);
167
+ return [];
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get trending in category recommendations using Algolia Recommend
173
+ */
174
+ protected override async getTrendingInCategoryRecommendations(
175
+ query: ProductRecommendationAlgorithmTrendingInCategoryQuery
176
+ ): Promise<ProductRecommendation[]> {
177
+ const client = this.getRecommendClient();
178
+
179
+ try {
180
+ const response = await client.getRecommendations({
181
+ requests: [
182
+ {
183
+ indexName: this.config.indexName,
184
+ model: 'trending-items',
185
+ facetName: 'categories',
186
+ facetValue: query.sourceCategory.key,
187
+ maxRecommendations: query.numberOfRecommendations,
188
+ threshold: this.getRecommendationThreshold('trending-items'),
189
+ queryParameters: this.getQueryParametersForRecommendations('trending-items')
190
+ } satisfies TrendingItemsQuery,
191
+ ],
192
+ });
193
+
194
+ const result = [];
195
+ if (response.results) {
196
+ for(const res of response.results) {
197
+ result.push(...this.parseRecommendation(res, query));
198
+ }
199
+ }
200
+ return result;
201
+ } catch (error) {
202
+ console.error('Error fetching trending in category recommendations:', error);
203
+ return [];
204
+ }
205
+ }
206
+
207
+
208
+ protected parseRecommendation(res: RecommendationsResults, query: ProductRecommendationsQuery) {
209
+ const result = [];
210
+ for(const hit of res.hits as AlgoliaRecommendHit[]) {
211
+ const recommendationIdentifier = {
212
+ key: res.queryID || 'x',
213
+ algorithm: query.algorithm,
214
+ abTestID: res.abTestID,
215
+ abTestVariantID: res.abTestVariantID
216
+ } satisfies AlgoliaProductRecommendationIdentifier
217
+ const recommendation = this.parseSingle(hit, recommendationIdentifier)
218
+ result.push(recommendation);
219
+ }
220
+ return result;
221
+ }
222
+
223
+ /**
224
+ * Maps Algolia recommendation results to ProductRecommendation format
225
+ */
226
+ protected parseSingle(hit: AlgoliaRecommendHit, recommendationIdentifier: AlgoliaProductRecommendationIdentifier): ProductRecommendation {
227
+ return {
228
+ recommendationIdentifier,
229
+ product: {
230
+ key: hit.objectID,
231
+ },
232
+ } satisfies ProductRecommendation;
233
+ }
234
+ }
@@ -40,10 +40,10 @@ interface AlgoliaNativeRecord {
40
40
  }
41
41
 
42
42
 
43
- export class AlgoliaSearchProvider extends ProductSearchProvider {
43
+ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
44
44
  protected config: AlgoliaConfiguration;
45
45
 
46
- constructor(config: AlgoliaConfiguration, cache: Cache, context: RequestContext) {
46
+ constructor(cache: Cache, context: RequestContext, config: AlgoliaConfiguration) {
47
47
  super(cache, context);
48
48
  this.config = config;
49
49
  }
@@ -195,7 +195,8 @@ export class AlgoliaSearchProvider extends ProductSearchProvider {
195
195
  facets: query.search.facets,
196
196
  filters: query.search.filters,
197
197
  paginationOptions: query.search.paginationOptions,
198
-
198
+ index: body.index || '',
199
+ key: body.queryID || '',
199
200
  },
200
201
  pageNumber: (body.page || 0) + 1,
201
202
  pageSize: body.hitsPerPage || 0,
@@ -203,7 +204,7 @@ export class AlgoliaSearchProvider extends ProductSearchProvider {
203
204
  totalPages: body.nbPages || 0,
204
205
  items: items,
205
206
  facets,
206
- } satisfies ProductSearchResult;
207
+ } satisfies AlgoliaProductSearchResult;
207
208
 
208
209
  return result;
209
210
  }
@@ -3,7 +3,8 @@ import type { z } from 'zod';
3
3
 
4
4
  export const AlgoliaCapabilitiesSchema = CapabilitiesSchema.pick({
5
5
  productSearch: true,
6
- analytics: true
6
+ analytics: true,
7
+ productRecommendations: true
7
8
  }).partial();
8
9
 
9
10
  export type AlgoliaCapabilities = z.infer<typeof AlgoliaCapabilitiesSchema>;
@@ -0,0 +1,9 @@
1
+ import { ProductRecommendationIdentifierSchema } from "@reactionary/core";
2
+ import z from "zod";
3
+
4
+ export const AlgoliaProductSearchIdentifierSchema = ProductRecommendationIdentifierSchema.extend({
5
+ abTestID: z.number().optional(),
6
+ abTestVariantID: z.number().optional()
7
+ });
8
+
9
+ export type AlgoliaProductRecommendationIdentifier = z.infer<typeof AlgoliaProductSearchIdentifierSchema>;