mbd-studio-sdk 4.1.5 → 4.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.
Files changed (2) hide show
  1. package/llms.txt +488 -306
  2. package/package.json +1 -1
package/llms.txt CHANGED
@@ -1,42 +1,276 @@
1
- # MBD Studio SDK – Reference for AI Coding Agents
1
+ # MBD Studio SDK – LLM Reference
2
+
3
+ ## Introduction
4
+
5
+ The MBD Studio SDK provides search, features, scoring, and ranking for personalized feeds. It targets Elasticsearch-backed indices and MBD backend services.
6
+
7
+ **Entry points:**
8
+ - `StudioConfig({ apiKey, commonUrl?, servicesUrl? })` – API configuration
9
+ - `StudioV1({ config })` or `StudioV1({ apiKey, commonUrl })` – main client
10
+ - `studio.search()` – search builder
11
+ - `studio.features(version)` – feature fetcher
12
+ - `studio.scoring()` – scoring/reranking
13
+ - `studio.ranking()` – ranking with diversity/limits
14
+
15
+ **Typical flow:**
16
+ 1. `forUser(index, userId)` – set user context for personalization
17
+ 2. `search().index(name).include()/exclude()/boost().<filters>.execute()` – get candidates
18
+ 3. `addCandidates(hits)` – attach to context
19
+ 4. `features().execute()` → `addFeatures(result)` – enrich with signals
20
+ 5. `scoring().model(endpoint).execute()` → `addScores(result, key)` – ML scores
21
+ 6. `ranking().sortBy(...).execute()` → `addRanking(result)` – final ordering
22
+ 7. `getFeed()` – retrieve ranked items
23
+
24
+ **Search endpoint selection (automatic):**
25
+ - `esQuery()` → `/search/es_query` (raw ES query)
26
+ - `text()` or `vector()` → `/search/semantic` (no filters)
27
+ - `boost()` filters present → `/search/boost`
28
+ - else → `/search/filter_and_sort`
2
29
 
3
- ## Overview
30
+ ---
31
+
32
+ ## Data Catalog
33
+
34
+ ### Indices
35
+
36
+ | Index | Description |
37
+ |-------|-------------|
38
+ | farcaster-items | Farcaster posts (casts, frames, etc.) |
39
+ | zora-coins | Zora coins/tokens |
40
+ | polymarket-items | Polymarket prediction markets |
41
+ | polymarket-wallets | Polymarket wallet profiles |
42
+ | kalshi-items | Kalshi prediction markets |
43
+
44
+ Canonical names (for features/scores): `farcaster-items`, `zora-coins`, `polymarket-items`, `polymarket-wallets`, `kalshi-items`. Index aliases may include version suffixes (e.g. `farcaster-items-v2`).
45
+
46
+ ### farcaster-items
47
+
48
+ **Sort fields:** item_creation_timestamp, score_popular, score_trending, num_follower, score_reaction, item_id, null
49
+
50
+ **Filter types:** user_preferences, ai_labels, coin_labels, authors, author_followed_by, publication_types, channels, domains, apps, languages, keywords, locations, miniapps, video, item_features, date, spam_moderation, social_filters, beacon_models, blacklist, custom_json
51
+
52
+ **Key fields:** user_id, type (cast, image, video, etc.), text_enriched, domains, lang, channels, item_creation_timestamp, score_spam, score_not_ok, num_like, num_comment, num_share, num_follower, score_popular, score_trending, score_reaction, ai_labels_high/med/low
53
+
54
+ ### zora-coins
55
+
56
+ **Sort fields:** score_popular, score_trending, num_buy, num_sell, zora_market_cap, zora_price_in_usdc, zora_total_supply, zora_total_volume, zora_unique_holders, zora_volume_24h, num_follower, zora_created_at, zora_updated_at, last_interaction_timestamp, null
57
+
58
+ **Filter types:** ai_labels, date, publication_types, media_content_types, domains, authors, author_followed_by, market_info, spam_moderation, keywords, is_null, boolean_filters
59
+
60
+ **Key fields:** zora_name, zora_description, zora_creator_farcaster_id, user_name, domain, zora_media_content_type, active, zora_updated_at, total_volume, volume_24h, price_usdc, market_cap, unique_holders, total_supply
61
+
62
+ ### polymarket-items
63
+
64
+ **Sort fields:** created_at, updated_at, liquidity, volume, volume_24hr, volume_1wk, volume_1mo, volume_1yr, one_hour_price_change_abs, one_day_price_change_abs, one_week_price_change_abs, one_month_price_change_abs, spread, null
65
+
66
+ **Filter types:** item_ids, wallet_prefs1, ai_labels, tags, date, boolean_filters, keywords, numeric_filters
67
+
68
+ **Boost-only:** boost_wallet_labels, boost_wallet_tags
4
69
 
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).
70
+ **Key fields:** created_at, updated_at, start_date, end_date, game_start_time, liquidity, volume, volume_24hr, volume_1wk, volume_1mo, volume_1yr, span_days, featured, restricted, closed, price_under05_or_over95, price_0_or_1, ai_labels_med, tags
6
71
 
7
- ## Entry Points
72
+ ### polymarket-wallets
8
73
 
9
- - `StudioConfig` API config (apiKey required; commonUrl or servicesUrl for endpoints)
10
- - `StudioV1` – Main client. Import: `import { StudioConfig, StudioV1 } from 'mbd-studio-sdk'`
74
+ **Sort fields:** updated_at, pnl, volume, null
11
75
 
12
- ## Typical Flow
76
+ **Filter types:** user_ids, ai_labels2, tags2, is_null, date, numeric_filters
13
77
 
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
78
+ **Key fields:** user_id, name, pseudonym, pfp, updated_at, volume, pnl, events_per_day, ai_labels_med, tags (label_01..label_10, tag_01..tag_10)
22
79
 
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.
80
+ ### kalshi-items
81
+
82
+ **Sort fields:** created_at, updated_at, volume, start_date, end_date, null
83
+
84
+ **Filter types:** item_ids, ai_labels, date, boolean_filters, keywords, numeric_filters
85
+
86
+ **Key fields:** question, description, created_at, updated_at, start_date, end_date, volume, active, closed
24
87
 
25
88
  ---
26
89
 
27
- ## index.js
90
+ ## Filter Types
28
91
 
29
- ```javascript
30
- import * as V1 from './V1/index.js';
31
- export { StudioConfig } from './StudioConfig.js';
32
- export const StudioV1 = V1.Studio;
92
+ All filters require `include()`, `exclude()`, or `boost()` to be called first. Filters in `include` → ES `must`; `exclude` → ES `must_not`; `boost` → ES `should`. For `boost`, non–group_boost filters require a non-null boost value.
93
+
94
+ ### term
95
+
96
+ Exact match on a single value.
97
+
98
+ ```js
99
+ .include().term('type', 'cast')
100
+ .exclude().term('closed', true)
101
+ .boost().term('lang', 'en', 1.5)
102
+ ```
103
+
104
+ Backend: `{ "term": { field: { "value": value } } }`
105
+
106
+ ### terms
107
+
108
+ Match any of multiple values. Value can be array or comma-separated string.
109
+
110
+ ```js
111
+ .include().terms('channels', ['/base', '/degen'])
112
+ .terms('user_id', '123,456,789') // normalized to array
113
+ .boost().terms('ai_labels_med', ['crypto', 'defi'], 2)
114
+ ```
115
+
116
+ Backend: `{ "terms": { field: value } }`
117
+
118
+ ### numeric
119
+
120
+ Numeric comparison. Operators: `>`, `>=`, `<`, `<=`.
121
+
122
+ ```js
123
+ .include().numeric('volume', '>=', 10000)
124
+ .exclude().numeric('score_spam', '>', 0.5)
125
+ .boost().numeric('num_follower', '>=', 1000, 1.2)
126
+ ```
127
+
128
+ Backend: `{ "range": { field: { gt|gte|lt|lte: value } } }`
129
+
130
+ ### date
131
+
132
+ Date range. At least one of dateFrom or dateTo required. ISO strings or Elasticsearch date math (e.g. `now-7d`, `now/d`).
133
+
134
+ ```js
135
+ .include().date('item_creation_timestamp', 'now-7d', 'now')
136
+ .include().date('created_at', '2024-01-01', null)
137
+ .exclude().date('updated_at', null, 'now-30d')
138
+ ```
139
+
140
+ Backend: `{ "range": { field: { gte?: date_from, lte?: date_to } } }`
141
+
142
+ ### match
143
+
144
+ Full-text match on keywords. Value normalized to array; each keyword is a separate match clause, OR’d together.
145
+
146
+ ```js
147
+ .include().match('text_enriched', 'crypto defi')
148
+ .include().match('zora_name', ['bitcoin', 'ethereum'])
149
+ ```
150
+
151
+ Backend: `{ "bool": { "should": [ { "match": { field: keyword } } for each ], "minimum_should_match": 1 } }`
152
+
153
+ ### geo
154
+
155
+ Geo-distance filter. Value: array of strings `"geo:lat,lon"`. Default distance: 100km.
156
+
157
+ ```js
158
+ .include().geo('location', ['geo:40.7128,-74.0060', 'geo:37.7749,-122.4194'])
33
159
  ```
34
160
 
161
+ Backend: `{ "bool": { "should": [ { "geo_distance": { "distance": "100km", field: { lat, lon } } } ], "minimum_should_match": 1 } }`
162
+
163
+ ### is_null / not_null
164
+
165
+ Check if field is missing or present.
166
+
167
+ ```js
168
+ .include().isNull('zora_creator_farcaster_id')
169
+ .exclude().notNull('pfp')
170
+ ```
171
+
172
+ Backend: is_null → `{ "bool": { "must_not": [ { "exists": { "field" } } ] } }`; not_null → `{ "exists": { "field" } }`
173
+
174
+ ### custom
175
+
176
+ Raw Elasticsearch filter clause. Value is the clause object.
177
+
178
+ ```js
179
+ .include().custom('_', { "script": { "script": { "source": "..." } } })
180
+ ```
181
+
182
+ Backend: `value` is used as-is.
183
+
184
+ ### group_boost
185
+
186
+ Boosts items by group membership from a lookup document. The lookup doc has paths `{group}_01`, `{group}_02`, … `{group}_n`. Items matching `{group}_01` get max_boost; `{group}_02` get slightly less; etc. Linear decay from max to min over n groups.
187
+
188
+ **Parameters:**
189
+ - `lookup_index` – index containing the lookup doc
190
+ - `field` – field on the main index to match against
191
+ - `value` – document ID in lookup_index
192
+ - `group` – prefix for path (e.g. `"label"` → label_01, label_02, …)
193
+ - `min_boost` – boost for last group (default 1)
194
+ - `max_boost` – boost for first group (default 5)
195
+ - `n` – number of groups (default 10)
196
+
197
+ ```js
198
+ .boost().groupBoost('polymarket-wallets', 'ai_labels_med', walletId, 'label', 1, 5, 10)
199
+ .boost().groupBoost('polymarket-wallets', 'tags', walletId, 'tag', 1, 3, 5)
200
+ ```
201
+
202
+ Backend: builds `bool.should` with `terms` lookup per group path, each with its own boost (linear interpolation).
203
+
204
+ ### terms_lookup
205
+
206
+ Filter by terms fetched from another document. Fetches `path` from doc `value` in `lookup_index`, matches `field`.
207
+
208
+ ```js
209
+ .include().termsLookup('following-graph', 'user_id', farcasterFid, 'following')
210
+ .include().termsLookup('user-prefs', 'ai_labels_med', userId, 'preferred_labels')
211
+ ```
212
+
213
+ Backend: `{ "terms": { field: { "index": lookup_index, "id": value, "path": path } } }`
214
+
215
+ ### console_account
216
+
217
+ Lookup terms from console-accounts index. Used for blacklists, whitelists, etc.
218
+
219
+ ```js
220
+ .exclude().consoleAccount('user_id', consoleAccountId, 'exclude_user_ids')
221
+ .include().consoleAccount('item_id', consoleAccountId, 'include_item_ids')
222
+ ```
223
+
224
+ Backend: `{ "terms": { field: { "index": "console-accounts", "id": value, "path": path } } }`
225
+
226
+ ---
227
+
228
+ ## FAQ
229
+
230
+ **Q: Why do I get "call include(), exclude(), or boost() before adding filters"?**
231
+ A: You must call one of these before any filter. They set which array (include/exclude/boost) the next filters go into.
232
+
233
+ **Q: Can I use filters with semantic search?**
234
+ A: No. Semantic search (`text()` or `vector()`) uses `/search/semantic` and does not support include/exclude/boost. Use filter_and_sort or boost for filtering.
235
+
236
+ **Q: Can I sort when using boost?**
237
+ A: No. Boost endpoint does not support `sortBy()`. Use filter_and_sort if you need sorting.
238
+
239
+ **Q: What is the difference between term and terms?**
240
+ A: `term` matches a single value exactly. `terms` matches any value in a list.
241
+
242
+ **Q: What is the difference between term and match?**
243
+ A: `term` is exact (keyword-like). `match` is full-text (analyzed, OR across keywords).
244
+
245
+ **Q: How does group_boost work with polymarket-wallets?**
246
+ A: A wallet doc has `label_01`, `label_02`, … (or `tag_01`, …) with preferred labels/tags. Items matching label_01 get highest boost; label_02 get less; etc. Use `group` = `"label"` or `"tag"` and `field` = `ai_labels_med` or `tags`.
247
+
248
+ **Q: onlyIds, selectFields, includeVectors – can I use more than one?**
249
+ A: No. They are mutually exclusive.
250
+
251
+ **Q: How do I debug a failing query?**
252
+ A: Use `search.lastCall` (endpoint + payload) and `search.lastResult` after `execute()`. Enable `log` in config to see requests/results.
253
+
254
+ **Q: Index names with versions?**
255
+ A: Use base names like `farcaster-items`; backend may resolve to versioned aliases. `findIndex()` maps versioned names to canonical base for features/scores.
256
+
35
257
  ---
36
258
 
37
- ## StudioConfig.js
259
+ ## Source Code
38
260
 
39
- ```javascript
261
+ Order: entry → config → Studio → Search → Filter base → filters → Features → Scoring → Ranking → utils.
262
+
263
+ ---
264
+
265
+ ### index.js
266
+ ```js
267
+ import * as V1 from './V1/index.js';
268
+ export { StudioConfig } from './StudioConfig.js';
269
+ export const StudioV1 = V1.Studio;
270
+ ```
271
+
272
+ ### StudioConfig.js
273
+ ```js
40
274
  const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
41
275
  export class StudioConfig {
42
276
  constructor(options) {
@@ -52,8 +286,7 @@ export class StudioConfig {
52
286
  const services = { searchService, storiesService, featuresService, scoringService, rankingService };
53
287
  const missing = Object.entries(services).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k);
54
288
  if (missing.length > 0) throw new Error(`StudioConfig: when using servicesUrl, all service URLs are required. Missing: ${missing.join(', ')}`);
55
- [this.searchService, this.storiesService, this.featuresService, this.scoringService, this.rankingService] =
56
- [searchService, storiesService, featuresService, scoringService, rankingService].map((u) => u.trim().replace(/\/$/, ''));
289
+ [this.searchService, this.storiesService, this.featuresService, this.scoringService, this.rankingService] = [searchService, storiesService, featuresService, scoringService, rankingService].map(u => u.trim().replace(/\/$/, ''));
57
290
  } else {
58
291
  this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = defaultBaseUrl;
59
292
  }
@@ -64,32 +297,13 @@ export class StudioConfig {
64
297
  }
65
298
  ```
66
299
 
67
- ---
68
-
69
- ## V1/index.js
70
-
71
- ```javascript
300
+ ### V1/index.js
301
+ ```js
72
302
  export { Studio } from './Studio.js';
73
303
  ```
74
304
 
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
- ---
89
-
90
- ## V1/Studio.js
91
-
92
- ```javascript
305
+ ### V1/Studio.js
306
+ ```js
93
307
  import { StudioConfig } from '../StudioConfig.js';
94
308
  import { Search } from './search/Search.js';
95
309
  import { Features, sortAvailableFeatures } from './features/Features.js';
@@ -114,8 +328,7 @@ export class Studio {
114
328
  async frequentValues(index, field, size = 25) { return this.search().index(index).frequentValues(field, size); }
115
329
  addCandidates(array) { this._candidates.push(...array); }
116
330
  features(version = 'v1') {
117
- let items = [];
118
- if (this._candidates?.length > 0) items = this._candidates.map((hit) => ({ index: hit._index, id: hit._id }));
331
+ const items = (this._candidates && this._candidates.length > 0) ? this._candidates.map((hit) => ({ index: hit._index, id: hit._id })) : [];
119
332
  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 });
120
333
  }
121
334
  addFeatures(featuresResult) {
@@ -129,57 +342,51 @@ export class Studio {
129
342
  hit._features = hit._features ? { ...hit._features, ...hitFeatures } : hitFeatures;
130
343
  hit._scores = hit._scores ? { ...hit._scores, ...hitScores } : hitScores;
131
344
  hit._info = hit._info ? { ...hit._info, ...hitInfo } : hitInfo;
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;
345
+ if (hit._features) for (const [k, v] of Object.entries(hit._features)) if (typeof v === 'number' && !Number.isNaN(v)) availableFeatures[k] = true;
346
+ if (hit._scores) for (const [k, v] of Object.entries(hit._scores)) if (typeof v === 'number' && !Number.isNaN(v)) availableScores[k] = true;
134
347
  }
135
348
  this._log(`Available features: ${sortAvailableFeatures(Object.keys(availableFeatures))}`);
136
349
  this._log(`Available scores: ${sortAvailableFeatures(Object.keys(availableScores))}`);
137
350
  }
138
351
  scoring() {
139
352
  const userId = this._forUser?.id ?? null;
140
- const itemIds = Array.isArray(this._candidates) && this._candidates.length > 0 ? this._candidates.map((c) => (c?._id != null ? String(c._id) : null)).filter(Boolean) : [];
353
+ const itemIds = Array.isArray(this._candidates) && this._candidates.length > 0 ? this._candidates.map((c) => (c && c._id != null ? String(c._id) : null)).filter(Boolean) : [];
141
354
  return new Scoring({ url: this._config.scoringService, apiKey: this._config.apiKey, log: this._log, show: this._show, userId, itemIds, origin: this._origin });
142
355
  }
143
356
  addScores(scoringResult, scoringKey) {
144
- const rankedItemIds = scoringResult;
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]; }
357
+ if (!this._candidates || !scoringResult || !Array.isArray(scoringResult)) return;
358
+ const rankToScore = {}; scoringResult.forEach((id, i) => { rankToScore[id] = 1.0 - (i / scoringResult.length); });
359
+ for (const hit of this._candidates) { if (rankToScore[hit._id] != null) { if (!hit._scores) hit._scores = {}; hit._scores[scoringKey] = rankToScore[hit._id]; } }
148
360
  }
149
361
  ranking() { return new Ranking({ url: this._config.rankingService, apiKey: this._config.apiKey, log: this._log, show: this._show, candidates: this._candidates, origin: this._origin }); }
150
362
  addRanking(rankingResult) {
151
363
  const rankedItems = rankingResult?.items;
152
- if (!this._candidates || !Array.isArray(rankedItems)) return;
364
+ if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) return;
153
365
  const scoreByItemId = {}; rankedItems.forEach(({ item_id, score }) => { scoreByItemId[item_id] = score; });
154
366
  for (const hit of this._candidates) hit._ranking_score = scoreByItemId[hit._id];
155
367
  this._candidates.sort((a, b) => (b._ranking_score ?? -Infinity) - (a._ranking_score ?? -Infinity));
156
368
  }
157
- log(string) { this._log(string); }
158
- show(results) { this._show(results === undefined ? this._candidates : results); }
159
369
  getFeed() { return this._candidates; }
370
+ dropFeatures() { for (const c of this._candidates) delete c._features; }
371
+ dropVectors() { const keys = ['text_vector', 'item_sem_embed', 'item_sem_embed2']; for (const c of this._candidates) { const src = c._source; if (src) for (const k of keys) delete src[k]; } }
372
+ transform(fn) { this._candidates = this._candidates.map(fn); }
373
+ log(s) { this._log(s); }
374
+ show(results) { this._show(results === undefined ? this._candidates : results); }
160
375
  }
161
376
  ```
162
377
 
163
- ---
164
-
165
- ## V1/search/Search.js
166
-
167
- ```javascript
378
+ ### V1/search/Search.js
379
+ ```js
168
380
  import { Filter, TermFilter, TermsFilter, NumericFilter, MatchFilter, GeoFilter, DateFilter, IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter } from './filters/index.js';
169
381
 
170
382
  export class Search {
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;
383
+ _index = null; _es_query = null; _size = 100; _only_ids = false; _include_vector = false; _select_fields = null; _text = null; _vector = null; _sort_by = null; _include = []; _exclude = []; _boost = []; _active_array = null; lastCall = null; lastResult = null;
174
384
  constructor(options) {
175
385
  if (!options || typeof options !== 'object') throw new Error('Search: options object is required');
176
386
  const { url, apiKey, origin = 'sdk', log, show } = options;
177
387
  if (typeof url !== 'string' || !url.trim()) throw new Error('Search: options.url is required');
178
388
  if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Search: options.apiKey is required');
179
- this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
180
- this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
181
- this._log = typeof log === 'function' ? log : console.log.bind(console);
182
- this._show = typeof show === 'function' ? show : console.log.bind(console);
389
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._origin = origin?.trim() || 'sdk'; this._log = typeof log === 'function' ? log : console.log.bind(console); this._show = typeof show === 'function' ? show : console.log.bind(console);
183
390
  }
184
391
  getEndpoint() {
185
392
  if (this._es_query != null) return '/search/es_query';
@@ -196,10 +403,7 @@ export class Search {
196
403
  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) };
197
404
  if (feedType === 'boost') payload.boost = serializeFilters(this._boost);
198
405
  if (feedType === 'filter_and_sort' && this._sort_by) payload.sort_by = this._sort_by;
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
- }
406
+ 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; }
203
407
  if (this._only_ids) payload.only_ids = true;
204
408
  if (Array.isArray(this._select_fields) && this._select_fields.length > 0) payload.select_fields = this._select_fields;
205
409
  return payload;
@@ -211,60 +415,52 @@ export class Search {
211
415
  if ((hasOnlyIds ? 1 : 0) + (hasSelectFields ? 1 : 0) + (hasIncludeVector ? 1 : 0) > 1) throw new Error('Search: onlyIds, selectFields, includeVectors are mutually exclusive');
212
416
  const endpoint = this.getEndpoint();
213
417
  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()');
418
+ if (endpoint === '/search/semantic') { if (this._include.length || this._exclude.length || this._boost.length) throw new Error('Search: semantic does not support filters'); if (this._sort_by) throw new Error('Search: semantic does not support sortBy'); }
419
+ if (endpoint === '/search/boost' && this._sort_by) throw new Error('Search: boost does not support sortBy');
219
420
  const payload = this.getPayload(), url = `${this._url}${endpoint}`;
220
- this.log(`Sending request to ${url}`);
421
+ this.log(`Sending request to ${url}`); this.log(`Payload:\n${JSON.stringify(payload, null, 2)}`);
221
422
  const startTime = performance.now();
222
423
  const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
223
- if (!response.ok) { const text = await response.text(); throw new Error(`Search API error: ${response.status} ${text}`); }
424
+ if (!response.ok) { const text = await response.text(); throw new Error(`Search API error: ${response.status} ${response.statusText}${text ? ' — ' + text : ''}`); }
224
425
  const result = await response.json();
225
426
  if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
226
- result.took_sdk = Math.round(performance.now() - startTime);
227
- this.lastCall = { endpoint, payload }; this.lastResult = result;
228
- if (!result.result) throw new Error('Search.execute: result.result is undefined');
229
- const res = result.result; return res.hits;
427
+ result.took_sdk = Math.round(performance.now() - startTime); this.lastCall = { endpoint, payload }; this.lastResult = result;
428
+ const res = result.result; if (!res) throw new Error('Search.execute: result.result is undefined');
429
+ this._log(`Search result: total_hits=${res?.total_hits ?? 0} fetched=${res?.hits?.length ?? 0} took_es=${res?.took_es ?? 0} took_backend=${res?.took_backend ?? 0}`);
430
+ return res.hits;
230
431
  }
231
432
  async frequentValues(field, size = 25) {
232
433
  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');
234
- const n = Number(size); if (!Number.isInteger(n) || n <= 0) throw new Error('Search.frequentValues: size must be positive integer');
235
- const endpoint = `/search/frequent_values/${encodeURIComponent(this._index)}/${encodeURIComponent(field.trim())}?size=${n}`;
236
- const response = await fetch(`${this._url}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
434
+ const n = Number(size); if (!Number.isInteger(n) || n <= 0) throw new Error('Search.frequentValues: size must be a positive integer');
435
+ const url = `${this._url}/search/frequent_values/${encodeURIComponent(this._index)}/${encodeURIComponent(field.trim())}?size=${n}`;
436
+ const response = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
237
437
  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;
438
+ const result = await response.json(); if (result?.error != null) throw new Error(String(result.error));
439
+ return result;
239
440
  }
240
441
  async lookup(docId) {
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');
243
- const endpoint = `/search/document/${encodeURIComponent(this._index)}/${encodeURIComponent(docId.trim())}`;
244
- const response = await fetch(`${this._url}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
442
+ if (!this._index || typeof this._index !== 'string' || !docId?.trim()) throw new Error('Search.lookup: index and docId required');
443
+ const url = `${this._url}/search/document/${encodeURIComponent(this._index)}/${encodeURIComponent(docId.trim())}`;
444
+ const response = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${this._apiKey}` } });
245
445
  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;
446
+ const result = await response.json(); if (result?.error != null) throw new Error(String(result.error));
447
+ return result;
247
448
  }
248
449
  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; }
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; }
450
+ size(size) { const n = Number(size); if (!Number.isInteger(n) || n <= 0 || n >= 2000) throw new Error('Search.size: must be 1..1999'); this._size = n; return this; }
250
451
  onlyIds(value) { this._only_ids = value == null ? true : Boolean(value); return this; }
251
452
  includeVectors(value) { this._include_vector = value == null ? true : Boolean(value); return this; }
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; }
453
+ selectFields(fields) { if (fields === null) { this._select_fields = null; return this; } if (!Array.isArray(fields)) throw new Error('selectFields: array or null'); this._select_fields = fields.map((f) => (typeof f === 'string' ? f.trim() : String(f))); return this; }
253
454
  text(text) { if (typeof text !== 'string' || !text.trim()) throw new Error('Search.text: non-empty string'); this._text = text.trim(); return this; }
254
- vector(vector) { if (!Array.isArray(vector)) throw new Error('Search.vector: array required'); this._vector = vector; return this; }
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; }
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; }
455
+ vector(vector) { if (!Array.isArray(vector)) throw new Error('Search.vector: array'); this._vector = vector; return this; }
456
+ esQuery(rawQuery) { if (rawQuery == null || typeof rawQuery !== 'object' || Array.isArray(rawQuery)) throw new Error('esQuery: plain object'); this._es_query = rawQuery; return this; }
457
+ sortBy(field, direction = 'desc') { if (typeof field !== 'string' || !field.trim()) throw new Error('sortBy: field required'); if (direction !== 'asc' && direction !== 'desc') throw new Error('sortBy: asc|desc'); this._sort_by = { field: field.trim(), order: direction }; return this; }
257
458
  include() { this._active_array = this._include; return this; }
258
459
  exclude() { this._active_array = this._exclude; return this; }
259
460
  boost() { this._active_array = this._boost; return this; }
260
461
  _requireActiveArray() { if (this._active_array === null) throw new Error('Search: call include(), exclude(), or boost() before filters'); }
261
462
  _requireBoostForBoostArray(boost) { if (this._active_array === this._boost && boost == null) throw new Error('Search: boost array requires non-null boost'); }
262
- filter(filterInstance) {
263
- this._requireActiveArray();
264
- if (filterInstance == null || !(filterInstance instanceof Filter)) throw new Error('Search.filter: Filter instance required');
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)');
266
- this._active_array.push(filterInstance); return this;
267
- }
463
+ filter(f) { this._requireActiveArray(); if (!(f instanceof Filter)) throw new Error('filter must be Filter instance'); if (this._active_array === this._boost && f.filter !== 'group_boost' && f.boost == null) throw new Error('boost array: filter needs boost'); this._active_array.push(f); return this; }
268
464
  term(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermFilter(field, value, boost)); return this; }
269
465
  terms(field, values, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermsFilter(field, values, boost)); return this; }
270
466
  numeric(field, operator, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NumericFilter(field, operator, value, boost)); return this; }
@@ -277,16 +473,13 @@ export class Search {
277
473
  groupBoost(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) { this._requireActiveArray(); this._active_array.push(new GroupBoostFilter(lookup_index, field, value, group, min_boost, max_boost, n)); return this; }
278
474
  termsLookup(lookup_index, field, value, path, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermsLookupFilter(lookup_index, field, value, path, boost)); return this; }
279
475
  consoleAccount(field, value, path, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new ConsoleAccountFilter(field, value, path, boost)); return this; }
280
- log(string) { this._log(string); }
476
+ log(s) { this._log(s); }
281
477
  show(results) { this._show(results); }
282
478
  }
283
479
  ```
284
480
 
285
- ---
286
-
287
- ## V1/search/filters/Filter.js
288
-
289
- ```javascript
481
+ ### V1/search/filters/Filter.js
482
+ ```js
290
483
  export function normalizeToArray(value) {
291
484
  if (Array.isArray(value)) return value;
292
485
  if (typeof value === 'string') return value.includes(',') ? value.split(',').map((s) => s.trim()).filter(Boolean) : [value];
@@ -300,215 +493,217 @@ export class Filter {
300
493
  }
301
494
  ```
302
495
 
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
- ```
322
-
323
- ---
324
-
325
- ## V1/search/filters (implementations)
326
-
327
- ```javascript
328
- // TermFilter.js
496
+ ### V1/search/filters/TermFilter.js
497
+ ```js
329
498
  import { Filter } from './Filter.js';
330
499
  export class TermFilter extends Filter { constructor(field, value, boost = null) { super('term', field, boost); this.value = value; } }
500
+ ```
331
501
 
332
- // TermsFilter.js
502
+ ### V1/search/filters/TermsFilter.js
503
+ ```js
333
504
  import { Filter, normalizeToArray } from './Filter.js';
334
505
  export class TermsFilter extends Filter { constructor(field, value, boost = null) { super('terms', field, boost); this.value = normalizeToArray(value); } }
506
+ ```
335
507
 
336
- // NumericFilter.js
508
+ ### V1/search/filters/NumericFilter.js
509
+ ```js
337
510
  import { Filter } from './Filter.js';
338
511
  export class NumericFilter extends Filter { constructor(field, operator, value, boost = null) { super('numeric', field, boost); this.operator = operator; this.value = value; } }
512
+ ```
339
513
 
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)
514
+ ### V1/search/filters/DateFilter.js
515
+ ```js
345
516
  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; } }
517
+ export class DateFilter extends Filter {
518
+ constructor(field, dateFrom = null, dateTo = null, boost = null) {
519
+ super('date', field, boost);
520
+ if (dateFrom == null && dateTo == null) throw new Error('DateFilter: dateFrom or dateTo required');
521
+ this.value = {}; if (dateFrom != null) this.value.date_from = dateFrom; if (dateTo != null) this.value.date_to = dateTo;
522
+ }
523
+ }
524
+ ```
347
525
 
348
- // GeoFilter.js
526
+ ### V1/search/filters/GeoFilter.js
527
+ ```js
349
528
  import { Filter } from './Filter.js';
350
529
  export class GeoFilter extends Filter { constructor(field, value, boost = null) { super('geo', field, boost); this.value = value; } }
530
+ ```
351
531
 
352
- // IsNullFilter.js
532
+ ### V1/search/filters/MatchFilter.js
533
+ ```js
534
+ import { Filter, normalizeToArray } from './Filter.js';
535
+ export class MatchFilter extends Filter { constructor(field, value, boost = null) { super('match', field, boost); this.value = normalizeToArray(value); } }
536
+ ```
537
+
538
+ ### V1/search/filters/IsNullFilter.js
539
+ ```js
353
540
  import { Filter } from './Filter.js';
354
541
  export class IsNullFilter extends Filter { constructor(field, boost = null) { super('is_null', field, boost); } }
542
+ ```
355
543
 
356
- // NotNullFilter.js
544
+ ### V1/search/filters/NotNullFilter.js
545
+ ```js
357
546
  import { Filter } from './Filter.js';
358
547
  export class NotNullFilter extends Filter { constructor(field, boost = null) { super('not_null', field, boost); } }
548
+ ```
359
549
 
360
- // CustomFilter.js
550
+ ### V1/search/filters/CustomFilter.js
551
+ ```js
361
552
  import { Filter } from './Filter.js';
362
553
  export class CustomFilter extends Filter { constructor(field, value, boost = null) { super('custom', field, boost); this.value = value; } }
554
+ ```
363
555
 
364
- // GroupBoostFilter.js (lookup_index, field, value, group, min_boost, max_boost, n)
556
+ ### V1/search/filters/GroupBoostFilter.js
557
+ ```js
365
558
  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; } }
559
+ export class GroupBoostFilter extends Filter {
560
+ constructor(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) {
561
+ super('group_boost', field, null);
562
+ this.lookup_index = lookup_index; this.value = value; this.group = group;
563
+ this.min_boost = min_boost; this.max_boost = max_boost; this.n = n;
564
+ }
565
+ }
566
+ ```
367
567
 
368
- // TermsLookupFilter.js (path: dot-notation in lookup doc, e.g. "followers.ids")
568
+ ### V1/search/filters/TermsLookupFilter.js
569
+ ```js
369
570
  import { Filter } from './Filter.js';
370
571
  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; } }
572
+ ```
371
573
 
372
- // ConsoleAccountFilter.js (path: dot-notation in account doc)
574
+ ### V1/search/filters/ConsoleAccountFilter.js
575
+ ```js
373
576
  import { Filter } from './Filter.js';
374
577
  export class ConsoleAccountFilter extends Filter { constructor(field, value, path, boost = null) { super('console_account', field, boost); this.value = value; this.path = path; } }
375
578
  ```
376
579
 
377
- ---
378
-
379
- ## V1/features/Features.js
580
+ ### V1/search/filters/index.js
581
+ ```js
582
+ export { Filter } from './Filter.js';
583
+ export { TermFilter } from './TermFilter.js';
584
+ export { TermsFilter } from './TermsFilter.js';
585
+ export { NumericFilter } from './NumericFilter.js';
586
+ export { MatchFilter } from './MatchFilter.js';
587
+ export { GeoFilter } from './GeoFilter.js';
588
+ export { DateFilter } from './DateFilter.js';
589
+ export { IsNullFilter } from './IsNullFilter.js';
590
+ export { NotNullFilter } from './NotNullFilter.js';
591
+ export { CustomFilter } from './CustomFilter.js';
592
+ export { GroupBoostFilter } from './GroupBoostFilter.js';
593
+ export { TermsLookupFilter } from './TermsLookupFilter.js';
594
+ export { ConsoleAccountFilter } from './ConsoleAccountFilter.js';
595
+ ```
380
596
 
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'];
597
+ ### V1/features/Features.js
598
+ ```js
599
+ 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'];
383
600
  export function sortAvailableFeatures(available) {
384
- const preferred = PREFERRED_FEATURE_COLUMNS.filter((col) => available.includes(col));
385
- const nonPreferred = available.filter((col) => !PREFERRED_FEATURE_COLUMNS.includes(col));
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();
389
- return [...preferred, ...regular, ...aiColumns, ...tagColumns];
601
+ const preferred = PREFERRED_FEATURE_COLUMNS.filter((c) => available.includes(c));
602
+ const rest = available.filter((c) => !PREFERRED_FEATURE_COLUMNS.includes(c));
603
+ const regular = rest.filter((c) => !c.startsWith('AI:') && !c.startsWith('TAG:')).sort();
604
+ const ai = rest.filter((c) => c.startsWith('AI:')).sort();
605
+ const tag = rest.filter((c) => c.startsWith('TAG:')).sort();
606
+ return [...preferred, ...regular, ...ai, ...tag];
390
607
  }
391
608
  export class Features {
392
- _version = 'v1'; _user = null; _items = []; lastCall = null; lastResult = null;
393
609
  constructor(options) {
394
- if (!options || typeof options !== 'object') throw new Error('Features: options required');
395
610
  const { url, apiKey, version = 'v1', items = [], userIndex, userId, origin = 'sdk', log, show } = options;
396
- if (typeof url !== 'string' || !url.trim()) throw new Error('Features: url required');
397
- if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Features: apiKey required');
398
- this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._version = version;
399
- this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
400
- this._log = typeof log === 'function' ? log : console.log.bind(console);
401
- this._show = typeof show === 'function' ? show : console.log.bind(console);
402
- if (Array.isArray(items) && items.length > 0) this._items = items;
403
- if (userIndex != null && userId != null) this._user = { index: userIndex, id: userId };
611
+ if (!url?.trim() || !apiKey?.trim()) throw new Error('Features: url and apiKey required');
612
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._version = version; this._origin = origin?.trim() || 'sdk';
613
+ this._log = typeof log === 'function' ? log : console.log; this._show = typeof show === 'function' ? show : console.log;
614
+ this._items = Array.isArray(items) && items.length > 0 ? items : []; this._user = (userIndex != null && userId != null) ? { index: userIndex, id: userId } : null;
404
615
  }
405
- getEndpoint() { return `/features/${this._version}`; }
406
- getPayload() { return { origin: this._origin, user: this._user, items: this._items.map((item) => ({ ...item })) }; }
407
616
  version(v) { this._version = v; return this; }
408
617
  items(items) { this._items = [...items]; return this; }
409
618
  user(index, userId) { this._user = { index, id: userId }; return this; }
410
619
  async execute() {
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');
413
- const url = `${this._url}${this.getEndpoint()}`;
414
- const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
415
- if (!response.ok) throw new Error(`Features API error: ${response.status}`);
416
- const result = await response.json();
417
- if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
418
- this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
419
- if (!result.result) throw new Error('Features.execute: result.result undefined'); return result.result;
620
+ if (!this._user?.index || !this._user?.id) throw new Error('Features: user required');
621
+ if (!this._items.length) throw new Error('Features: items required');
622
+ const url = `${this._url}/features/${this._version}`;
623
+ const payload = { origin: this._origin, user: this._user, items: this._items.map((i) => ({ ...i })) };
624
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
625
+ if (!response.ok) throw new Error(`Features API: ${response.status}`);
626
+ const result = await response.json(); if (result?.error != null) throw new Error(String(result.error));
627
+ return result.result;
420
628
  }
421
- log(string) { this._log(string); }
422
- show(results) { this._show(results); }
629
+ log(s) { this._log(s); }
630
+ show(r) { this._show(r); }
423
631
  }
424
632
  ```
425
633
 
426
- ---
427
-
428
- ## V1/scoring/Scoring.js
429
-
430
- ```javascript
634
+ ### V1/scoring/Scoring.js
635
+ ```js
431
636
  export class Scoring {
432
- _userId = null; _itemIds = []; _modelEndpoint = null; lastCall = null; lastResult = null;
433
637
  constructor(options) {
434
- if (!options || typeof options !== 'object') throw new Error('Scoring: options required');
435
638
  const { url, apiKey, userId = null, itemIds = [], origin = 'sdk', log, show } = options;
436
- if (typeof url !== 'string' || !url.trim()) throw new Error('Scoring: url required');
437
- if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Scoring: apiKey required');
438
- this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
439
- this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
440
- this._log = typeof log === 'function' ? log : console.log.bind(console);
441
- this._show = typeof show === 'function' ? show : console.log.bind(console);
442
- if (userId != null && typeof userId === 'string' && userId.trim()) this._userId = userId.trim();
443
- if (Array.isArray(itemIds) && itemIds.length > 0) this._itemIds = itemIds.map((id) => (typeof id === 'string' ? id : String(id)));
639
+ if (!url?.trim() || !apiKey?.trim()) throw new Error('Scoring: url and apiKey required');
640
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._origin = origin?.trim() || 'sdk';
641
+ this._log = typeof log === 'function' ? log : console.log; this._show = typeof show === 'function' ? show : console.log;
642
+ this._userId = userId?.trim() || null; this._itemIds = Array.isArray(itemIds) && itemIds.length > 0 ? itemIds.map((id) => String(id)) : []; this._modelEndpoint = null;
444
643
  }
445
- getEndpoint() { if (!this._modelEndpoint?.trim()) throw new Error('Scoring: model(endpoint) required'); return this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`; }
446
- getPayload() { return { origin: this._origin, user_id: this._userId, item_ids: [...this._itemIds] }; }
447
644
  model(endpoint) { this._modelEndpoint = endpoint; return this; }
448
- userId(userId) { this._userId = userId; return this; }
449
- itemIds(itemIds) { this._itemIds = itemIds; return this; }
645
+ userId(id) { this._userId = id; return this; }
646
+ itemIds(ids) { this._itemIds = [...ids]; return this; }
450
647
  async execute() {
451
- if (!this._modelEndpoint?.trim()) throw new Error('Scoring: model(endpoint) required');
648
+ if (!this._modelEndpoint?.trim()) throw new Error('Scoring: model endpoint required');
452
649
  if (!this._userId?.trim()) throw new Error('Scoring: user_id required');
453
- if (!Array.isArray(this._itemIds) || this._itemIds.length === 0) throw new Error('Scoring: item_ids required');
454
- const url = `${this._url}${this.getEndpoint()}`;
455
- const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
456
- if (!response.ok) throw new Error(`Scoring API error: ${response.status}`);
457
- const result = await response.json();
458
- if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error));
459
- this.lastCall = { endpoint: this.getEndpoint(), payload: this.getPayload() }; this.lastResult = result;
460
- if (result.result === undefined) throw new Error('Scoring: result.result undefined'); return result.result;
650
+ if (!this._itemIds.length) throw new Error('Scoring: item_ids required');
651
+ const path = this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`;
652
+ const url = `${this._url}${path}`;
653
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify({ origin: this._origin, user_id: this._userId, item_ids: this._itemIds }) });
654
+ if (!response.ok) throw new Error(`Scoring API: ${response.status}`);
655
+ const result = await response.json(); if (result?.error != null) throw new Error(String(result.error));
656
+ return result.result;
461
657
  }
462
- log(string) { this._log(string); }
463
- show(results) { this._show(results); }
658
+ log(s) { this._log(s); }
659
+ show(r) { this._show(r); }
464
660
  }
465
661
  ```
466
662
 
467
- ---
468
-
469
- ## V1/ranking/Ranking.js
470
-
471
- ```javascript
663
+ ### V1/ranking/Ranking.js
664
+ ```js
472
665
  export class Ranking {
473
- _candidates = []; _sortMethod = 'sort'; _sortParams = null; _diversityMethod = null; _diversityParams = null;
474
- _limitsByFieldEnabled = false; _everyN = 10; _limitRules = []; lastCall = null; lastResult = null;
475
666
  constructor(options) {
476
- if (!options || typeof options !== 'object') throw new Error('Ranking: options required');
477
667
  const { url, apiKey, candidates = [], origin = 'sdk', log, show } = options;
478
- if (typeof url !== 'string' || !url.trim()) throw new Error('Ranking: url required');
479
- if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Ranking: apiKey required');
480
- this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
481
- this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
482
- this._log = typeof log === 'function' ? log : console.log.bind(console);
483
- this._show = typeof show === 'function' ? show : console.log.bind(console);
484
- this._candidates = candidates;
668
+ if (!url?.trim() || !apiKey?.trim()) throw new Error('Ranking: url and apiKey required');
669
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._origin = origin?.trim() || 'sdk'; this._candidates = candidates;
670
+ this._sortMethod = 'sort'; this._sortParams = null; this._diversityMethod = null; this._diversityParams = null; this._limitsByFieldEnabled = false; this._everyN = 10; this._limitRules = [];
671
+ this._log = typeof log === 'function' ? log : console.log; this._show = typeof show === 'function' ? show : console.log;
485
672
  }
486
- getEndpoint() { return '/ranking/feed'; }
487
- _getUsefulFieldsAndNeedEmbedding() {
488
- const useful = new Set(); let needEmbedding = false;
489
- if (this._sortParams) {
490
- if (this._sortMethod === 'sort' && Array.isArray(this._sortParams.fields)) this._sortParams.fields.forEach((f) => useful.add(f));
491
- if ((this._sortMethod === 'linear' || this._sortMethod === 'mix') && Array.isArray(this._sortParams)) this._sortParams.forEach((p) => p.field && useful.add(p.field));
492
- }
493
- if (this._diversityMethod === 'fields' && this._diversityParams?.fields) this._diversityParams.fields.forEach((f) => useful.add(f));
494
- if (this._diversityMethod === 'semantic') needEmbedding = true;
495
- if (this._limitsByFieldEnabled && this._limitRules.length) this._limitRules.forEach((r) => r.field && useful.add(r.field));
496
- return { usefulFields: useful, needEmbedding };
673
+ sortingMethod(x) {
674
+ if (!['sort','linear','mix'].includes(x)) throw new Error('sortingMethod: sort|linear|mix');
675
+ this._sortMethod = x;
676
+ if (x === 'sort') this._sortParams = { fields: [], direction: [] };
677
+ if (x === 'linear' || x === 'mix') this._sortParams = [];
678
+ return this;
497
679
  }
498
- getPayload() {
499
- const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
500
- const hits = this._candidates || [];
501
- const items = hits.map((hit) => {
502
- const item = { item_id: hit._id };
503
- for (const key of usefulFields) { const v = hit._features?.[key] ?? hit._scores?.[key]; if (v !== undefined) item[key] = v; }
504
- if (needEmbedding) { let embed = hit._source?.item_sem_embed2; if (!embed?.length) embed = hit._source?.text_vector; if (embed) item.embed = embed; }
505
- return item;
506
- });
507
- const payload = { origin: this._origin, items };
508
- const sortConfig = this._buildSortConfig(); if (sortConfig) payload.sort = sortConfig;
509
- const diversityConfig = this._buildDiversityConfig(); if (diversityConfig) payload.diversity = diversityConfig;
510
- const limitsByFieldConfig = this._buildLimitsByFieldConfig(); if (limitsByFieldConfig) payload.limits_by_field = limitsByFieldConfig;
511
- return payload;
680
+ sortBy(field, direction = 'desc', field2, direction2 = 'desc') {
681
+ if (this._sortMethod !== 'sort') throw new Error('sortBy only for sort method');
682
+ this._sortMethod = 'sort'; this._sortParams = this._sortParams || { fields: [], direction: [] };
683
+ if (!field?.trim()) throw new Error('sortBy: field required');
684
+ if (!['asc','desc'].includes(direction)) throw new Error('sortBy: asc|desc');
685
+ this._sortParams = { fields: [field.trim()], direction: [direction] };
686
+ if (field2?.trim()) { this._sortParams.fields.push(field2.trim()); this._sortParams.direction.push(direction2 === 'asc' ? 'asc' : 'desc'); }
687
+ return this;
688
+ }
689
+ weight(field, w) { if (this._sortMethod !== 'linear') throw new Error('weight only for linear'); if (!Array.isArray(this._sortParams)) this._sortParams = []; this._sortParams.push({ field: field?.trim(), weight: Number(w) }); return this; }
690
+ mix(field, direction, percentage) { if (this._sortMethod === 'linear') throw new Error('mix not for linear'); this._sortMethod = 'mix'; if (!Array.isArray(this._sortParams)) this._sortParams = []; this._sortParams.push({ field: field?.trim(), direction, percentage: Number(percentage) || 0 }); return this; }
691
+ diversity(method) { if (!['fields','semantic'].includes(method)) throw new Error('diversity: fields|semantic'); this._diversityMethod = method; this._diversityParams = method === 'fields' ? { fields: [] } : { lambda: 0.5, horizon: 20 }; return this; }
692
+ fields(arr) { if (this._diversityMethod !== 'fields') throw new Error('fields only for diversity(fields)'); if (!this._diversityParams?.fields) this._diversityParams = { fields: [] }; (Array.isArray(arr) ? arr : [arr]).forEach((n) => { const s = n?.trim(); if (s && !this._diversityParams.fields.includes(s)) this._diversityParams.fields.push(s); }); return this; }
693
+ horizon(n) { if (this._diversityMethod !== 'semantic') throw new Error('horizon only for semantic'); (this._diversityParams = this._diversityParams || {}).horizon = Number(n); return this; }
694
+ lambda(v) { if (this._diversityMethod !== 'semantic') throw new Error('lambda only for semantic'); (this._diversityParams = this._diversityParams || {}).lambda = Number(v); return this; }
695
+ limitByField() { this._limitsByFieldEnabled = true; return this; }
696
+ every(n) { this._everyN = Number(n); return this; }
697
+ limit(field, max) { const f = field?.trim(); if (!f) throw new Error('limit: field required'); const r = this._limitRules.find((x) => x.field === f); if (r) r.limit = Number(max) || 0; else this._limitRules.push({ field: f, limit: Number(max) || 0 }); return this; }
698
+ candidates(c) { this._candidates = c; return this; }
699
+ async execute() {
700
+ if (!Array.isArray(this._candidates) || !this._candidates.length) throw new Error('Ranking: candidates required');
701
+ const sortConfig = this._buildSortConfig(); if (!sortConfig) throw new Error('Ranking: sort config required');
702
+ const payload = this.getPayload(); const url = `${this._url}/ranking/feed`;
703
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
704
+ if (!response.ok) throw new Error(`Ranking API: ${response.status}`);
705
+ const result = await response.json(); if (result?.error != null) throw new Error(String(result.error));
706
+ return result.result;
512
707
  }
513
708
  _buildSortConfig() {
514
709
  if (!this._sortParams) return undefined;
@@ -528,54 +723,41 @@ export class Ranking {
528
723
  const everyN = Number(this._everyN); if (!Number.isInteger(everyN) || everyN < 2) return undefined;
529
724
  return { every_n: everyN, rules: this._limitRules.map((r) => ({ field: r.field, limit: Number(r.limit) || 0 })) };
530
725
  }
531
- sortingMethod(x) {
532
- if (x !== 'sort' && x !== 'linear' && x !== 'mix') throw new Error('Ranking.sortingMethod: sort|linear|mix');
533
- this._sortMethod = x;
534
- if (x === 'sort') this._sortParams = { fields: [], direction: [] };
535
- if (x === 'linear') this._sortParams = [];
536
- if (x === 'mix') this._sortParams = [];
537
- return this;
538
- }
539
- sortBy(field, direction = 'desc', field2, direction2 = 'desc') {
540
- if (this._sortMethod === 'linear' || this._sortMethod === 'mix') throw new Error('Ranking.sortBy: only for sortingMethod("sort")');
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');
545
- this._sortParams = { fields: [f], direction: [direction] };
546
- if (typeof field2 === 'string' && field2.trim()) { this._sortParams.fields.push(field2.trim()); this._sortParams.direction.push(direction2 === 'asc' ? 'asc' : 'desc'); }
547
- return this;
726
+ _getUsefulFieldsAndNeedEmbedding() {
727
+ const useful = new Set(); let needEmbedding = false;
728
+ if (this._sortParams) {
729
+ if (this._sortMethod === 'sort' && Array.isArray(this._sortParams.fields)) this._sortParams.fields.forEach((f) => useful.add(f));
730
+ if ((this._sortMethod === 'linear' || this._sortMethod === 'mix') && Array.isArray(this._sortParams)) this._sortParams.forEach((p) => p.field && useful.add(p.field));
731
+ }
732
+ if (this._diversityMethod === 'fields' && this._diversityParams?.fields) this._diversityParams.fields.forEach((f) => useful.add(f));
733
+ if (this._diversityMethod === 'semantic') needEmbedding = true;
734
+ if (this._limitsByFieldEnabled && this._limitRules.length) this._limitRules.forEach((r) => r.field && useful.add(r.field));
735
+ return { usefulFields: useful, needEmbedding };
548
736
  }
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; }
555
- limitByField() { this._limitsByFieldEnabled = true; return this; }
556
- every(n) { this._everyN = Number(n); 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; }
558
- candidates(candidates) { this._candidates = candidates; return this; }
559
- async execute() {
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) });
564
- if (!response.ok) throw new Error(`Ranking API error: ${response.status}`);
565
- const result = await response.json();
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;
737
+ getPayload() {
738
+ const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
739
+ const items = (this._candidates || []).map((hit) => {
740
+ const item = { item_id: hit._id };
741
+ for (const k of usefulFields) { const v = hit._features?.[k] ?? hit._scores?.[k]; if (v !== undefined) item[k] = v; }
742
+ if (needEmbedding) { let embed = hit._source?.item_sem_embed2; if (!embed?.length) embed = hit._source?.text_vector; if (embed) item.embed = embed; }
743
+ return item;
744
+ });
745
+ const payload = { origin: this._origin, items };
746
+ const sortConfig = this._buildSortConfig(); if (sortConfig) payload.sort = sortConfig;
747
+ const divConfig = this._buildDiversityConfig(); if (divConfig) payload.diversity = divConfig;
748
+ const limConfig = this._buildLimitsByFieldConfig(); if (limConfig) payload.limits_by_field = limConfig;
749
+ return payload;
568
750
  }
569
- log(string) { this._log(string); }
570
- show(results) { this._show(results); }
751
+ log(s) { this._log(s); }
752
+ show(r) { this._show(r); }
571
753
  }
572
754
  ```
573
755
 
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.
756
+ ### V1/utils/indexUtils.js
757
+ ```js
758
+ export function findIndex(index) {
759
+ const options = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets', 'kalshi-items'];
760
+ for (const opt of options) if (index?.startsWith(opt)) return opt;
761
+ return null;
762
+ }
763
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbd-studio-sdk",
3
- "version": "4.1.5",
3
+ "version": "4.2.0",
4
4
  "description": "SDK for Embed Recommendation Engine APIs",
5
5
  "keywords": [
6
6
  "web3",