mbd-studio-sdk 4.1.0 → 4.1.2

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/V1/Studio.js CHANGED
@@ -158,6 +158,26 @@ export class Studio {
158
158
  }
159
159
  this._candidates.sort((a, b) => (b._ranking_score ?? -Infinity) - (a._ranking_score ?? -Infinity));
160
160
  }
161
+ getFeed() {
162
+ return this._candidates;
163
+ }
164
+ dropFeatures() {
165
+ for (const c of this._candidates) {
166
+ delete c._features;
167
+ }
168
+ }
169
+ dropVectors() {
170
+ const keys = ['text_vector', 'item_sem_embed', 'item_sem_embed2'];
171
+ for (const c of this._candidates) {
172
+ const src = c._source;
173
+ if (src && typeof src === 'object') {
174
+ for (const k of keys) delete src[k];
175
+ }
176
+ }
177
+ }
178
+ transform(fn) {
179
+ this._candidates = this._candidates.map(fn);
180
+ }
161
181
  log(string) {
162
182
  this._log(string);
163
183
  }
@@ -165,7 +185,4 @@ export class Studio {
165
185
  if (results === undefined) results = this._candidates;
166
186
  this._show(results);
167
187
  }
168
- getFeed() {
169
- return this._candidates;
170
- }
171
188
  }
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Ensures the value is an array. Backend expects array for terms/match filters.
3
+ * - Arrays are returned as-is
4
+ * - Strings are converted: comma-separated strings are split; single values wrapped in array
5
+ */
6
+ export function normalizeToArray(value) {
7
+ if (Array.isArray(value)) return value;
8
+ if (typeof value === 'string') {
9
+ return value.includes(',')
10
+ ? value.split(',').map((s) => s.trim()).filter(Boolean)
11
+ : [value];
12
+ }
13
+ return value != null ? [value] : [];
14
+ }
15
+
1
16
  export class Filter {
2
17
  constructor(filterType, field, boost = null) {
3
18
  if (new.target === Filter) throw new Error('Filter is abstract and cannot be instantiated directly');
@@ -1,7 +1,7 @@
1
- import { Filter } from './Filter.js';
1
+ import { Filter, normalizeToArray } from './Filter.js';
2
2
  export class MatchFilter extends Filter {
3
3
  constructor(field, value, boost = null) {
4
4
  super('match', field, boost);
5
- this.value = value;
5
+ this.value = normalizeToArray(value);
6
6
  }
7
7
  }
@@ -1,7 +1,7 @@
1
- import { Filter } from './Filter.js';
1
+ import { Filter, normalizeToArray } from './Filter.js';
2
2
  export class TermsFilter extends Filter {
3
3
  constructor(field, value, boost = null) {
4
4
  super('terms', field, boost);
5
- this.value = value;
5
+ this.value = normalizeToArray(value);
6
6
  }
7
7
  }
package/index.d.ts CHANGED
@@ -52,10 +52,13 @@ export interface SearchResult {
52
52
  max_score?: number;
53
53
  }
54
54
 
55
- export interface FrequentValuesResult {
56
- [key: string]: unknown;
55
+ export interface FrequentValueItem {
56
+ id: string | number;
57
+ count: number;
57
58
  }
58
59
 
60
+ export type FrequentValuesResult = FrequentValueItem[];
61
+
59
62
  export class Search {
60
63
  lastCall: { endpoint: string; payload: unknown } | null;
61
64
  lastResult: unknown;
package/llms.txt CHANGED
@@ -1,37 +1,42 @@
1
- # MBD Studio SDK – AI Agent Reference
1
+ # MBD Studio SDK – Reference for AI Coding Agents
2
2
 
3
- ## Introduction
3
+ ## Overview
4
4
 
5
- **What it is:** SDK for MBD Studio backend services search, features, scoring, and ranking for personalized feeds (Polymarket, Farcaster, Zora, etc.).
5
+ SDK for MBD Studio backend services: **search** (candidate retrieval), **features** (signals/metadata), **scoring** (ML models for relevance/reranking), and **ranking** (final ranked feed with diversity).
6
6
 
7
- **Entry points:**
8
- - `import { StudioConfig, StudioV1 } from 'mbd-studio-sdk'`
9
- - Main client: `new StudioV1({ config })` where `config = new StudioConfig({ apiKey })`
7
+ ## Entry Points
10
8
 
11
- **Typical flow:**
12
- 1. `forUser(index, userId)` set user for personalization
13
- 2. `search().index(name).include()/exclude()/boost().term()|numeric()|...execute()` — get candidates
14
- 3. `addCandidates(hits)` — attach to context
15
- 4. `features("v1").execute()` → `addFeatures(result)` — enrich with signals
16
- 5. `scoring().model("/scoring/...").execute()` → `addScores(result, key)` — ML reranking
17
- 6. `ranking().sortingMethod().mix()|sortBy().diversity().execute()` → `addRanking(result)` — final ranked list
18
- 7. `getFeed()` — get sorted candidates
9
+ - `StudioConfig` – API config (apiKey required; commonUrl or servicesUrl for endpoints)
10
+ - `StudioV1` – Main client. Import: `import { StudioConfig, StudioV1 } from 'mbd-studio-sdk'`
19
11
 
20
- **Search endpoint selection (auto):** es_query > semantic (text/vector) > boost > filter_and_sort. Call `include()`/`exclude()`/`boost()` before adding filters.
12
+ ## Typical Flow
21
13
 
22
- **Debug tips:** All services have `lastCall` and `lastResult`. Use `config.log`/`config.show` or pass `log`/`show` in options. Features require `forUser()` and `addCandidates()` first. Ranking needs `addFeatures()` or `addScores()` for sort fields. Semantic diversity needs `includeVectors(true)` in search.
14
+ 1. `new StudioConfig({ apiKey })` `new StudioV1({ config })`
15
+ 2. `mbd.forUser(index, userId)` – set user for personalization (optional)
16
+ 3. `mbd.search().index("...").include()/exclude()/boost().term(...).execute()` → hits
17
+ 4. `mbd.addCandidates(hits)` – attach hits to context
18
+ 5. `mbd.features("v1").execute()` → `mbd.addFeatures(result)` – enrich with signals
19
+ 6. `mbd.scoring().model("/scoring/...").execute()` → `mbd.addScores(result, "key")` – ML scores
20
+ 7. `mbd.ranking().sortingMethod(...).mix(...).execute()` → `mbd.addRanking(result)`
21
+ 8. `mbd.getFeed()` – final ranked candidates
22
+
23
+ **Tip:** Search endpoints are chosen by payload: `es_query` > `semantic` (text/vector) > `boost` > `filter_and_sort`. Call `include()`/`exclude()`/`boost()` before adding filters. `findIndex()` maps index names (e.g. `farcaster-items-v2`) to canonical keys for feature/score lookups.
23
24
 
24
25
  ---
25
26
 
26
27
  ## index.js
28
+
29
+ ```javascript
27
30
  import * as V1 from './V1/index.js';
28
31
  export { StudioConfig } from './StudioConfig.js';
29
32
  export const StudioV1 = V1.Studio;
33
+ ```
30
34
 
31
- ## V1/index.js
32
- export { Studio } from './Studio.js';
35
+ ---
33
36
 
34
37
  ## StudioConfig.js
38
+
39
+ ```javascript
35
40
  const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
36
41
  export class StudioConfig {
37
42
  constructor(options) {
@@ -47,11 +52,8 @@ export class StudioConfig {
47
52
  const services = { searchService, storiesService, featuresService, scoringService, rankingService };
48
53
  const missing = Object.entries(services).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k);
49
54
  if (missing.length > 0) throw new Error(`StudioConfig: when using servicesUrl, all service URLs are required. Missing: ${missing.join(', ')}`);
50
- this.searchService = searchService.trim().replace(/\/$/, '');
51
- this.storiesService = storiesService.trim().replace(/\/$/, '');
52
- this.featuresService = featuresService.trim().replace(/\/$/, '');
53
- this.scoringService = scoringService.trim().replace(/\/$/, '');
54
- this.rankingService = rankingService.trim().replace(/\/$/, '');
55
+ [this.searchService, this.storiesService, this.featuresService, this.scoringService, this.rankingService] =
56
+ [searchService, storiesService, featuresService, scoringService, rankingService].map((u) => u.trim().replace(/\/$/, ''));
55
57
  } else {
56
58
  this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = defaultBaseUrl;
57
59
  }
@@ -60,8 +62,34 @@ export class StudioConfig {
60
62
  this.show = typeof show === 'function' ? show : console.log.bind(console);
61
63
  }
62
64
  }
65
+ ```
66
+
67
+ ---
68
+
69
+ ## V1/index.js
70
+
71
+ ```javascript
72
+ export { Studio } from './Studio.js';
73
+ ```
74
+
75
+ ---
76
+
77
+ ## V1/utils/indexUtils.js
78
+
79
+ ```javascript
80
+ // Maps index names to canonical base (e.g. farcaster-items-v2 → farcaster-items). Used for feature/score keys.
81
+ export function findIndex(index) {
82
+ const indexOptions = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets'];
83
+ for (const option of indexOptions) if (index.startsWith(option)) return option;
84
+ return null;
85
+ }
86
+ ```
87
+
88
+ ---
63
89
 
64
90
  ## V1/Studio.js
91
+
92
+ ```javascript
65
93
  import { StudioConfig } from '../StudioConfig.js';
66
94
  import { Search } from './search/Search.js';
67
95
  import { Features, sortAvailableFeatures } from './features/Features.js';
@@ -86,7 +114,8 @@ export class Studio {
86
114
  async frequentValues(index, field, size = 25) { return this.search().index(index).frequentValues(field, size); }
87
115
  addCandidates(array) { this._candidates.push(...array); }
88
116
  features(version = 'v1') {
89
- const items = (this._candidates && this._candidates.length > 0) ? this._candidates.map((hit) => ({ index: hit._index, id: hit._id })) : [];
117
+ let items = [];
118
+ if (this._candidates?.length > 0) items = this._candidates.map((hit) => ({ index: hit._index, id: hit._id }));
90
119
  return new Features({ url: this._config.featuresService, apiKey: this._config.apiKey, log: this._log, show: this._show, version, items, userIndex: this._forUser?.index, userId: this._forUser?.id, origin: this._origin });
91
120
  }
92
121
  addFeatures(featuresResult) {
@@ -100,33 +129,28 @@ export class Studio {
100
129
  hit._features = hit._features ? { ...hit._features, ...hitFeatures } : hitFeatures;
101
130
  hit._scores = hit._scores ? { ...hit._scores, ...hitScores } : hitScores;
102
131
  hit._info = hit._info ? { ...hit._info, ...hitInfo } : hitInfo;
103
- if (hit._features) for (const [k, v] of Object.entries(hit._features)) if (typeof v === 'number' && !Number.isNaN(v)) availableFeatures[k] = true;
104
- if (hit._scores) for (const [k, v] of Object.entries(hit._scores)) if (typeof v === 'number' && !Number.isNaN(v)) availableScores[k] = true;
132
+ if (hit._features) for (const [key, value] of Object.entries(hit._features)) if (typeof value === 'number' && !Number.isNaN(value)) availableFeatures[key] = true;
133
+ if (hit._scores) for (const [key, value] of Object.entries(hit._scores)) if (typeof value === 'number' && !Number.isNaN(value)) availableScores[key] = true;
105
134
  }
106
135
  this._log(`Available features: ${sortAvailableFeatures(Object.keys(availableFeatures))}`);
107
136
  this._log(`Available scores: ${sortAvailableFeatures(Object.keys(availableScores))}`);
108
137
  }
109
138
  scoring() {
110
139
  const userId = this._forUser?.id ?? null;
111
- const itemIds = Array.isArray(this._candidates) && this._candidates.length > 0 ? this._candidates.map((c) => c && c._id != null ? String(c._id) : null).filter(Boolean) : [];
140
+ const itemIds = Array.isArray(this._candidates) && this._candidates.length > 0 ? this._candidates.map((c) => (c?._id != null ? String(c._id) : null)).filter(Boolean) : [];
112
141
  return new Scoring({ url: this._config.scoringService, apiKey: this._config.apiKey, log: this._log, show: this._show, userId, itemIds, origin: this._origin });
113
142
  }
114
143
  addScores(scoringResult, scoringKey) {
115
144
  const rankedItemIds = scoringResult;
116
- if (!this._candidates || !rankedItemIds || !Array.isArray(rankedItemIds)) return;
117
- const rankToScore = {};
118
- rankedItemIds.forEach((itemId, index) => { rankToScore[itemId] = 1.0 - (index / rankedItemIds.length); });
119
- for (const hit of this._candidates) {
120
- const hitScore = rankToScore[hit._id];
121
- if (hitScore) { if (!hit._scores) hit._scores = {}; hit._scores[scoringKey] = hitScore; }
122
- }
145
+ if (!this._candidates || !Array.isArray(rankedItemIds)) return;
146
+ const rankToScore = {}; rankedItemIds.forEach((itemId, i) => { rankToScore[itemId] = 1.0 - (i / rankedItemIds.length); });
147
+ for (const hit of this._candidates) if (rankToScore[hit._id] != null) { if (!hit._scores) hit._scores = {}; hit._scores[scoringKey] = rankToScore[hit._id]; }
123
148
  }
124
149
  ranking() { return new Ranking({ url: this._config.rankingService, apiKey: this._config.apiKey, log: this._log, show: this._show, candidates: this._candidates, origin: this._origin }); }
125
150
  addRanking(rankingResult) {
126
151
  const rankedItems = rankingResult?.items;
127
- if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) return;
128
- const scoreByItemId = {};
129
- rankedItems.forEach(({ item_id, score }) => { scoreByItemId[item_id] = score; });
152
+ if (!this._candidates || !Array.isArray(rankedItems)) return;
153
+ const scoreByItemId = {}; rankedItems.forEach(({ item_id, score }) => { scoreByItemId[item_id] = score; });
130
154
  for (const hit of this._candidates) hit._ranking_score = scoreByItemId[hit._id];
131
155
  this._candidates.sort((a, b) => (b._ranking_score ?? -Infinity) - (a._ranking_score ?? -Infinity));
132
156
  }
@@ -134,21 +158,19 @@ export class Studio {
134
158
  show(results) { this._show(results === undefined ? this._candidates : results); }
135
159
  getFeed() { return this._candidates; }
136
160
  }
161
+ ```
137
162
 
138
- ## index.d.ts (types summary)
139
- // StudioConfig: apiKey, commonUrl?, servicesUrl?, log?, show?
140
- // SearchHit: _index, _id, _source, _features, _scores, _info, _ranking_score
141
- // Search: index, size, onlyIds, includeVectors, selectFields, text, vector, esQuery, sortBy, include/exclude/boost, term/terms/numeric/date/geo/match/isNull/notNull/custom, groupBoost, termsLookup, consoleAccount, execute, frequentValues, lookup
142
- // Features: version, items, user, execute
143
- // Scoring: model, userId, itemIds, execute
144
- // Ranking: sortingMethod(sort|linear|mix), sortBy, weight, mix, diversity(fields|semantic), fields, horizon, lambda, limitByField, every, limit, candidates, execute
163
+ ---
145
164
 
146
165
  ## V1/search/Search.js
166
+
167
+ ```javascript
147
168
  import { Filter, TermFilter, TermsFilter, NumericFilter, MatchFilter, GeoFilter, DateFilter, IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter } from './filters/index.js';
148
169
 
149
170
  export class Search {
150
- _index = null; _es_query = null; _size = 100; _only_ids = false; _include_vector = false; _select_fields = null; _text = null; _vector = null; _sort_by = null;
151
- _include = []; _exclude = []; _boost = []; _active_array = null; lastCall = null; lastResult = null;
171
+ _index = null; _es_query = null; _size = 100; _only_ids = false; _include_vector = false; _select_fields = null;
172
+ _text = null; _vector = null; _sort_by = null; _include = []; _exclude = []; _boost = [];
173
+ _active_array = null; lastCall = null; lastResult = null;
152
174
  constructor(options) {
153
175
  if (!options || typeof options !== 'object') throw new Error('Search: options object is required');
154
176
  const { url, apiKey, origin = 'sdk', log, show } = options;
@@ -174,7 +196,10 @@ export class Search {
174
196
  const payload = { index: this._index, origin: this._origin, feed_type: feedType, include_vector: this._include_vector, size: this._size, include: serializeFilters(this._include), exclude: serializeFilters(this._exclude) };
175
197
  if (feedType === 'boost') payload.boost = serializeFilters(this._boost);
176
198
  if (feedType === 'filter_and_sort' && this._sort_by) payload.sort_by = this._sort_by;
177
- if (feedType === 'semantic') { if (typeof this._text === 'string' && this._text.length > 0) payload.text = this._text; if (Array.isArray(this._vector) && this._vector.length > 0) payload.vector = this._vector; }
199
+ if (feedType === 'semantic') {
200
+ if (typeof this._text === 'string' && this._text.length > 0) payload.text = this._text;
201
+ if (Array.isArray(this._vector) && this._vector.length > 0) payload.vector = this._vector;
202
+ }
178
203
  if (this._only_ids) payload.only_ids = true;
179
204
  if (Array.isArray(this._select_fields) && this._select_fields.length > 0) payload.select_fields = this._select_fields;
180
205
  return payload;
@@ -184,40 +209,41 @@ export class Search {
184
209
  if (this._es_query != null && (typeof this._es_query !== 'object' || Array.isArray(this._es_query))) throw new Error('Search.execute: esQuery() must be called with a plain object');
185
210
  const hasOnlyIds = this._only_ids === true, hasSelectFields = Array.isArray(this._select_fields) && this._select_fields.length > 0, hasIncludeVector = this._include_vector === true;
186
211
  if ((hasOnlyIds ? 1 : 0) + (hasSelectFields ? 1 : 0) + (hasIncludeVector ? 1 : 0) > 1) throw new Error('Search: onlyIds, selectFields, includeVectors are mutually exclusive');
187
- if (this.getEndpoint() === '/search/es_query' && (this._include.length || this._exclude.length || this._boost.length)) throw new Error('Search: esQuery does not support include/exclude/boost');
188
- if (this.getEndpoint() === '/search/semantic' && (this._include.length || this._exclude.length || this._boost.length)) throw new Error('Search: semantic does not support include/exclude/boost');
189
- if (this.getEndpoint() === '/search/semantic' && this._sort_by) throw new Error('Search: semantic does not support sortBy');
190
- if (this.getEndpoint() === '/search/boost' && this._sort_by) throw new Error('Search: boost does not support sortBy');
191
- const endpoint = this.getEndpoint(), payload = this.getPayload(), url = `${this._url}${endpoint}`;
212
+ const endpoint = this.getEndpoint();
213
+ if (endpoint === '/search/es_query' && (this._include.length || this._exclude.length || this._boost.length)) throw new Error('Search: esQuery() does not support include/exclude/boost');
214
+ if (endpoint === '/search/semantic') {
215
+ if (this._include.length || this._exclude.length || this._boost.length) throw new Error('Search: semantic does not support include/exclude/boost');
216
+ if (this._sort_by != null) throw new Error('Search: semantic does not support sortBy()');
217
+ }
218
+ if (endpoint === '/search/boost' && this._sort_by != null) throw new Error('Search: boost does not support sortBy()');
219
+ const payload = this.getPayload(), url = `${this._url}${endpoint}`;
192
220
  this.log(`Sending request to ${url}`);
193
221
  const startTime = performance.now();
194
222
  const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
195
223
  if (!response.ok) { const text = await response.text(); throw new Error(`Search API error: ${response.status} — ${text}`); }
196
224
  const result = await response.json();
197
- if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
225
+ if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
198
226
  result.took_sdk = Math.round(performance.now() - startTime);
199
227
  this.lastCall = { endpoint, payload }; this.lastResult = result;
200
- const res = result.result; if (!res) throw new Error('Search.execute: result.result is undefined');
201
- return res.hits;
228
+ if (!result.result) throw new Error('Search.execute: result.result is undefined');
229
+ const res = result.result; return res.hits;
202
230
  }
203
231
  async frequentValues(field, size = 25) {
204
232
  if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.frequentValues: index must be set');
233
+ if (typeof field !== 'string' || !field.trim()) throw new Error('Search.frequentValues: field must be non-empty');
205
234
  const n = Number(size); if (!Number.isInteger(n) || n <= 0) throw new Error('Search.frequentValues: size must be positive integer');
206
235
  const endpoint = `/search/frequent_values/${encodeURIComponent(this._index)}/${encodeURIComponent(field.trim())}?size=${n}`;
207
236
  const response = await fetch(`${this._url}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
208
- if (!response.ok) throw new Error(`Search frequentValues error: ${response.status}`);
209
- const result = await response.json();
210
- if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
211
- return result;
237
+ if (!response.ok) throw new Error(`frequentValues error: ${response.status}`);
238
+ const result = await response.json(); if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error)); return result;
212
239
  }
213
240
  async lookup(docId) {
214
241
  if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.lookup: index must be set');
242
+ if (typeof docId !== 'string' || !docId.trim()) throw new Error('Search.lookup: docId must be non-empty');
215
243
  const endpoint = `/search/document/${encodeURIComponent(this._index)}/${encodeURIComponent(docId.trim())}`;
216
244
  const response = await fetch(`${this._url}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
217
- if (!response.ok) throw new Error(`Search lookup error: ${response.status}`);
218
- const result = await response.json();
219
- if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
220
- return result;
245
+ if (!response.ok) throw new Error(`lookup error: ${response.status}`);
246
+ const result = await response.json(); if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error)); return result;
221
247
  }
222
248
  index(selected_index) { if (typeof selected_index !== 'string' || !selected_index.trim()) throw new Error('Search.index: selected_index required'); this._index = selected_index.trim(); return this; }
223
249
  size(size) { const n = Number(size); if (!Number.isInteger(n) || n <= 0 || n >= 2000) throw new Error('Search.size: 0 < size < 2000'); this._size = n; return this; }
@@ -225,7 +251,7 @@ export class Search {
225
251
  includeVectors(value) { this._include_vector = value == null ? true : Boolean(value); return this; }
226
252
  selectFields(fields) { if (fields === null) { this._select_fields = null; return this; } if (!Array.isArray(fields)) throw new Error('Search.selectFields: array or null'); this._select_fields = fields.map((f) => (typeof f === 'string' ? f.trim() : String(f))); return this; }
227
253
  text(text) { if (typeof text !== 'string' || !text.trim()) throw new Error('Search.text: non-empty string'); this._text = text.trim(); return this; }
228
- vector(vector) { if (!Array.isArray(vector)) throw new Error('Search.vector: array'); this._vector = vector; return this; }
254
+ vector(vector) { if (!Array.isArray(vector)) throw new Error('Search.vector: array required'); this._vector = vector; return this; }
229
255
  esQuery(rawQuery) { if (rawQuery == null || typeof rawQuery !== 'object' || Array.isArray(rawQuery)) throw new Error('Search.esQuery: plain object'); this._es_query = rawQuery; return this; }
230
256
  sortBy(field, direction = 'desc') { if (typeof field !== 'string' || !field.trim()) throw new Error('Search.sortBy: field required'); if (direction !== 'asc' && direction !== 'desc') throw new Error('Search.sortBy: asc|desc'); this._sort_by = { field: field.trim(), order: direction }; return this; }
231
257
  include() { this._active_array = this._include; return this; }
@@ -236,7 +262,7 @@ export class Search {
236
262
  filter(filterInstance) {
237
263
  this._requireActiveArray();
238
264
  if (filterInstance == null || !(filterInstance instanceof Filter)) throw new Error('Search.filter: Filter instance required');
239
- if (this._active_array === this._boost && filterInstance.filter !== 'group_boost' && filterInstance.boost == null) throw new Error('Search: boost array filter needs boost');
265
+ if (this._active_array === this._boost && filterInstance.filter !== 'group_boost' && filterInstance.boost == null) throw new Error('Search: boost array requires non-null boost (group_boost exempt)');
240
266
  this._active_array.push(filterInstance); return this;
241
267
  }
242
268
  term(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermFilter(field, value, boost)); return this; }
@@ -254,34 +280,112 @@ export class Search {
254
280
  log(string) { this._log(string); }
255
281
  show(results) { this._show(results); }
256
282
  }
283
+ ```
284
+
285
+ ---
257
286
 
258
287
  ## V1/search/filters/Filter.js
288
+
289
+ ```javascript
290
+ export function normalizeToArray(value) {
291
+ if (Array.isArray(value)) return value;
292
+ if (typeof value === 'string') return value.includes(',') ? value.split(',').map((s) => s.trim()).filter(Boolean) : [value];
293
+ return value != null ? [value] : [];
294
+ }
259
295
  export class Filter {
260
296
  constructor(filterType, field, boost = null) {
261
297
  if (new.target === Filter) throw new Error('Filter is abstract');
262
298
  this.filter = filterType; this.field = field; this.boost = boost;
263
299
  }
264
300
  }
301
+ ```
302
+
303
+ ---
304
+
305
+ ## V1/search/filters/index.js
306
+
307
+ ```javascript
308
+ export { Filter } from './Filter.js';
309
+ export { TermFilter } from './TermFilter.js';
310
+ export { TermsFilter } from './TermsFilter.js';
311
+ export { NumericFilter } from './NumericFilter.js';
312
+ export { MatchFilter } from './MatchFilter.js';
313
+ export { GeoFilter } from './GeoFilter.js';
314
+ export { DateFilter } from './DateFilter.js';
315
+ export { IsNullFilter } from './IsNullFilter.js';
316
+ export { NotNullFilter } from './NotNullFilter.js';
317
+ export { CustomFilter } from './CustomFilter.js';
318
+ export { GroupBoostFilter } from './GroupBoostFilter.js';
319
+ export { TermsLookupFilter } from './TermsLookupFilter.js';
320
+ export { ConsoleAccountFilter } from './ConsoleAccountFilter.js';
321
+ ```
265
322
 
266
- ## V1/search/filters/*Filter.js (extend Filter)
267
- // TermFilter(field, value, boost): super('term', field, boost); this.value = value
268
- // TermsFilter(field, value, boost): super('terms', field, boost); this.value = value
269
- // NumericFilter(field, operator, value, boost): super('numeric', field, boost); this.operator = operator; this.value = value
270
- // MatchFilter, GeoFilter, CustomFilter: same pattern with this.value
271
- // DateFilter(field, dateFrom, dateTo, boost): value = { date_from?, date_to? }; at least one required
272
- // IsNullFilter, NotNullFilter: no value
273
- // GroupBoostFilter(lookup_index, field, value, group, min_boost, max_boost, n): super('group_boost', field, null)
274
- // TermsLookupFilter(lookup_index, field, value, path): path = dot-notation in lookup doc (e.g. "followers.ids")
275
- // ConsoleAccountFilter(field, value, path): path = dot-notation in account doc
323
+ ---
324
+
325
+ ## V1/search/filters (implementations)
326
+
327
+ ```javascript
328
+ // TermFilter.js
329
+ import { Filter } from './Filter.js';
330
+ export class TermFilter extends Filter { constructor(field, value, boost = null) { super('term', field, boost); this.value = value; } }
331
+
332
+ // TermsFilter.js
333
+ import { Filter, normalizeToArray } from './Filter.js';
334
+ export class TermsFilter extends Filter { constructor(field, value, boost = null) { super('terms', field, boost); this.value = normalizeToArray(value); } }
335
+
336
+ // NumericFilter.js
337
+ import { Filter } from './Filter.js';
338
+ export class NumericFilter extends Filter { constructor(field, operator, value, boost = null) { super('numeric', field, boost); this.operator = operator; this.value = value; } }
339
+
340
+ // MatchFilter.js
341
+ import { Filter, normalizeToArray } from './Filter.js';
342
+ export class MatchFilter extends Filter { constructor(field, value, boost = null) { super('match', field, boost); this.value = normalizeToArray(value); } }
343
+
344
+ // DateFilter.js (dateFrom or dateTo required)
345
+ import { Filter } from './Filter.js';
346
+ export class DateFilter extends Filter { constructor(field, dateFrom = null, dateTo = null, boost = null) { super('date', field, boost); if (dateFrom == null && dateTo == null) throw new Error('DateFilter: dateFrom or dateTo required'); const value = {}; if (dateFrom != null) value.date_from = dateFrom; if (dateTo != null) value.date_to = dateTo; this.value = value; } }
347
+
348
+ // GeoFilter.js
349
+ import { Filter } from './Filter.js';
350
+ export class GeoFilter extends Filter { constructor(field, value, boost = null) { super('geo', field, boost); this.value = value; } }
351
+
352
+ // IsNullFilter.js
353
+ import { Filter } from './Filter.js';
354
+ export class IsNullFilter extends Filter { constructor(field, boost = null) { super('is_null', field, boost); } }
355
+
356
+ // NotNullFilter.js
357
+ import { Filter } from './Filter.js';
358
+ export class NotNullFilter extends Filter { constructor(field, boost = null) { super('not_null', field, boost); } }
359
+
360
+ // CustomFilter.js
361
+ import { Filter } from './Filter.js';
362
+ export class CustomFilter extends Filter { constructor(field, value, boost = null) { super('custom', field, boost); this.value = value; } }
363
+
364
+ // GroupBoostFilter.js (lookup_index, field, value, group, min_boost, max_boost, n)
365
+ import { Filter } from './Filter.js';
366
+ export class GroupBoostFilter extends Filter { constructor(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) { super('group_boost', field, null); this.lookup_index = lookup_index; this.value = value; this.group = group; this.min_boost = min_boost; this.max_boost = max_boost; this.n = n; } }
367
+
368
+ // TermsLookupFilter.js (path: dot-notation in lookup doc, e.g. "followers.ids")
369
+ import { Filter } from './Filter.js';
370
+ export class TermsLookupFilter extends Filter { constructor(lookup_index, field, value, path, boost = null) { super('terms_lookup', field, boost); this.lookup_index = lookup_index; this.value = value; this.path = path; } }
371
+
372
+ // ConsoleAccountFilter.js (path: dot-notation in account doc)
373
+ import { Filter } from './Filter.js';
374
+ export class ConsoleAccountFilter extends Filter { constructor(field, value, path, boost = null) { super('console_account', field, boost); this.value = value; this.path = path; } }
375
+ ```
376
+
377
+ ---
276
378
 
277
379
  ## V1/features/Features.js
278
- export const PREFERRED_FEATURE_COLUMNS = ['found','original_rank','sem_sim_fuzzy','sem_sim_closest','usr_primary_labels','usr_secondary_labels','usr_primary_tags','usr_secondary_tags','user_affinity_avg','user_affinity_usdc','user_affinity_count','cluster_1'..'cluster_10','sem_sim_cluster1'..'sem_sim_cluster5'];
380
+
381
+ ```javascript
382
+ export const PREFERRED_FEATURE_COLUMNS = ['found', 'original_rank', 'sem_sim_fuzzy', 'sem_sim_closest', 'usr_primary_labels', 'usr_secondary_labels', 'usr_primary_tags', 'usr_secondary_tags', 'user_affinity_avg', 'user_affinity_usdc', 'user_affinity_count', 'cluster_1', 'cluster_2', 'cluster_3', 'cluster_4', 'cluster_5', 'cluster_6', 'cluster_7', 'cluster_8', 'cluster_9', 'cluster_10', 'sem_sim_cluster1', 'sem_sim_cluster2', 'sem_sim_cluster3', 'sem_sim_cluster4', 'sem_sim_cluster5'];
279
383
  export function sortAvailableFeatures(available) {
280
384
  const preferred = PREFERRED_FEATURE_COLUMNS.filter((col) => available.includes(col));
281
385
  const nonPreferred = available.filter((col) => !PREFERRED_FEATURE_COLUMNS.includes(col));
282
- const regular = nonPreferred.filter((c) => !c.startsWith('AI:') && !c.startsWith('TAG:')).sort();
283
- const aiColumns = nonPreferred.filter((c) => c.startsWith('AI:')).sort();
284
- const tagColumns = nonPreferred.filter((c) => c.startsWith('TAG:')).sort();
386
+ const regular = nonPreferred.filter((col) => !col.startsWith('AI:') && !col.startsWith('TAG:')).sort();
387
+ const aiColumns = nonPreferred.filter((col) => col.startsWith('AI:')).sort();
388
+ const tagColumns = nonPreferred.filter((col) => col.startsWith('TAG:')).sort();
285
389
  return [...preferred, ...regular, ...aiColumns, ...tagColumns];
286
390
  }
287
391
  export class Features {
@@ -304,22 +408,26 @@ export class Features {
304
408
  items(items) { this._items = [...items]; return this; }
305
409
  user(index, userId) { this._user = { index, id: userId }; return this; }
306
410
  async execute() {
307
- if (!this._user || !this._user.index || !this._user.id) throw new Error('Features.execute: user must be set');
308
- if (!Array.isArray(this._items) || this._items.length === 0) throw new Error('Features.execute: items must be set');
411
+ if (!this._user?.index || !this._user?.id) throw new Error('Features.execute: user must be set');
412
+ if (!Array.isArray(this._items) || this._items.length === 0) throw new Error('Features.execute: items must be non-empty');
309
413
  const url = `${this._url}${this.getEndpoint()}`;
310
414
  const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
311
415
  if (!response.ok) throw new Error(`Features API error: ${response.status}`);
312
416
  const result = await response.json();
313
- if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
417
+ if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
314
418
  this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
315
- const res = result.result; if (!res) throw new Error('Features.execute: result.result undefined');
316
- return res;
419
+ if (!result.result) throw new Error('Features.execute: result.result undefined'); return result.result;
317
420
  }
318
421
  log(string) { this._log(string); }
319
422
  show(results) { this._show(results); }
320
423
  }
424
+ ```
425
+
426
+ ---
321
427
 
322
428
  ## V1/scoring/Scoring.js
429
+
430
+ ```javascript
323
431
  export class Scoring {
324
432
  _userId = null; _itemIds = []; _modelEndpoint = null; lastCall = null; lastResult = null;
325
433
  constructor(options) {
@@ -334,31 +442,36 @@ export class Scoring {
334
442
  if (userId != null && typeof userId === 'string' && userId.trim()) this._userId = userId.trim();
335
443
  if (Array.isArray(itemIds) && itemIds.length > 0) this._itemIds = itemIds.map((id) => (typeof id === 'string' ? id : String(id)));
336
444
  }
337
- getEndpoint() { if (!this._modelEndpoint || !this._modelEndpoint.trim()) throw new Error('Scoring: model(endpoint) required'); return this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`; }
445
+ getEndpoint() { if (!this._modelEndpoint?.trim()) throw new Error('Scoring: model(endpoint) required'); return this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`; }
338
446
  getPayload() { return { origin: this._origin, user_id: this._userId, item_ids: [...this._itemIds] }; }
339
447
  model(endpoint) { this._modelEndpoint = endpoint; return this; }
340
448
  userId(userId) { this._userId = userId; return this; }
341
449
  itemIds(itemIds) { this._itemIds = itemIds; return this; }
342
450
  async execute() {
343
- if (!this._modelEndpoint || !this._modelEndpoint.trim()) throw new Error('Scoring: model required');
344
- if (!this._userId || typeof this._userId !== 'string' || !this._userId.trim()) throw new Error('Scoring: user_id required');
451
+ if (!this._modelEndpoint?.trim()) throw new Error('Scoring: model(endpoint) required');
452
+ if (!this._userId?.trim()) throw new Error('Scoring: user_id required');
345
453
  if (!Array.isArray(this._itemIds) || this._itemIds.length === 0) throw new Error('Scoring: item_ids required');
346
454
  const url = `${this._url}${this.getEndpoint()}`;
347
455
  const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
348
456
  if (!response.ok) throw new Error(`Scoring API error: ${response.status}`);
349
457
  const result = await response.json();
350
- if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
458
+ if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
351
459
  this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
352
- const res = result.result; if (res === undefined) throw new Error('Scoring: result.result undefined');
353
- return res;
460
+ if (result.result === undefined) throw new Error('Scoring: result.result undefined'); return result.result;
354
461
  }
355
462
  log(string) { this._log(string); }
356
463
  show(results) { this._show(results); }
357
464
  }
465
+ ```
466
+
467
+ ---
358
468
 
359
469
  ## V1/ranking/Ranking.js
470
+
471
+ ```javascript
360
472
  export class Ranking {
361
- _candidates = []; _sortMethod = 'sort'; _sortParams = null; _diversityMethod = null; _diversityParams = null; _limitsByFieldEnabled = false; _everyN = 10; _limitRules = []; lastCall = null; lastResult = null;
473
+ _candidates = []; _sortMethod = 'sort'; _sortParams = null; _diversityMethod = null; _diversityParams = null;
474
+ _limitsByFieldEnabled = false; _everyN = 10; _limitRules = []; lastCall = null; lastResult = null;
362
475
  constructor(options) {
363
476
  if (!options || typeof options !== 'object') throw new Error('Ranking: options required');
364
477
  const { url, apiKey, candidates = [], origin = 'sdk', log, show } = options;
@@ -379,7 +492,7 @@ export class Ranking {
379
492
  }
380
493
  if (this._diversityMethod === 'fields' && this._diversityParams?.fields) this._diversityParams.fields.forEach((f) => useful.add(f));
381
494
  if (this._diversityMethod === 'semantic') needEmbedding = true;
382
- if (this._limitsByFieldEnabled && this._limitRules.length > 0) this._limitRules.forEach((r) => r.field && useful.add(r.field));
495
+ if (this._limitsByFieldEnabled && this._limitRules.length) this._limitRules.forEach((r) => r.field && useful.add(r.field));
383
496
  return { usefulFields: useful, needEmbedding };
384
497
  }
385
498
  getPayload() {
@@ -388,7 +501,7 @@ export class Ranking {
388
501
  const items = hits.map((hit) => {
389
502
  const item = { item_id: hit._id };
390
503
  for (const key of usefulFields) { const v = hit._features?.[key] ?? hit._scores?.[key]; if (v !== undefined) item[key] = v; }
391
- if (needEmbedding) { let embed = hit._source?.item_sem_embed2; if (!embed || !Array.isArray(embed)) embed = hit._source?.text_vector; if (embed) item.embed = embed; }
504
+ if (needEmbedding) { let embed = hit._source?.item_sem_embed2; if (!embed?.length) embed = hit._source?.text_vector; if (embed) item.embed = embed; }
392
505
  return item;
393
506
  });
394
507
  const payload = { origin: this._origin, items };
@@ -399,14 +512,14 @@ export class Ranking {
399
512
  }
400
513
  _buildSortConfig() {
401
514
  if (!this._sortParams) return undefined;
402
- if (this._sortMethod === 'sort' && this._sortParams.fields?.length > 0) return { method: 'sort', params: { ...this._sortParams } };
403
- if (this._sortMethod === 'linear' && Array.isArray(this._sortParams) && this._sortParams.length > 0) return { method: 'linear', params: this._sortParams.map((p) => ({ field: p.field, weight: p.weight })) };
404
- if (this._sortMethod === 'mix' && Array.isArray(this._sortParams) && this._sortParams.length > 0) return { method: 'mix', params: this._sortParams.map((p) => ({ field: p.field, direction: p.direction, percentage: p.percentage })) };
515
+ if (this._sortMethod === 'sort' && this._sortParams.fields?.length) return { method: 'sort', params: { ...this._sortParams } };
516
+ if (this._sortMethod === 'linear' && Array.isArray(this._sortParams) && this._sortParams.length) return { method: 'linear', params: this._sortParams.map((p) => ({ field: p.field, weight: p.weight })) };
517
+ if (this._sortMethod === 'mix' && Array.isArray(this._sortParams) && this._sortParams.length) return { method: 'mix', params: this._sortParams.map((p) => ({ field: p.field, direction: p.direction, percentage: p.percentage })) };
405
518
  return undefined;
406
519
  }
407
520
  _buildDiversityConfig() {
408
521
  if (!this._diversityMethod) return undefined;
409
- if (this._diversityMethod === 'fields' && this._diversityParams?.fields?.length > 0) return { method: 'fields', params: { fields: [...this._diversityParams.fields] } };
522
+ if (this._diversityMethod === 'fields' && this._diversityParams?.fields?.length) return { method: 'fields', params: { fields: [...this._diversityParams.fields] } };
410
523
  if (this._diversityMethod === 'semantic') return { method: 'semantic', params: { lambda: Number(this._diversityParams?.lambda ?? 0.5), horizon: Number(this._diversityParams?.horizon ?? 20) } };
411
524
  return undefined;
412
525
  }
@@ -419,66 +532,50 @@ export class Ranking {
419
532
  if (x !== 'sort' && x !== 'linear' && x !== 'mix') throw new Error('Ranking.sortingMethod: sort|linear|mix');
420
533
  this._sortMethod = x;
421
534
  if (x === 'sort') this._sortParams = { fields: [], direction: [] };
422
- if (x === 'linear' || x === 'mix') this._sortParams = [];
535
+ if (x === 'linear') this._sortParams = [];
536
+ if (x === 'mix') this._sortParams = [];
423
537
  return this;
424
538
  }
425
539
  sortBy(field, direction = 'desc', field2, direction2 = 'desc') {
426
540
  if (this._sortMethod === 'linear' || this._sortMethod === 'mix') throw new Error('Ranking.sortBy: only for sortingMethod("sort")');
427
- this._sortMethod = 'sort'; if (!this._sortParams || !this._sortParams.fields) this._sortParams = { fields: [], direction: [] };
428
- const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.sortBy: field required');
429
- if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.sortBy: asc|desc');
541
+ this._sortMethod = 'sort';
542
+ if (!this._sortParams?.fields) this._sortParams = { fields: [], direction: [] };
543
+ const f = typeof field === 'string' && field.trim() ? field.trim() : null;
544
+ if (!f) throw new Error('Ranking.sortBy: field required'); if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.sortBy: asc|desc');
430
545
  this._sortParams = { fields: [f], direction: [direction] };
431
546
  if (typeof field2 === 'string' && field2.trim()) { this._sortParams.fields.push(field2.trim()); this._sortParams.direction.push(direction2 === 'asc' ? 'asc' : 'desc'); }
432
547
  return this;
433
548
  }
434
- weight(field, w) { if (this._sortMethod !== 'linear') throw new Error('Ranking.weight: only for linear'); const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.weight: field required'); if (!Array.isArray(this._sortParams)) this._sortParams = []; this._sortParams.push({ field: f, weight: Number(w) }); return this; }
435
- mix(field, direction, percentage) {
436
- if (this._sortMethod === 'linear') throw new Error('Ranking.mix: only for mix');
437
- this._sortMethod = 'mix'; if (!Array.isArray(this._sortParams)) this._sortParams = [];
438
- const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.mix: field required');
439
- if (direction !== 'asc' && direction !== 'desc') throw new Error('Ranking.mix: asc|desc');
440
- this._sortParams.push({ field: f, direction, percentage: Number(percentage) || 0 }); return this;
441
- }
442
- diversity(method) {
443
- if (method !== 'fields' && method !== 'semantic') throw new Error('Ranking.diversity: fields|semantic');
444
- this._diversityMethod = method;
445
- if (method === 'fields') this._diversityParams = { fields: [] };
446
- if (method === 'semantic') this._diversityParams = { lambda: 0.5, horizon: 20 };
447
- return this;
448
- }
449
- fields(arrayOrItem) {
450
- if (this._diversityMethod !== 'fields') throw new Error('Ranking.fields: only for diversity("fields")');
451
- if (!this._diversityParams || !Array.isArray(this._diversityParams.fields)) this._diversityParams = { fields: [] };
452
- const add = (name) => { const s = typeof name === 'string' && name.trim() ? name.trim() : null; if (s && !this._diversityParams.fields.includes(s)) this._diversityParams.fields.push(s); };
453
- if (Array.isArray(arrayOrItem)) arrayOrItem.forEach(add); else add(arrayOrItem);
454
- return this;
455
- }
456
- horizon(n) { if (this._diversityMethod !== 'semantic') throw new Error('Ranking.horizon: only for semantic'); if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 }; this._diversityParams.horizon = Number(n); return this; }
457
- lambda(value) { if (this._diversityMethod !== 'semantic') throw new Error('Ranking.lambda: only for semantic'); if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 }; this._diversityParams.lambda = Number(value); return this; }
549
+ weight(field, w) { if (this._sortMethod !== 'linear') throw new Error('Ranking.weight: only for sortingMethod("linear")'); const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('field required'); if (!Array.isArray(this._sortParams)) this._sortParams = []; this._sortParams.push({ field: f, weight: Number(w) }); return this; }
550
+ mix(field, direction, percentage) { if (this._sortMethod === 'linear') throw new Error('Ranking.mix: only for sortingMethod("mix")'); this._sortMethod = 'mix'; if (!Array.isArray(this._sortParams)) this._sortParams = []; const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('field required'); if (direction !== 'asc' && direction !== 'desc') throw new Error('direction asc|desc'); this._sortParams.push({ field: f, direction, percentage: Number(percentage) || 0 }); return this; }
551
+ diversity(method) { if (method !== 'fields' && method !== 'semantic') throw new Error('Ranking.diversity: fields|semantic'); this._diversityMethod = method; if (method === 'fields') this._diversityParams = { fields: [] }; if (method === 'semantic') this._diversityParams = { lambda: 0.5, horizon: 20 }; return this; }
552
+ fields(arrayOrItem) { if (this._diversityMethod !== 'fields') throw new Error('Ranking.fields: only for diversity("fields")'); if (!Array.isArray(this._diversityParams?.fields)) this._diversityParams = { fields: [] }; const add = (name) => { const s = typeof name === 'string' && name.trim() ? name.trim() : null; if (s && !this._diversityParams.fields.includes(s)) this._diversityParams.fields.push(s); }; Array.isArray(arrayOrItem) ? arrayOrItem.forEach(add) : add(arrayOrItem); return this; }
553
+ horizon(n) { if (this._diversityMethod !== 'semantic') throw new Error('Ranking.horizon: only for diversity("semantic")'); if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 }; this._diversityParams.horizon = Number(n); return this; }
554
+ lambda(value) { if (this._diversityMethod !== 'semantic') throw new Error('Ranking.lambda: only for diversity("semantic")'); if (!this._diversityParams) this._diversityParams = { lambda: 0.5, horizon: 20 }; this._diversityParams.lambda = Number(value); return this; }
458
555
  limitByField() { this._limitsByFieldEnabled = true; return this; }
459
556
  every(n) { this._everyN = Number(n); return this; }
460
- limit(field, max) { const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('Ranking.limit: field required'); const existing = this._limitRules.find((r) => r.field === f); if (existing) existing.limit = Number(max) || 0; else this._limitRules.push({ field: f, limit: Number(max) || 0 }); return this; }
557
+ limit(field, max) { const f = typeof field === 'string' && field.trim() ? field.trim() : null; if (!f) throw new Error('limit: field required'); const existing = this._limitRules.find((r) => r.field === f); if (existing) existing.limit = Number(max) || 0; else this._limitRules.push({ field: f, limit: Number(max) || 0 }); return this; }
461
558
  candidates(candidates) { this._candidates = candidates; return this; }
462
559
  async execute() {
463
- if (!Array.isArray(this._candidates) || this._candidates.length === 0) throw new Error('Ranking.execute: candidates required');
464
- if (!this._buildSortConfig()) throw new Error('Ranking.execute: sort config required');
465
- const url = `${this._url}${this.getEndpoint()}`;
466
- const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
560
+ if (!Array.isArray(this._candidates) || this._candidates.length === 0) throw new Error('Ranking: candidates required');
561
+ if (!this._buildSortConfig()) throw new Error('Ranking: sort config required');
562
+ const url = `${this._url}${this.getEndpoint()}`, payload = this.getPayload();
563
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
467
564
  if (!response.ok) throw new Error(`Ranking API error: ${response.status}`);
468
565
  const result = await response.json();
469
- if (result && typeof result.error !== 'undefined' && result.error !== null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
470
- this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
471
- const res = result.result; if (!res) throw new Error('Ranking.execute: result undefined');
472
- return res;
566
+ if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
567
+ if (!result.result) throw new Error('Ranking: result undefined'); return result.result;
473
568
  }
474
569
  log(string) { this._log(string); }
475
570
  show(results) { this._show(results); }
476
571
  }
572
+ ```
477
573
 
478
- ## V1/utils/indexUtils.js
479
- /** Maps index names to canonical base (farcaster-items-v2 → farcaster-items). Used for feature/score keys. */
480
- export function findIndex(index) {
481
- const indexOptions = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets'];
482
- for (const option of indexOptions) if (index.startsWith(option)) return option;
483
- return null;
484
- }
574
+ ---
575
+
576
+ ## Debugging Tips
577
+
578
+ - **lastCall / lastResult**: `Search`, `Features`, `Scoring`, and `Ranking` expose `lastCall` (endpoint + payload) and `lastResult` for debugging failed requests.
579
+ - **Semantic diversity**: Ranking with `diversity('semantic')` needs `item_sem_embed2` or `text_vector` on hits; ensure `includeVectors(true)` when searching if you use semantic diversity.
580
+ - **Index canonicalization**: `addFeatures`/addScores use `findIndex(hit._index)`; if your index variant (e.g. `polymarket-items-v2`) is not in `indexOptions`, features/scores may not merge.
581
+ - **Filter boost**: When adding to `boost()` array, all filters except `groupBoost` require a non-null `boost` argument.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbd-studio-sdk",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "SDK for Embed Recommendation Engine APIs",
5
5
  "keywords": [
6
6
  "web3",