@reactionary/source 0.3.5 → 0.3.7
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/schemas/models/product-recommendations.model.ts +16 -1
- package/core/src/schemas/session.schema.ts +4 -4
- package/core/src/test/request-context.spec.ts +15 -0
- package/examples/node/package.json +6 -6
- package/examples/node/src/capabilities/product-recommendations.spec.ts +32 -1
- package/package.json +1 -1
- package/providers/algolia/src/providers/product-recommendations.provider.ts +38 -10
- package/providers/algolia/src/providers/product-search.provider.ts +1 -12
- package/providers/algolia/src/schema/search.schema.ts +13 -0
- package/providers/medusa/src/core/client.ts +6 -6
- package/providers/medusa/src/providers/product-recommendations.provider.ts +58 -11
- package/providers/meilisearch/src/providers/product-recommendations.provider.ts +39 -11
- package/providers/meilisearch/src/providers/product-search.provider.ts +1 -13
- package/providers/meilisearch/src/schema/search.schema.ts +11 -0
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { ProductIdentifierSchema, ProductRecommendationIdentifierSchema, ProductVariantIdentifierSchema } from "./identifiers.model.js";
|
|
3
|
+
import { ProductSearchResultItemSchema } from "./product-search.model.js";
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
+
export const BaseProductRecommendationSchema = z.looseObject({
|
|
5
6
|
recommendationIdentifier: ProductRecommendationIdentifierSchema.describe('The identifier for the product recommendation, which includes a key and an algorithm and any other vendor specific/instance specific data '),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const ProductRecommendationIdOnlySchema = BaseProductRecommendationSchema.extend({
|
|
10
|
+
recommendationReturnType: z.literal('idOnly').describe('The type of recommendation return'),
|
|
6
11
|
product: ProductIdentifierSchema.describe('The identifier for the recommended product.'),
|
|
7
12
|
});
|
|
13
|
+
export const ProductRecommendationProductSearchResultItemSchema = BaseProductRecommendationSchema.extend({
|
|
14
|
+
recommendationReturnType: z.literal('productSearchResultItem').describe('The type of recommendation return'),
|
|
15
|
+
product: ProductSearchResultItemSchema.describe('The recommended product, including its identifier, name, slug, and variants. This can be used to display the recommended product directly on the frontend without needing to make an additional request to fetch the product details.'),
|
|
16
|
+
});
|
|
8
17
|
|
|
18
|
+
export const ProductRecommendationSchema = z.discriminatedUnion('recommendationReturnType', [
|
|
19
|
+
ProductRecommendationIdOnlySchema,
|
|
20
|
+
ProductRecommendationProductSearchResultItemSchema
|
|
21
|
+
]);
|
|
9
22
|
|
|
23
|
+
export type ProductRecommendationIdOnly = z.infer<typeof ProductRecommendationIdOnlySchema>;
|
|
24
|
+
export type ProductRecommendationSearchItem = z.infer<typeof ProductRecommendationProductSearchResultItemSchema>;
|
|
10
25
|
export type ProductRecommendation = z.infer<typeof ProductRecommendationSchema>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { WebStoreIdentifierSchema } from './models/identifiers.model.js';
|
|
3
3
|
import { CurrencySchema } from './models/currency.model.js';
|
|
4
4
|
import { IdentitySchema } from './models/identity.model.js';
|
|
5
5
|
|
|
@@ -14,12 +14,12 @@ export const LanguageContextSchema = z.looseObject( {
|
|
|
14
14
|
export const IdentityContextSchema = z.looseObject({
|
|
15
15
|
identity: IdentitySchema,
|
|
16
16
|
personalizationKey: z.string(),
|
|
17
|
-
lastUpdated: z.date()
|
|
17
|
+
lastUpdated: z.coerce.date()
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
export const SessionSchema = z.
|
|
20
|
+
export const SessionSchema = z.looseObject({
|
|
21
21
|
identityContext: IdentityContextSchema
|
|
22
|
-
})
|
|
22
|
+
});
|
|
23
23
|
|
|
24
24
|
export const TaxJurisdictionSchema = z.object( {
|
|
25
25
|
countryCode: z.string().default('US'),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createInitialRequestContext } from "../initialization.js";
|
|
3
|
+
import { RequestContextSchema } from "../schemas/session.schema.js";
|
|
4
|
+
|
|
5
|
+
describe('Request Context', () => {
|
|
6
|
+
it('should be able to serialize the request context as a JSON string, and have it parse', async () => {
|
|
7
|
+
const context = createInitialRequestContext();
|
|
8
|
+
const contextString = JSON.stringify(context);
|
|
9
|
+
const reconstructedContext = JSON.parse(contextString);
|
|
10
|
+
|
|
11
|
+
const parse = RequestContextSchema.safeParse(reconstructedContext);
|
|
12
|
+
|
|
13
|
+
expect(parse.success).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/examples-node",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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.7",
|
|
8
|
+
"@reactionary/provider-commercetools": "0.3.7",
|
|
9
|
+
"@reactionary/provider-algolia": "0.3.7",
|
|
10
|
+
"@reactionary/provider-medusa": "0.3.7",
|
|
11
|
+
"@reactionary/provider-meilisearch": "0.3.7"
|
|
12
12
|
},
|
|
13
13
|
"type": "module"
|
|
14
14
|
}
|
|
@@ -36,6 +36,17 @@ describe.each([PrimaryProvider.MEDUSA])(
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
expect(result.value.length).toBeGreaterThan(0);
|
|
39
|
+
|
|
40
|
+
expect(result.value[0].recommendationReturnType).toBe('productSearchResultItem');
|
|
41
|
+
if (result.value[0].recommendationReturnType === 'productSearchResultItem') {
|
|
42
|
+
expect(result.value[0].product.identifier.key).toBeDefined();
|
|
43
|
+
expect(result.value[0].product.name).toBeDefined();
|
|
44
|
+
expect(result.value[0].product.slug).toBeDefined();
|
|
45
|
+
expect(result.value[0].product.variants).toBeDefined();
|
|
46
|
+
expect(result.value[0].product.variants.length).toBeGreaterThan(0);
|
|
47
|
+
expect(result.value[0].product.variants[0].variant.sku).toBeDefined();
|
|
48
|
+
expect(result.value[0].product.variants[0].image.sourceUrl).toBeDefined();
|
|
49
|
+
}
|
|
39
50
|
});
|
|
40
51
|
|
|
41
52
|
it('should return an empty result for an unknown collection', async () => {
|
|
@@ -76,7 +87,16 @@ describe.each([PrimaryProvider.MEILISEARCH])(
|
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
expect(result.value.length).toBeGreaterThan(0);
|
|
79
|
-
expect(result.value[0].
|
|
90
|
+
expect(result.value[0].recommendationReturnType).toBe('productSearchResultItem');
|
|
91
|
+
if (result.value[0].recommendationReturnType === 'productSearchResultItem') {
|
|
92
|
+
expect(result.value[0].product.identifier.key).toBeDefined();
|
|
93
|
+
expect(result.value[0].product.name).toBeDefined();
|
|
94
|
+
expect(result.value[0].product.slug).toBeDefined();
|
|
95
|
+
expect(result.value[0].product.variants).toBeDefined();
|
|
96
|
+
expect(result.value[0].product.variants.length).toBeGreaterThan(0);
|
|
97
|
+
expect(result.value[0].product.variants[0].variant.sku).toBeDefined();
|
|
98
|
+
expect(result.value[0].product.variants[0].image.sourceUrl).toBeDefined();
|
|
99
|
+
}
|
|
80
100
|
expect(result.value[0].recommendationIdentifier.key).toBeDefined();
|
|
81
101
|
});
|
|
82
102
|
|
|
@@ -122,6 +142,17 @@ describe.each([PrimaryProvider.ALGOLIA])(
|
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
expect(result.value.length).toBeGreaterThan(0);
|
|
145
|
+
expect(result.value[0].recommendationReturnType).toBe('productSearchResultItem');
|
|
146
|
+
if (result.value[0].recommendationReturnType === 'productSearchResultItem') {
|
|
147
|
+
expect(result.value[0].product.identifier.key).toBeDefined();
|
|
148
|
+
expect(result.value[0].product.name).toBeDefined();
|
|
149
|
+
expect(result.value[0].product.slug).toBeDefined();
|
|
150
|
+
expect(result.value[0].product.variants).toBeDefined();
|
|
151
|
+
expect(result.value[0].product.variants.length).toBeGreaterThan(0);
|
|
152
|
+
expect(result.value[0].product.variants[0].variant.sku).toBeDefined();
|
|
153
|
+
expect(result.value[0].product.variants[0].image.sourceUrl).toBeDefined();
|
|
154
|
+
}
|
|
155
|
+
|
|
125
156
|
});
|
|
126
157
|
|
|
127
158
|
it('should return an empty result for an unknown sku', async () => {
|
package/package.json
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
type ProductRecommendationAlgorithmTrendingInCategoryQuery,
|
|
9
9
|
type RequestContext,
|
|
10
10
|
type ProductRecommendationsQuery,
|
|
11
|
+
type ProductSearchResultItem,
|
|
12
|
+
type ProductSearchResultItemVariant,
|
|
13
|
+
ProductSearchResultItemVariantSchema,
|
|
14
|
+
ImageSchema,
|
|
11
15
|
} from '@reactionary/core';
|
|
12
16
|
import {
|
|
13
17
|
liteClient,
|
|
@@ -21,12 +25,8 @@ import {
|
|
|
21
25
|
} from 'algoliasearch/lite';
|
|
22
26
|
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
23
27
|
import type { AlgoliaProductRecommendationIdentifier } from '../schema/product-recommendation.schema.js';
|
|
28
|
+
import type { AlgoliaNativeRecord, AlgoliaNativeVariant } from '../schema/search.schema.js';
|
|
24
29
|
|
|
25
|
-
interface AlgoliaRecommendHit {
|
|
26
|
-
objectID: string;
|
|
27
|
-
sku?: string;
|
|
28
|
-
[key: string]: unknown;
|
|
29
|
-
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* AlgoliaProductRecommendationsProvider
|
|
@@ -209,7 +209,7 @@ export class AlgoliaProductRecommendationsProvider extends ProductRecommendation
|
|
|
209
209
|
|
|
210
210
|
protected parseRecommendation(res: RecommendationsResults, query: ProductRecommendationsQuery) {
|
|
211
211
|
const result = [];
|
|
212
|
-
for(const hit of res.hits as
|
|
212
|
+
for(const hit of res.hits as AlgoliaNativeRecord[]) {
|
|
213
213
|
const recommendationIdentifier = {
|
|
214
214
|
key: res.queryID || 'x',
|
|
215
215
|
algorithm: query.algorithm,
|
|
@@ -225,12 +225,40 @@ export class AlgoliaProductRecommendationsProvider extends ProductRecommendation
|
|
|
225
225
|
/**
|
|
226
226
|
* Maps Algolia recommendation results to ProductRecommendation format
|
|
227
227
|
*/
|
|
228
|
-
protected parseSingle(hit:
|
|
228
|
+
protected parseSingle(hit: AlgoliaNativeRecord, recommendationIdentifier: AlgoliaProductRecommendationIdentifier): ProductRecommendation {
|
|
229
|
+
|
|
230
|
+
const product = this.parseSearchResultItem(hit);
|
|
231
|
+
|
|
229
232
|
return {
|
|
230
233
|
recommendationIdentifier,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
234
|
+
recommendationReturnType: 'productSearchResultItem',
|
|
235
|
+
product: product,
|
|
234
236
|
} satisfies ProductRecommendation;
|
|
235
237
|
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
protected parseSearchResultItem(body: AlgoliaNativeRecord) {
|
|
241
|
+
const product = {
|
|
242
|
+
identifier: { key: body.objectID },
|
|
243
|
+
name: body.name || body.objectID,
|
|
244
|
+
slug: body.slug || body.objectID,
|
|
245
|
+
variants: [ ... (body.variants || []) ].map(variant => this.parseVariant(variant, body)),
|
|
246
|
+
} satisfies ProductSearchResultItem;
|
|
247
|
+
|
|
248
|
+
return product;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected parseVariant(variant: AlgoliaNativeVariant, product: AlgoliaNativeRecord): ProductSearchResultItemVariant {
|
|
252
|
+
const result = ProductSearchResultItemVariantSchema.parse({
|
|
253
|
+
variant: {
|
|
254
|
+
sku: variant.sku
|
|
255
|
+
},
|
|
256
|
+
image: ImageSchema.parse({
|
|
257
|
+
sourceUrl: variant.image,
|
|
258
|
+
altText: product.name || '',
|
|
259
|
+
})
|
|
260
|
+
} satisfies Partial<ProductSearchResultItemVariant>);
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
236
264
|
}
|
|
@@ -25,19 +25,8 @@ import {
|
|
|
25
25
|
} from '@reactionary/core';
|
|
26
26
|
import { algoliasearch, type SearchResponse } from 'algoliasearch';
|
|
27
27
|
import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
|
|
28
|
-
import type { AlgoliaProductSearchResult } from '../schema/search.schema.js';
|
|
28
|
+
import type { AlgoliaNativeRecord, AlgoliaNativeVariant, AlgoliaProductSearchResult } from '../schema/search.schema.js';
|
|
29
29
|
|
|
30
|
-
interface AlgoliaNativeVariant {
|
|
31
|
-
sku: string;
|
|
32
|
-
image: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface AlgoliaNativeRecord {
|
|
36
|
-
objectID: string;
|
|
37
|
-
slug?:string;
|
|
38
|
-
name?: string;
|
|
39
|
-
variants: Array<AlgoliaNativeVariant>;
|
|
40
|
-
}
|
|
41
30
|
|
|
42
31
|
|
|
43
32
|
export class AlgoliaProductSearchProvider extends ProductSearchProvider {
|
|
@@ -12,3 +12,16 @@ export const AlgoliaProductSearchResultSchema = ProductSearchResultSchema.extend
|
|
|
12
12
|
|
|
13
13
|
export type AlgoliaProductSearchResult = z.infer<typeof AlgoliaProductSearchResultSchema>;
|
|
14
14
|
export type AlgoliaProductSearchIdentifier = z.infer<typeof AlgoliaProductSearchIdentifierSchema>;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export interface AlgoliaNativeVariant {
|
|
18
|
+
sku: string;
|
|
19
|
+
image: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AlgoliaNativeRecord {
|
|
23
|
+
objectID: string;
|
|
24
|
+
slug?:string;
|
|
25
|
+
name?: string;
|
|
26
|
+
variants: Array<AlgoliaNativeVariant>;
|
|
27
|
+
}
|
|
@@ -40,7 +40,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
|
|
|
40
40
|
this.context.session[SESSION_KEY] = {};
|
|
41
41
|
}
|
|
42
42
|
const retVal = this.context.session[SESSION_KEY]
|
|
43
|
-
? this.context.session[SESSION_KEY][this.keyPrefix + '_' + key] || null
|
|
43
|
+
? (this.context.session[SESSION_KEY] as MedusaSession)[this.keyPrefix + '_' + key] || null
|
|
44
44
|
: null;
|
|
45
45
|
if (debug.enabled) {
|
|
46
46
|
debug(
|
|
@@ -49,7 +49,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
|
|
|
49
49
|
}`
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
|
-
return Promise.resolve(retVal);
|
|
52
|
+
return Promise.resolve(retVal as any);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
setItem(key: string, value: string): Promise<void> {
|
|
@@ -63,7 +63,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
|
|
|
63
63
|
} - Value: ${value}`
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
|
-
this.context.session[SESSION_KEY][this.keyPrefix + '_' + key] = value;
|
|
66
|
+
(this.context.session[SESSION_KEY] as MedusaSession)[this.keyPrefix + '_' + key] = value;
|
|
67
67
|
return Promise.resolve();
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -74,7 +74,7 @@ export class RequestContextTokenStore implements MedusaCustomStorage {
|
|
|
74
74
|
if (debug.enabled) {
|
|
75
75
|
debug(`Removing token item for key: ${this.keyPrefix + '_' + key}`);
|
|
76
76
|
}
|
|
77
|
-
delete this.context.session[SESSION_KEY][this.keyPrefix + '_' + key];
|
|
77
|
+
delete (this.context.session[SESSION_KEY] as MedusaSession)[this.keyPrefix + '_' + key];
|
|
78
78
|
return Promise.resolve();
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -220,12 +220,12 @@ export class MedusaAPI {
|
|
|
220
220
|
|
|
221
221
|
public getSessionData(): MedusaSession {
|
|
222
222
|
return this.context.session[SESSION_KEY]
|
|
223
|
-
? this.context.session[SESSION_KEY]
|
|
223
|
+
? this.context.session[SESSION_KEY] as Partial<MedusaSession>
|
|
224
224
|
: MedusaSessionSchema.parse({});
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
public setSessionData(sessionData: Partial<MedusaSession>): void {
|
|
228
|
-
const existingData = this.context.session[SESSION_KEY]
|
|
228
|
+
const existingData = this.context.session[SESSION_KEY] as Partial<MedusaSession>;
|
|
229
229
|
|
|
230
230
|
this.context.session[SESSION_KEY] = {
|
|
231
231
|
...existingData,
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import type { StoreProduct, StoreProductVariant } from '@medusajs/types';
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
+
error,
|
|
4
|
+
ImageSchema,
|
|
3
5
|
ProductRecommendationsProvider,
|
|
6
|
+
ProductSearchResultItemVariantSchema,
|
|
7
|
+
ProductVariantIdentifierSchema,
|
|
8
|
+
success,
|
|
9
|
+
type Cache,
|
|
10
|
+
type NotFoundError,
|
|
4
11
|
type ProductRecommendation,
|
|
5
12
|
type ProductRecommendationsByCollectionQuery,
|
|
13
|
+
type ProductSearchResultItem,
|
|
14
|
+
type ProductSearchResultItemVariant,
|
|
15
|
+
type ProductVariantIdentifier,
|
|
6
16
|
type RequestContext,
|
|
7
|
-
type Result
|
|
8
|
-
success, error,
|
|
9
|
-
NotFoundErrorSchema,
|
|
10
|
-
type NotFoundError,
|
|
11
|
-
errorToString,
|
|
17
|
+
type Result
|
|
12
18
|
} from '@reactionary/core';
|
|
13
19
|
import createDebug from 'debug';
|
|
14
20
|
import type { MedusaAPI } from '../core/client.js';
|
|
@@ -89,15 +95,17 @@ export class MedusaProductRecommendationsProvider extends ProductRecommendations
|
|
|
89
95
|
// Map products to recommendations
|
|
90
96
|
const recommendations: ProductRecommendation[] = [];
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
for (const productRes of productsResponse.products) {
|
|
101
|
+
const product = this.parseSearchResultItem(productRes);
|
|
93
102
|
recommendations.push({
|
|
94
103
|
recommendationIdentifier: {
|
|
95
|
-
key: `${collection.id}_${
|
|
104
|
+
key: `${collection.id}_${productRes.id}`,
|
|
96
105
|
algorithm: 'collection',
|
|
97
106
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
},
|
|
107
|
+
recommendationReturnType: 'productSearchResultItem',
|
|
108
|
+
product: product,
|
|
101
109
|
});
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -114,4 +122,43 @@ export class MedusaProductRecommendationsProvider extends ProductRecommendations
|
|
|
114
122
|
return success([]);
|
|
115
123
|
}
|
|
116
124
|
}
|
|
125
|
+
|
|
126
|
+
protected parseSearchResultItem(_body: StoreProduct) {
|
|
127
|
+
const heroVariant = _body.variants?.[0];
|
|
128
|
+
const identifier = { key: _body.id };
|
|
129
|
+
const slug = _body.handle;
|
|
130
|
+
const name = heroVariant?.title || _body.title;
|
|
131
|
+
const variants = [];
|
|
132
|
+
if (heroVariant) {
|
|
133
|
+
variants.push(this.parseVariant(heroVariant, _body));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = {
|
|
137
|
+
identifier,
|
|
138
|
+
name,
|
|
139
|
+
slug,
|
|
140
|
+
variants,
|
|
141
|
+
} satisfies ProductSearchResultItem;
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected parseVariant(
|
|
147
|
+
variant: StoreProductVariant,
|
|
148
|
+
product: StoreProduct
|
|
149
|
+
): ProductSearchResultItemVariant {
|
|
150
|
+
const img = ImageSchema.parse({
|
|
151
|
+
sourceUrl: product.images?.[0].url ?? '',
|
|
152
|
+
altText: product.title || undefined,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return ProductSearchResultItemVariantSchema.parse({
|
|
156
|
+
variant: ProductVariantIdentifierSchema.parse({
|
|
157
|
+
sku: variant.sku || '',
|
|
158
|
+
} satisfies ProductVariantIdentifier),
|
|
159
|
+
image: img,
|
|
160
|
+
} satisfies Partial<ProductSearchResultItemVariant>);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
117
164
|
}
|
|
@@ -2,18 +2,17 @@ import {
|
|
|
2
2
|
type Cache,
|
|
3
3
|
ProductRecommendationsProvider,
|
|
4
4
|
type ProductRecommendation,
|
|
5
|
-
type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery,
|
|
6
5
|
type ProductRecommendationAlgorithmSimilarProductsQuery,
|
|
7
|
-
type ProductRecommendationAlgorithmRelatedProductsQuery,
|
|
8
|
-
type ProductRecommendationAlgorithmTrendingInCategoryQuery,
|
|
9
6
|
type RequestContext,
|
|
7
|
+
type ProductSearchResultItem,
|
|
8
|
+
ImageSchema,
|
|
9
|
+
type ProductSearchResultItemVariant,
|
|
10
|
+
ProductSearchResultItemVariantSchema,
|
|
10
11
|
} from '@reactionary/core';
|
|
11
12
|
import { MeiliSearch, type Hits, type RecordAny, type SearchParams, type SearchResponse } from 'meilisearch';
|
|
12
13
|
import type { MeilisearchConfiguration } from '../schema/configuration.schema.js';
|
|
14
|
+
import type { MeilisearchNativeRecord, MeilisearchNativeVariant } from '../schema/index.js';
|
|
13
15
|
|
|
14
|
-
interface MeilisearchRecommendHit {
|
|
15
|
-
objectID: string;
|
|
16
|
-
}
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* MeilisearchProductRecommendationsProvider
|
|
@@ -57,7 +56,7 @@ export class MeilisearchProductRecommendationsProvider extends ProductRecommenda
|
|
|
57
56
|
limit: query.numberOfRecommendations,
|
|
58
57
|
};
|
|
59
58
|
|
|
60
|
-
const response = await index.searchSimilarDocuments<
|
|
59
|
+
const response = await index.searchSimilarDocuments<MeilisearchNativeRecord>({
|
|
61
60
|
id: query.sourceProduct.key,
|
|
62
61
|
limit: query.numberOfRecommendations,
|
|
63
62
|
embedder: this.config.useAIEmbedding,
|
|
@@ -75,15 +74,44 @@ export class MeilisearchProductRecommendationsProvider extends ProductRecommenda
|
|
|
75
74
|
/**
|
|
76
75
|
* Maps Meilisearch search results to ProductRecommendation format
|
|
77
76
|
*/
|
|
78
|
-
protected parseRecommendations(recommendation: SearchResponse<
|
|
77
|
+
protected parseRecommendations(recommendation: SearchResponse<MeilisearchNativeRecord>, algorithm: string): ProductRecommendation[] {
|
|
78
|
+
|
|
79
|
+
const product = this.parseSearchResultItem(recommendation.hits[0] as MeilisearchNativeRecord);
|
|
79
80
|
return recommendation.hits.map((hit) => ({
|
|
80
81
|
recommendationIdentifier: {
|
|
81
82
|
key: hit.objectID,
|
|
82
83
|
algorithm,
|
|
83
84
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
85
|
+
recommendationReturnType: 'productSearchResultItem',
|
|
86
|
+
product: product
|
|
87
87
|
}));
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
protected parseSearchResultItem(body: MeilisearchNativeRecord) {
|
|
94
|
+
const product = {
|
|
95
|
+
identifier: { key: body.objectID },
|
|
96
|
+
name: body.name || body.objectID,
|
|
97
|
+
slug: body.slug || body.objectID,
|
|
98
|
+
variants: [...(body.variants || [])].map(variant => this.parseVariant(variant, body)),
|
|
99
|
+
} satisfies ProductSearchResultItem;
|
|
100
|
+
|
|
101
|
+
return product;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected parseVariant(variant: MeilisearchNativeVariant, product: MeilisearchNativeRecord): ProductSearchResultItemVariant {
|
|
105
|
+
const result = ProductSearchResultItemVariantSchema.parse({
|
|
106
|
+
variant: {
|
|
107
|
+
sku: variant.sku
|
|
108
|
+
},
|
|
109
|
+
image: ImageSchema.parse({
|
|
110
|
+
sourceUrl: variant.image,
|
|
111
|
+
altText: product.name || '',
|
|
112
|
+
})
|
|
113
|
+
} satisfies Partial<ProductSearchResultItemVariant>);
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
89
117
|
}
|
|
@@ -25,19 +25,7 @@ import {
|
|
|
25
25
|
} from '@reactionary/core';
|
|
26
26
|
import { MeiliSearch, type SearchParams, type SearchResponse } from 'meilisearch';
|
|
27
27
|
import type { MeilisearchConfiguration } from '../schema/configuration.schema.js';
|
|
28
|
-
import type { MeilisearchProductSearchResult } from '../schema/search.schema.js';
|
|
29
|
-
|
|
30
|
-
interface MeilisearchNativeVariant {
|
|
31
|
-
sku: string;
|
|
32
|
-
image: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface MeilisearchNativeRecord {
|
|
36
|
-
objectID: string;
|
|
37
|
-
slug?: string;
|
|
38
|
-
name?: string;
|
|
39
|
-
variants: Array<MeilisearchNativeVariant>;
|
|
40
|
-
}
|
|
28
|
+
import type { MeilisearchNativeRecord, MeilisearchNativeVariant, MeilisearchProductSearchResult } from '../schema/search.schema.js';
|
|
41
29
|
|
|
42
30
|
|
|
43
31
|
export class MeilisearchSearchProvider extends ProductSearchProvider {
|
|
@@ -12,3 +12,14 @@ export const MeilisearchProductSearchResultSchema = ProductSearchResultSchema.ex
|
|
|
12
12
|
|
|
13
13
|
export type MeilisearchProductSearchResult = z.infer<typeof MeilisearchProductSearchResultSchema>;
|
|
14
14
|
export type MeilisearchProductSearchIdentifier = z.infer<typeof MeilisearchProductSearchIdentifierSchema>;
|
|
15
|
+
export interface MeilisearchNativeVariant {
|
|
16
|
+
sku: string;
|
|
17
|
+
image: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MeilisearchNativeRecord {
|
|
21
|
+
objectID: string;
|
|
22
|
+
slug?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
variants: Array<MeilisearchNativeVariant>;
|
|
25
|
+
}
|