mbd-studio-sdk 2.1.2 → 2.2.0
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/StudioConfig.js +18 -105
- package/V1/Studio.js +17 -126
- package/V1/features/Features.js +8 -123
- package/V1/ranking/Ranking.js +22 -241
- package/V1/scoring/Scoring.js +4 -92
- package/V1/search/Search.js +39 -387
- package/V1/search/filters/ConsoleAccountFilter.js +0 -10
- package/V1/search/filters/CustomFilter.js +0 -9
- package/V1/search/filters/DateFilter.js +1 -13
- package/V1/search/filters/Filter.js +1 -13
- package/V1/search/filters/GeoFilter.js +0 -9
- package/V1/search/filters/GroupBoostFilter.js +0 -14
- package/V1/search/filters/IsNullFilter.js +0 -8
- package/V1/search/filters/MatchFilter.js +0 -9
- package/V1/search/filters/NotNullFilter.js +0 -8
- package/V1/search/filters/NumericFilter.js +0 -10
- package/V1/search/filters/TermFilter.js +0 -9
- package/V1/search/filters/TermsFilter.js +0 -9
- package/V1/search/filters/TermsLookupFilter.js +0 -11
- package/V1/utils/indexUtils.js +0 -5
- package/index.js +0 -1
- package/package.json +1 -1
package/V1/ranking/Ranking.js
CHANGED
|
@@ -1,55 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ranking client for MBD Studio SDK.
|
|
3
|
-
* Fluent API: methods return the instance for chaining.
|
|
4
|
-
* Rank items (candidates) with sort, diversity, and limits-by-field.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
export class Ranking {
|
|
8
|
-
/** @type {string} */
|
|
9
|
-
_url;
|
|
10
|
-
|
|
11
|
-
/** @type {string} */
|
|
12
|
-
_apiKey;
|
|
13
|
-
|
|
14
|
-
/** @type {Array} */
|
|
15
2
|
_candidates = [];
|
|
16
|
-
|
|
17
|
-
/** @type {string} */
|
|
18
3
|
_sortMethod = 'sort';
|
|
19
|
-
|
|
20
|
-
/** @type {{ fields?: string[], direction?: string[] } | Array<{ field: string, weight: number }> | Array<{ field: string, direction: string, percentage: number }> | null} */
|
|
21
4
|
_sortParams = null;
|
|
22
|
-
|
|
23
|
-
/** @type {string | null} */
|
|
24
5
|
_diversityMethod = null;
|
|
25
|
-
|
|
26
|
-
/** @type {{ fields?: string[] } | { lambda?: number, horizon?: number } | null} */
|
|
27
6
|
_diversityParams = null;
|
|
28
|
-
|
|
29
|
-
/** @type {boolean} */
|
|
30
7
|
_limitsByFieldEnabled = false;
|
|
31
|
-
|
|
32
|
-
/** @type {number} */
|
|
33
8
|
_everyN = 10;
|
|
34
|
-
|
|
35
|
-
/** @type {Array<{ field: string, limit: number }>} */
|
|
36
9
|
_limitRules = [];
|
|
37
|
-
|
|
38
|
-
/** @type {{ endpoint: string, payload: object } | null} */
|
|
39
10
|
lastCall = null;
|
|
40
|
-
|
|
41
|
-
/** @type {object | null} */
|
|
42
11
|
lastResult = null;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {Object} options
|
|
46
|
-
* @param {string} options.url - Ranking service base URL
|
|
47
|
-
* @param {string} options.apiKey - API key for authentication
|
|
48
|
-
* @param {Array} [options.candidates] - Items to rank (hits with _id, _features, _scores, _source)
|
|
49
|
-
* @param {string} [options.origin='sdk'] - origin sent in backend call payloads
|
|
50
|
-
* @param {function(string): void} [options.log] - Optional override for log()
|
|
51
|
-
* @param {function(*): void} [options.show] - Optional override for show()
|
|
52
|
-
*/
|
|
53
12
|
constructor(options) {
|
|
54
13
|
if (!options || typeof options !== 'object') {
|
|
55
14
|
throw new Error('Ranking: options object is required');
|
|
@@ -68,23 +27,12 @@ export class Ranking {
|
|
|
68
27
|
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
69
28
|
this._candidates = candidates;
|
|
70
29
|
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Return the endpoint path for the ranking feed.
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
30
|
getEndpoint() {
|
|
77
31
|
return '/ranking/feed';
|
|
78
32
|
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Collect useful field names from sort, diversity, and limits config.
|
|
82
|
-
* @private
|
|
83
|
-
*/
|
|
84
33
|
_getUsefulFieldsAndNeedEmbedding() {
|
|
85
34
|
const useful = new Set();
|
|
86
35
|
let needEmbedding = false;
|
|
87
|
-
|
|
88
36
|
if (this._sortParams) {
|
|
89
37
|
if (this._sortMethod === 'sort' && Array.isArray(this._sortParams.fields)) {
|
|
90
38
|
this._sortParams.fields.forEach((f) => useful.add(f));
|
|
@@ -99,24 +47,15 @@ export class Ranking {
|
|
|
99
47
|
if (this._diversityMethod === 'fields' && this._diversityParams?.fields) {
|
|
100
48
|
this._diversityParams.fields.forEach((f) => useful.add(f));
|
|
101
49
|
}
|
|
102
|
-
if (this._diversityMethod === 'semantic')
|
|
103
|
-
needEmbedding = true;
|
|
104
|
-
}
|
|
50
|
+
if (this._diversityMethod === 'semantic') needEmbedding = true;
|
|
105
51
|
if (this._limitsByFieldEnabled && this._limitRules.length > 0) {
|
|
106
52
|
this._limitRules.forEach((r) => r.field && useful.add(r.field));
|
|
107
53
|
}
|
|
108
|
-
|
|
109
54
|
return { usefulFields: useful, needEmbedding };
|
|
110
55
|
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Build the request payload: items from candidates plus sort, diversity, limits_by_field.
|
|
114
|
-
* @returns {object}
|
|
115
|
-
*/
|
|
116
56
|
getPayload() {
|
|
117
57
|
const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
|
|
118
58
|
const hits = this._candidates || [];
|
|
119
|
-
|
|
120
59
|
const items = hits.map((hit) => {
|
|
121
60
|
const item = { item_id: hit._id };
|
|
122
61
|
for (const key of usefulFields) {
|
|
@@ -125,31 +64,20 @@ export class Ranking {
|
|
|
125
64
|
}
|
|
126
65
|
if (needEmbedding) {
|
|
127
66
|
let embed = hit._source?.item_sem_embed2;
|
|
128
|
-
if (!embed || !Array.isArray(embed))
|
|
129
|
-
embed = hit._source?.text_vector;
|
|
130
|
-
}
|
|
67
|
+
if (!embed || !Array.isArray(embed)) embed = hit._source?.text_vector;
|
|
131
68
|
if (embed) item.embed = embed;
|
|
132
69
|
}
|
|
133
70
|
return item;
|
|
134
71
|
});
|
|
135
|
-
|
|
136
72
|
const payload = { origin: this._origin, items };
|
|
137
|
-
|
|
138
73
|
const sortConfig = this._buildSortConfig();
|
|
139
74
|
if (sortConfig) payload.sort = sortConfig;
|
|
140
|
-
|
|
141
75
|
const diversityConfig = this._buildDiversityConfig();
|
|
142
76
|
if (diversityConfig) payload.diversity = diversityConfig;
|
|
143
|
-
|
|
144
77
|
const limitsByFieldConfig = this._buildLimitsByFieldConfig();
|
|
145
78
|
if (limitsByFieldConfig) payload.limits_by_field = limitsByFieldConfig;
|
|
146
|
-
|
|
147
79
|
return payload;
|
|
148
80
|
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* @private
|
|
152
|
-
*/
|
|
153
81
|
_buildSortConfig() {
|
|
154
82
|
if (!this._sortParams) return undefined;
|
|
155
83
|
if (this._sortMethod === 'sort' && this._sortParams.fields?.length > 0) {
|
|
@@ -159,17 +87,10 @@ export class Ranking {
|
|
|
159
87
|
return { method: 'linear', params: this._sortParams.map((p) => ({ field: p.field, weight: p.weight })) };
|
|
160
88
|
}
|
|
161
89
|
if (this._sortMethod === 'mix' && Array.isArray(this._sortParams) && this._sortParams.length > 0) {
|
|
162
|
-
return {
|
|
163
|
-
method: 'mix',
|
|
164
|
-
params: this._sortParams.map((p) => ({ field: p.field, direction: p.direction, percentage: p.percentage })),
|
|
165
|
-
};
|
|
90
|
+
return { method: 'mix', params: this._sortParams.map((p) => ({ field: p.field, direction: p.direction, percentage: p.percentage })) };
|
|
166
91
|
}
|
|
167
92
|
return undefined;
|
|
168
93
|
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* @private
|
|
172
|
-
*/
|
|
173
94
|
_buildDiversityConfig() {
|
|
174
95
|
if (!this._diversityMethod) return undefined;
|
|
175
96
|
if (this._diversityMethod === 'fields' && this._diversityParams?.fields?.length > 0) {
|
|
@@ -182,25 +103,12 @@ export class Ranking {
|
|
|
182
103
|
}
|
|
183
104
|
return undefined;
|
|
184
105
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* @private
|
|
188
|
-
*/
|
|
189
106
|
_buildLimitsByFieldConfig() {
|
|
190
107
|
if (!this._limitsByFieldEnabled || !this._limitRules.length) return undefined;
|
|
191
108
|
const everyN = Number(this._everyN);
|
|
192
109
|
if (!Number.isInteger(everyN) || everyN < 2) return undefined;
|
|
193
|
-
return {
|
|
194
|
-
every_n: everyN,
|
|
195
|
-
rules: this._limitRules.map((r) => ({ field: r.field, limit: Number(r.limit) || 0 })),
|
|
196
|
-
};
|
|
110
|
+
return { every_n: everyN, rules: this._limitRules.map((r) => ({ field: r.field, limit: Number(r.limit) || 0 })) };
|
|
197
111
|
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Set the sorting method. Applies to subsequent sortBy / weight / mix calls.
|
|
201
|
-
* @param {'sort' | 'linear' | 'mix'} x - Sorting method
|
|
202
|
-
* @returns {this}
|
|
203
|
-
*/
|
|
204
112
|
sortingMethod(x) {
|
|
205
113
|
if (x !== 'sort' && x !== 'linear' && x !== 'mix') {
|
|
206
114
|
throw new Error('Ranking.sortingMethod: must be "sort", "linear", or "mix"');
|
|
@@ -211,31 +119,15 @@ export class Ranking {
|
|
|
211
119
|
if (x === 'mix') this._sortParams = [];
|
|
212
120
|
return this;
|
|
213
121
|
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Set sort by field(s) and direction(s). Applies when sortingMethod is 'sort'.
|
|
217
|
-
* If sorting method was not set yet, it is set to 'sort' for convenience.
|
|
218
|
-
* @param {string} field - Primary sort field
|
|
219
|
-
* @param {'asc' | 'desc'} [direction='desc'] - Primary direction
|
|
220
|
-
* @param {string} [field2] - Optional second sort field
|
|
221
|
-
* @param {'asc' | 'desc'} [direction2='desc'] - Optional second direction
|
|
222
|
-
* @returns {this}
|
|
223
|
-
*/
|
|
224
122
|
sortBy(field, direction = 'desc', field2, direction2 = 'desc') {
|
|
225
123
|
if (this._sortMethod === 'linear' || this._sortMethod === 'mix') {
|
|
226
124
|
throw new Error('Ranking.sortBy: only applies when sortingMethod is "sort" (already set to something else)');
|
|
227
125
|
}
|
|
228
126
|
this._sortMethod = 'sort';
|
|
229
|
-
if (!this._sortParams || !this._sortParams.fields) {
|
|
230
|
-
this._sortParams = { fields: [], direction: [] };
|
|
231
|
-
}
|
|
127
|
+
if (!this._sortParams || !this._sortParams.fields) this._sortParams = { fields: [], direction: [] };
|
|
232
128
|
const f = typeof field === 'string' && field.trim() ? field.trim() : null;
|
|
233
|
-
if (!f)
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
if (direction !== 'asc' && direction !== 'desc') {
|
|
237
|
-
throw new Error('Ranking.sortBy: direction must be "asc" or "desc"');
|
|
238
|
-
}
|
|
129
|
+
if (!f) throw new Error('Ranking.sortBy: field must be a non-empty string');
|
|
130
|
+
if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.sortBy: direction must be "asc" or "desc"');
|
|
239
131
|
this._sortParams = { fields: [f], direction: [direction] };
|
|
240
132
|
if (typeof field2 === 'string' && field2.trim()) {
|
|
241
133
|
this._sortParams.fields.push(field2.trim());
|
|
@@ -243,168 +135,74 @@ export class Ranking {
|
|
|
243
135
|
}
|
|
244
136
|
return this;
|
|
245
137
|
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Add a weighted field for linear combination. Applies when sortingMethod is 'linear'.
|
|
249
|
-
* @param {string} field - Field name
|
|
250
|
-
* @param {number} w - Weight
|
|
251
|
-
* @returns {this}
|
|
252
|
-
*/
|
|
253
138
|
weight(field, w) {
|
|
254
|
-
if (this._sortMethod !== 'linear')
|
|
255
|
-
throw new Error('Ranking.weight: only applies when sortingMethod is "linear"');
|
|
256
|
-
}
|
|
139
|
+
if (this._sortMethod !== 'linear') throw new Error('Ranking.weight: only applies when sortingMethod is "linear"');
|
|
257
140
|
const f = typeof field === 'string' && field.trim() ? field.trim() : null;
|
|
258
|
-
if (!f)
|
|
259
|
-
throw new Error('Ranking.weight: field must be a non-empty string');
|
|
260
|
-
}
|
|
141
|
+
if (!f) throw new Error('Ranking.weight: field must be a non-empty string');
|
|
261
142
|
if (!Array.isArray(this._sortParams)) this._sortParams = [];
|
|
262
143
|
this._sortParams.push({ field: f, weight: Number(w) });
|
|
263
144
|
return this;
|
|
264
145
|
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Add a mix row (field, direction, percentage). Applies when sortingMethod is 'mix'.
|
|
268
|
-
* If sorting method was not set yet, it is set to 'mix' for convenience.
|
|
269
|
-
* @param {string} field - Field name
|
|
270
|
-
* @param {'asc' | 'desc'} direction - Sort direction
|
|
271
|
-
* @param {number} percentage - Percentage weight (0–100)
|
|
272
|
-
* @returns {this}
|
|
273
|
-
*/
|
|
274
146
|
mix(field, direction, percentage) {
|
|
275
|
-
if (this._sortMethod === 'linear')
|
|
276
|
-
throw new Error('Ranking.mix: only applies when sortingMethod is "mix" (already set to "linear")');
|
|
277
|
-
}
|
|
147
|
+
if (this._sortMethod === 'linear') throw new Error('Ranking.mix: only applies when sortingMethod is "mix" (already set to "linear")');
|
|
278
148
|
this._sortMethod = 'mix';
|
|
279
149
|
if (!Array.isArray(this._sortParams)) this._sortParams = [];
|
|
280
150
|
const f = typeof field === 'string' && field.trim() ? field.trim() : null;
|
|
281
|
-
if (!f)
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
if (direction !== 'asc' && direction !== 'desc') {
|
|
285
|
-
throw new Error('Ranking.mix: direction must be "asc" or "desc"');
|
|
286
|
-
}
|
|
151
|
+
if (!f) throw new Error('Ranking.mix: field must be a non-empty string');
|
|
152
|
+
if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.mix: direction must be "asc" or "desc"');
|
|
287
153
|
this._sortParams.push({ field: f, direction, percentage: Number(percentage) || 0 });
|
|
288
154
|
return this;
|
|
289
155
|
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Set the diversity method.
|
|
293
|
-
* @param {'fields' | 'semantic'} method - Diversity method
|
|
294
|
-
* @returns {this}
|
|
295
|
-
*/
|
|
296
156
|
diversity(method) {
|
|
297
|
-
if (method !== 'fields' && method !== 'semantic')
|
|
298
|
-
throw new Error('Ranking.diversity: method must be "fields" or "semantic"');
|
|
299
|
-
}
|
|
157
|
+
if (method !== 'fields' && method !== 'semantic') throw new Error('Ranking.diversity: method must be "fields" or "semantic"');
|
|
300
158
|
this._diversityMethod = method;
|
|
301
159
|
if (method === 'fields') this._diversityParams = { fields: [] };
|
|
302
160
|
if (method === 'semantic') this._diversityParams = { lambda: 0.5, horizon: 20 };
|
|
303
161
|
return this;
|
|
304
162
|
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Add field(s) for diversity when diversityMethod is 'fields'. Pass array or single field name.
|
|
308
|
-
* @param {string[] | string} arrayOrItem - Field name(s)
|
|
309
|
-
* @returns {this}
|
|
310
|
-
*/
|
|
311
163
|
fields(arrayOrItem) {
|
|
312
|
-
if (this._diversityMethod !== 'fields')
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
if (!this._diversityParams || !Array.isArray(this._diversityParams.fields)) {
|
|
316
|
-
this._diversityParams = { fields: [] };
|
|
317
|
-
}
|
|
164
|
+
if (this._diversityMethod !== 'fields') throw new Error('Ranking.fields: only applies when diversity(method) is "fields"');
|
|
165
|
+
if (!this._diversityParams || !Array.isArray(this._diversityParams.fields)) this._diversityParams = { fields: [] };
|
|
318
166
|
const add = (name) => {
|
|
319
167
|
const s = typeof name === 'string' && name.trim() ? name.trim() : null;
|
|
320
168
|
if (s && !this._diversityParams.fields.includes(s)) this._diversityParams.fields.push(s);
|
|
321
169
|
};
|
|
322
|
-
if (Array.isArray(arrayOrItem))
|
|
323
|
-
|
|
324
|
-
} else {
|
|
325
|
-
add(arrayOrItem);
|
|
326
|
-
}
|
|
170
|
+
if (Array.isArray(arrayOrItem)) arrayOrItem.forEach(add);
|
|
171
|
+
else add(arrayOrItem);
|
|
327
172
|
return this;
|
|
328
173
|
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Set horizon for diversity method 'semantic'. Default 20.
|
|
332
|
-
* @param {number} n - Horizon value
|
|
333
|
-
* @returns {this}
|
|
334
|
-
*/
|
|
335
174
|
horizon(n) {
|
|
336
|
-
if (this._diversityMethod !== 'semantic')
|
|
337
|
-
throw new Error('Ranking.horizon: only applies when diversity(method) is "semantic"');
|
|
338
|
-
}
|
|
175
|
+
if (this._diversityMethod !== 'semantic') throw new Error('Ranking.horizon: only applies when diversity(method) is "semantic"');
|
|
339
176
|
if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 };
|
|
340
177
|
this._diversityParams.horizon = Number(n);
|
|
341
178
|
return this;
|
|
342
179
|
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Set lambda for diversity method 'semantic'. Default 0.5.
|
|
346
|
-
* @param {number} value - Lambda value (0–1)
|
|
347
|
-
* @returns {this}
|
|
348
|
-
*/
|
|
349
180
|
lambda(value) {
|
|
350
|
-
if (this._diversityMethod !== 'semantic')
|
|
351
|
-
throw new Error('Ranking.lambda: only applies when diversity(method) is "semantic"');
|
|
352
|
-
}
|
|
181
|
+
if (this._diversityMethod !== 'semantic') throw new Error('Ranking.lambda: only applies when diversity(method) is "semantic"');
|
|
353
182
|
if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 };
|
|
354
183
|
this._diversityParams.lambda = Number(value);
|
|
355
184
|
return this;
|
|
356
185
|
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Enable limits-by-field. Use every(n) and limit(field, max) to configure.
|
|
360
|
-
* @returns {this}
|
|
361
|
-
*/
|
|
362
186
|
limitByField() {
|
|
363
187
|
this._limitsByFieldEnabled = true;
|
|
364
188
|
return this;
|
|
365
189
|
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Set window size (every_n) for limits by field. Default 10.
|
|
369
|
-
* @param {number} n - Window size (e.g. every 10 items)
|
|
370
|
-
* @returns {this}
|
|
371
|
-
*/
|
|
372
190
|
every(n) {
|
|
373
191
|
this._everyN = Number(n);
|
|
374
192
|
return this;
|
|
375
193
|
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Add a limit rule: max occurrences of field value per window.
|
|
379
|
-
* @param {string} field - Field name
|
|
380
|
-
* @param {number} max - Maximum count per window
|
|
381
|
-
* @returns {this}
|
|
382
|
-
*/
|
|
383
194
|
limit(field, max) {
|
|
384
195
|
const f = typeof field === 'string' && field.trim() ? field.trim() : null;
|
|
385
|
-
if (!f)
|
|
386
|
-
throw new Error('Ranking.limit: field must be a non-empty string');
|
|
387
|
-
}
|
|
196
|
+
if (!f) throw new Error('Ranking.limit: field must be a non-empty string');
|
|
388
197
|
const existing = this._limitRules.find((r) => r.field === f);
|
|
389
198
|
if (existing) existing.limit = Number(max) || 0;
|
|
390
199
|
else this._limitRules.push({ field: f, limit: Number(max) || 0 });
|
|
391
200
|
return this;
|
|
392
201
|
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Set the candidates (hits) to rank. Each should have _id, and optionally _features, _scores, _source.
|
|
396
|
-
* @param {Array} candidates - Array of hit objects
|
|
397
|
-
* @returns {this}
|
|
398
|
-
*/
|
|
399
202
|
candidates(candidates) {
|
|
400
203
|
this._candidates = candidates;
|
|
401
204
|
return this;
|
|
402
205
|
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Execute the ranking request. Sets lastCall and lastResult on success; throws on error.
|
|
406
|
-
* @returns {Promise<object>} The result object (result.result.items with item_id and score)
|
|
407
|
-
*/
|
|
408
206
|
async execute() {
|
|
409
207
|
if (!Array.isArray(this._candidates) || this._candidates.length === 0) {
|
|
410
208
|
throw new Error('Ranking.execute: candidates must be set and non-empty (pass to constructor or call candidates([...]))');
|
|
@@ -423,10 +221,7 @@ export class Ranking {
|
|
|
423
221
|
const startTime = performance.now();
|
|
424
222
|
const response = await fetch(url, {
|
|
425
223
|
method: 'POST',
|
|
426
|
-
headers: {
|
|
427
|
-
'Content-Type': 'application/json',
|
|
428
|
-
Authorization: `Bearer ${this._apiKey}`,
|
|
429
|
-
},
|
|
224
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` },
|
|
430
225
|
body: JSON.stringify(payload),
|
|
431
226
|
});
|
|
432
227
|
const frontendTime = Math.round(performance.now() - startTime);
|
|
@@ -446,31 +241,17 @@ export class Ranking {
|
|
|
446
241
|
result.took_frontend = frontendTime;
|
|
447
242
|
this.lastCall = { endpoint, payload };
|
|
448
243
|
this.lastResult = result;
|
|
449
|
-
|
|
450
244
|
const res = result.result;
|
|
451
|
-
if (!res)
|
|
452
|
-
throw new Error('Ranking.execute: response result is undefined');
|
|
453
|
-
}
|
|
454
|
-
|
|
245
|
+
if (!res) throw new Error('Ranking.execute: response result is undefined');
|
|
455
246
|
const resultItems = res.items || [];
|
|
456
247
|
this._log('Ranking result:');
|
|
457
248
|
this._log(` took_frontend_ms: ${result.took_frontend}`);
|
|
458
249
|
this._log(` items: ${resultItems.length}`);
|
|
459
250
|
return res;
|
|
460
251
|
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Log a string.
|
|
464
|
-
* @param {string} string
|
|
465
|
-
*/
|
|
466
252
|
log(string) {
|
|
467
253
|
this._log(string);
|
|
468
254
|
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Show results.
|
|
472
|
-
* @param {*} results
|
|
473
|
-
*/
|
|
474
255
|
show(results) {
|
|
475
256
|
this._show(results);
|
|
476
257
|
}
|
package/V1/scoring/Scoring.js
CHANGED
|
@@ -1,41 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scoring client for MBD Studio SDK.
|
|
3
|
-
* Fluent API: methods return the instance for chaining.
|
|
4
|
-
* Score or rerank items for a given user using a selected model.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
export class Scoring {
|
|
8
|
-
/** @type {string} */
|
|
9
|
-
_url;
|
|
10
|
-
|
|
11
|
-
/** @type {string} */
|
|
12
|
-
_apiKey;
|
|
13
|
-
|
|
14
|
-
/** @type {string | null} */
|
|
15
2
|
_userId = null;
|
|
16
|
-
|
|
17
|
-
/** @type {string[]} */
|
|
18
3
|
_itemIds = [];
|
|
19
|
-
|
|
20
|
-
/** @type {string | null} */
|
|
21
4
|
_modelEndpoint = null;
|
|
22
|
-
|
|
23
|
-
/** @type {{ endpoint: string, payload: object } | null} */
|
|
24
5
|
lastCall = null;
|
|
25
|
-
|
|
26
|
-
/** @type {object | null} */
|
|
27
6
|
lastResult = null;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {Object} options
|
|
31
|
-
* @param {string} options.url - Scoring service base URL
|
|
32
|
-
* @param {string} options.apiKey - API key for authentication
|
|
33
|
-
* @param {string} [options.userId] - User id for scoring context
|
|
34
|
-
* @param {string[]} [options.itemIds] - Item ids to score/rerank
|
|
35
|
-
* @param {string} [options.origin='sdk'] - origin sent in backend call payloads
|
|
36
|
-
* @param {function(string): void} [options.log] - Optional override for log()
|
|
37
|
-
* @param {function(*): void} [options.show] - Optional override for show()
|
|
38
|
-
*/
|
|
39
7
|
constructor(options) {
|
|
40
8
|
if (!options || typeof options !== 'object') {
|
|
41
9
|
throw new Error('Scoring: options object is required');
|
|
@@ -52,71 +20,32 @@ export class Scoring {
|
|
|
52
20
|
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
53
21
|
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
54
22
|
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
55
|
-
if (userId != null && typeof userId === 'string' && userId.trim())
|
|
56
|
-
this._userId = userId.trim();
|
|
57
|
-
}
|
|
23
|
+
if (userId != null && typeof userId === 'string' && userId.trim()) this._userId = userId.trim();
|
|
58
24
|
if (Array.isArray(itemIds) && itemIds.length > 0) {
|
|
59
25
|
this._itemIds = itemIds.map((id) => (typeof id === 'string' ? id : String(id)));
|
|
60
26
|
}
|
|
61
27
|
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Return the endpoint path for the current model (must be set via model(endpoint)).
|
|
65
|
-
* @returns {string}
|
|
66
|
-
*/
|
|
67
28
|
getEndpoint() {
|
|
68
29
|
if (!this._modelEndpoint || !this._modelEndpoint.trim()) {
|
|
69
30
|
throw new Error('Scoring.getEndpoint: model endpoint must be set (call model(endpoint) first)');
|
|
70
31
|
}
|
|
71
32
|
return this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`;
|
|
72
33
|
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build the request payload (origin + user_id + item_ids).
|
|
76
|
-
* @returns {object}
|
|
77
|
-
*/
|
|
78
34
|
getPayload() {
|
|
79
|
-
return {
|
|
80
|
-
origin: this._origin,
|
|
81
|
-
user_id: this._userId,
|
|
82
|
-
item_ids: [...this._itemIds],
|
|
83
|
-
};
|
|
35
|
+
return { origin: this._origin, user_id: this._userId, item_ids: [...this._itemIds] };
|
|
84
36
|
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Set the model endpoint for scoring/reranking (required).
|
|
88
|
-
* @param {string} endpoint - Endpoint path (e.g. '/scoring/ranking_model/polymarket-rerank-v1')
|
|
89
|
-
* @returns {this}
|
|
90
|
-
*/
|
|
91
37
|
model(endpoint) {
|
|
92
38
|
this._modelEndpoint = endpoint;
|
|
93
39
|
return this;
|
|
94
40
|
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Set the user id for scoring context.
|
|
98
|
-
* @param {string} userId - User identifier
|
|
99
|
-
* @returns {this}
|
|
100
|
-
*/
|
|
101
41
|
userId(userId) {
|
|
102
42
|
this._userId = userId;
|
|
103
43
|
return this;
|
|
104
44
|
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Set the item ids to score/rerank.
|
|
108
|
-
* @param {string[]} itemIds - Array of item id strings
|
|
109
|
-
* @returns {this}
|
|
110
|
-
*/
|
|
111
45
|
itemIds(itemIds) {
|
|
112
46
|
this._itemIds = itemIds;
|
|
113
47
|
return this;
|
|
114
48
|
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Execute the scoring request. Sets lastCall and lastResult on success; throws on error.
|
|
118
|
-
* @returns {Promise<object>} The result object (e.g. result.result with personalizedRanking, etc.)
|
|
119
|
-
*/
|
|
120
49
|
async execute() {
|
|
121
50
|
if (!this._modelEndpoint || !this._modelEndpoint.trim()) {
|
|
122
51
|
throw new Error('Scoring.execute: model endpoint must be set (call model(endpoint) first)');
|
|
@@ -134,10 +63,7 @@ export class Scoring {
|
|
|
134
63
|
const startTime = performance.now();
|
|
135
64
|
const response = await fetch(url, {
|
|
136
65
|
method: 'POST',
|
|
137
|
-
headers: {
|
|
138
|
-
'Content-Type': 'application/json',
|
|
139
|
-
Authorization: `Bearer ${this._apiKey}`,
|
|
140
|
-
},
|
|
66
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` },
|
|
141
67
|
body: JSON.stringify(payload),
|
|
142
68
|
});
|
|
143
69
|
const frontendTime = Math.round(performance.now() - startTime);
|
|
@@ -157,30 +83,16 @@ export class Scoring {
|
|
|
157
83
|
result.took_frontend = frontendTime;
|
|
158
84
|
this.lastCall = { endpoint, payload };
|
|
159
85
|
this.lastResult = result;
|
|
160
|
-
|
|
161
86
|
const res = result.result;
|
|
162
|
-
if (res === undefined)
|
|
163
|
-
throw new Error('Scoring.execute: result.result is undefined');
|
|
164
|
-
}
|
|
165
|
-
|
|
87
|
+
if (res === undefined) throw new Error('Scoring.execute: result.result is undefined');
|
|
166
88
|
this._log('Scoring result:');
|
|
167
89
|
this._log(` took_frontend_ms: ${result.took_frontend}`);
|
|
168
90
|
this._log(` Array length: ${res.length}`);
|
|
169
91
|
return res;
|
|
170
92
|
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Log a string.
|
|
174
|
-
* @param {string} string
|
|
175
|
-
*/
|
|
176
93
|
log(string) {
|
|
177
94
|
this._log(string);
|
|
178
95
|
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Show results.
|
|
182
|
-
* @param {*} results
|
|
183
|
-
*/
|
|
184
96
|
show(results) {
|
|
185
97
|
this._show(results);
|
|
186
98
|
}
|