mbd-studio-sdk 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/StudioConfig.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
2
2
 
3
-
3
+ /** API config: apiKey required; commonUrl (single base) or servicesUrl (per-service) for endpoints. */
4
4
  export class StudioConfig {
5
5
  constructor(options) {
6
6
  if (!options || typeof options !== 'object') {
package/V1/Studio.js CHANGED
@@ -5,6 +5,7 @@ import { Scoring } from './scoring/Scoring.js';
5
5
  import { Ranking } from './ranking/Ranking.js';
6
6
  import { findIndex } from './utils/indexUtils.js';
7
7
 
8
+ /** Main client: search, features, scoring, ranking. Use addCandidates() then addFeatures/addScores/addRanking to enrich. */
8
9
  export class Studio {
9
10
  constructor(options) {
10
11
  if (!options || typeof options !== 'object') {
@@ -56,6 +57,7 @@ export class Studio {
56
57
  origin: this._origin,
57
58
  });
58
59
  }
60
+ /** Merges featuresResult (features, scores, info) into hits. Keys use canonical index names (findIndex). */
59
61
  addFeatures(featuresResult) {
60
62
  const hits = this._candidates || [];
61
63
  const features = featuresResult.features;
@@ -118,6 +120,7 @@ export class Studio {
118
120
  origin: this._origin,
119
121
  });
120
122
  }
123
+ /** Maps ranked item IDs to scores (1.0 for rank 0, decreasing by rank). Merges into hit._scores[scoringKey]. */
121
124
  addScores(scoringResult, scoringKey) {
122
125
  const rankedItemIds = scoringResult;
123
126
  if (!this._candidates || !rankedItemIds || !Array.isArray(rankedItemIds)) return;
@@ -144,6 +147,7 @@ export class Studio {
144
147
  origin: this._origin,
145
148
  });
146
149
  }
150
+ /** Sets hit._ranking_score from ranking items, then sorts candidates by score descending. */
147
151
  addRanking(rankingResult) {
148
152
  const rankedItems = rankingResult?.items;
149
153
  if (!this._candidates || !rankedItems || !Array.isArray(rankedItems)) return;
@@ -1,3 +1,4 @@
1
+ /** Feature columns shown first when listing available features (relevance, affinity, clusters, semantic). */
1
2
  export const PREFERRED_FEATURE_COLUMNS = [
2
3
  'found', 'original_rank', 'sem_sim_fuzzy', 'sem_sim_closest',
3
4
  'usr_primary_labels', 'usr_secondary_labels', 'usr_primary_tags', 'usr_secondary_tags',
@@ -110,7 +111,7 @@ export class Features {
110
111
  item_embed_rate_pct: ((res.item_embed_rate ?? 0) * 100).toFixed(1),
111
112
  };
112
113
  this._log('Features result:');
113
- this._log(` took_frontend_ms: ${infos.took_frontend_ms}`);
114
+ this._log(` took_sdk_ms: ${infos.took_sdk_ms}`);
114
115
  this._log(` took_backend_ms: ${infos.took_backend_ms}`);
115
116
  this._log(` took_dynamo_user_ms: ${infos.took_dynamo_user_ms}`);
116
117
  this._log(` took_dynamo_items_ms: ${infos.took_dynamo_items_ms}`);
@@ -30,6 +30,7 @@ export class Ranking {
30
30
  getEndpoint() {
31
31
  return '/ranking/feed';
32
32
  }
33
+ /** Collects fields referenced by sort/diversity/limits; sets needEmbedding for semantic diversity. */
33
34
  _getUsefulFieldsAndNeedEmbedding() {
34
35
  const useful = new Set();
35
36
  let needEmbedding = false;
@@ -53,6 +54,7 @@ export class Ranking {
53
54
  }
54
55
  return { usefulFields: useful, needEmbedding };
55
56
  }
57
+ /** Builds items from hit._features, hit._scores; adds embed (item_sem_embed2 or text_vector) if semantic diversity. */
56
58
  getPayload() {
57
59
  const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
58
60
  const hits = this._candidates || [];
@@ -3,6 +3,7 @@ import {
3
3
  IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter,
4
4
  } from './filters/index.js';
5
5
 
6
+ /** Search builder: index(), include()/exclude()/boost(), filters, execute(). Call include/exclude/boost before adding filters. */
6
7
  export class Search {
7
8
  _index = null;
8
9
  _es_query = null;
@@ -16,6 +17,7 @@ export class Search {
16
17
  _include = [];
17
18
  _exclude = [];
18
19
  _boost = [];
20
+ /** Set by include()/exclude()/boost(); filters are pushed to whichever array is active. */
19
21
  _active_array = null;
20
22
  lastCall = null;
21
23
  lastResult = null;
@@ -36,6 +38,7 @@ export class Search {
36
38
  this._log = typeof log === 'function' ? log : console.log.bind(console);
37
39
  this._show = typeof show === 'function' ? show : console.log.bind(console);
38
40
  }
41
+ /** Endpoint selection: es_query > semantic (text/vector) > boost > filter_and_sort. */
39
42
  getEndpoint() {
40
43
  if (this._es_query != null) return '/search/es_query';
41
44
  const hasTextOrVector = (typeof this._text === 'string' && this._text.length > 0) || (Array.isArray(this._vector) && this._vector.length > 0);
@@ -76,7 +79,40 @@ export class Search {
76
79
  if (this._es_query != null && (typeof this._es_query !== 'object' || Array.isArray(this._es_query))) {
77
80
  throw new Error('Search.execute: esQuery() must be called with a plain object (e.g. { query: {...}, sort: [...] })');
78
81
  }
82
+ const hasOnlyIds = this._only_ids === true;
83
+ const hasSelectFields = Array.isArray(this._select_fields) && this._select_fields.length > 0;
84
+ const hasIncludeVector = this._include_vector === true;
85
+ const exclusiveCount = (hasOnlyIds ? 1 : 0) + (hasSelectFields ? 1 : 0) + (hasIncludeVector ? 1 : 0);
86
+ if (exclusiveCount > 1) {
87
+ throw new Error(
88
+ 'Search: onlyIds, selectFields, and includeVectors are mutually exclusive; only one may be set at a time.'
89
+ );
90
+ }
79
91
  const endpoint = this.getEndpoint();
92
+ if (endpoint === '/search/es_query') {
93
+ if (this._include.length > 0 || this._exclude.length > 0 || this._boost.length > 0) {
94
+ throw new Error(
95
+ 'Search: esQuery() does not support include(), exclude(), or boost() filters. Add filters directly in your Elasticsearch query.'
96
+ );
97
+ }
98
+ } else if (endpoint === '/search/semantic') {
99
+ if (this._include.length > 0 || this._exclude.length > 0 || this._boost.length > 0) {
100
+ throw new Error(
101
+ 'Search: semantic search does not support include(), exclude(), or boost() filters. Use filter_and_sort or boost endpoints for filtering.'
102
+ );
103
+ }
104
+ if (this._sort_by != null) {
105
+ throw new Error(
106
+ 'Search: semantic search does not support sortBy(). Results are ranked by similarity.'
107
+ );
108
+ }
109
+ } else if (endpoint === '/search/boost') {
110
+ if (this._sort_by != null) {
111
+ throw new Error(
112
+ 'Search: boost endpoint does not support sortBy(). Use filter_and_sort for sorting.'
113
+ );
114
+ }
115
+ }
80
116
  const payload = this.getPayload();
81
117
  const url = `${this._url}${endpoint}`;
82
118
  this.log(`Sending request to ${url}`);
@@ -1,4 +1,6 @@
1
1
  import { Filter } from './Filter.js';
2
+
3
+ /** Filters by console account data. path: dot-notation field in the account doc. */
2
4
  export class ConsoleAccountFilter extends Filter {
3
5
  constructor(field, value, path, boost = null) {
4
6
  super('console_account', field, boost);
@@ -1,4 +1,6 @@
1
1
  import { Filter } from './Filter.js';
2
+
3
+ /** Boosts items by group membership. lookup_index/field/value identify the group; min_boost/max_boost/n control boost range. */
2
4
  export class GroupBoostFilter extends Filter {
3
5
  constructor(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) {
4
6
  super('group_boost', field, null);
@@ -1,4 +1,6 @@
1
1
  import { Filter } from './Filter.js';
2
+
3
+ /** Filters by terms fetched from another index. path: dot-notation field in lookup doc (e.g. "followers.ids"). */
2
4
  export class TermsLookupFilter extends Filter {
3
5
  constructor(lookup_index, field, value, path, boost = null) {
4
6
  super('terms_lookup', field, boost);
@@ -1,3 +1,4 @@
1
+ /** Maps index names to canonical base (e.g. farcaster-items-v2 → farcaster-items). Used for feature/score keys. */
1
2
  export function findIndex(index) {
2
3
  const indexOptions = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets'];
3
4
  for (const option of indexOptions) {
package/index.d.ts ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * MBD Studio SDK – search, features, scoring, and ranking for personalized feeds.
3
+ * @example
4
+ * const config = new StudioConfig({ apiKey, commonUrl });
5
+ * const studio = new StudioV1({ config });
6
+ * const hits = await studio.search().index('farcaster-items').include().term('type', 'cast').execute();
7
+ */
8
+
9
+ // --- StudioConfig ---
10
+
11
+ export interface StudioConfigOptions {
12
+ apiKey: string;
13
+ commonUrl?: string;
14
+ servicesUrl?: {
15
+ searchService: string;
16
+ storiesService: string;
17
+ featuresService: string;
18
+ scoringService: string;
19
+ rankingService: string;
20
+ };
21
+ log?: (msg: string) => void;
22
+ show?: (results?: unknown) => void;
23
+ }
24
+
25
+ export class StudioConfig {
26
+ constructor(options: StudioConfigOptions);
27
+ }
28
+
29
+ // --- Filter (abstract base, used by Search.filter()) ---
30
+
31
+ export class Filter {
32
+ constructor(filterType: string, field: string, boost?: number | null);
33
+ }
34
+
35
+ // --- Search ---
36
+
37
+ export interface SearchHit {
38
+ _index?: string;
39
+ _id?: string;
40
+ _source?: Record<string, unknown>;
41
+ _features?: Record<string, number>;
42
+ _scores?: Record<string, number>;
43
+ _info?: Record<string, unknown>;
44
+ _ranking_score?: number;
45
+ }
46
+
47
+ export interface SearchResult {
48
+ total_hits?: number;
49
+ hits?: SearchHit[];
50
+ took_es?: number;
51
+ took_backend?: number;
52
+ max_score?: number;
53
+ }
54
+
55
+ export interface FrequentValuesResult {
56
+ [key: string]: unknown;
57
+ }
58
+
59
+ export class Search {
60
+ lastCall: { endpoint: string; payload: unknown } | null;
61
+ lastResult: unknown;
62
+
63
+ index(selected_index: string): this;
64
+ size(size: number): this;
65
+ onlyIds(value?: boolean): this;
66
+ includeVectors(value?: boolean): this;
67
+ selectFields(fields: string[] | null): this;
68
+ text(text: string): this;
69
+ vector(vector: number[]): this;
70
+ esQuery(rawQuery: Record<string, unknown>): this;
71
+ sortBy(field: string, direction?: 'asc' | 'desc', field2?: string, direction2?: 'asc' | 'desc'): this;
72
+ include(): this;
73
+ exclude(): this;
74
+ boost(): this;
75
+ filter(filterInstance: Filter): this;
76
+ term(field: string, value: string | number | boolean, boost?: number | null): this;
77
+ terms(field: string, values: (string | number | boolean)[], boost?: number | null): this;
78
+ numeric(field: string, operator: string, value: number, boost?: number | null): this;
79
+ date(field: string, dateFrom?: string | null, dateTo?: string | null, boost?: number | null): this;
80
+ geo(field: string, value: unknown, boost?: number | null): this;
81
+ match(field: string, value: string, boost?: number | null): this;
82
+ isNull(field: string, boost?: number | null): this;
83
+ notNull(field: string, boost?: number | null): this;
84
+ custom(field: string, value: unknown, boost?: number | null): this;
85
+ groupBoost(
86
+ lookup_index: string,
87
+ field: string,
88
+ value: unknown,
89
+ group: string,
90
+ min_boost?: number | null,
91
+ max_boost?: number | null,
92
+ n?: number | null
93
+ ): this;
94
+ termsLookup(
95
+ lookup_index: string,
96
+ field: string,
97
+ value: unknown,
98
+ path: string,
99
+ boost?: number | null
100
+ ): this;
101
+ consoleAccount(field: string, value: unknown, path: string, boost?: number | null): this;
102
+ execute(): Promise<SearchHit[]>;
103
+ frequentValues(field: string, size?: number): Promise<FrequentValuesResult>;
104
+ lookup(docId: string): Promise<unknown>;
105
+ log(string: string): void;
106
+ show(results?: unknown): void;
107
+ }
108
+
109
+ // --- Features ---
110
+
111
+ export interface FeaturesResult {
112
+ features?: Record<string, Record<string, Record<string, number>>>;
113
+ scores?: Record<string, Record<string, Record<string, number>>>;
114
+ info?: Record<string, Record<string, Record<string, unknown>>>;
115
+ took_backend?: number;
116
+ hit_rate?: number;
117
+ item_embed_rate?: number;
118
+ [key: string]: unknown;
119
+ }
120
+
121
+ export class Features {
122
+ lastCall: { endpoint: string; payload: unknown } | null;
123
+ lastResult: unknown;
124
+
125
+ version(v: string): this;
126
+ items(items: Array<{ index: string; id: string }>): this;
127
+ user(index: string, userId: string): this;
128
+ execute(): Promise<FeaturesResult>;
129
+ log(string: string): void;
130
+ show(results?: unknown): void;
131
+ }
132
+
133
+ // --- Scoring ---
134
+
135
+ export class Scoring {
136
+ lastCall: { endpoint: string; payload: unknown } | null;
137
+ lastResult: unknown;
138
+
139
+ model(endpoint: string): this;
140
+ userId(userId: string): this;
141
+ itemIds(itemIds: string[]): this;
142
+ execute(): Promise<string[]>;
143
+ log(string: string): void;
144
+ show(results?: unknown): void;
145
+ }
146
+
147
+ // --- Ranking ---
148
+
149
+ export interface RankingItem {
150
+ item_id: string;
151
+ score: number;
152
+ }
153
+
154
+ export interface RankingResult {
155
+ items: RankingItem[];
156
+ [key: string]: unknown;
157
+ }
158
+
159
+ export class Ranking {
160
+ lastCall: { endpoint: string; payload: unknown } | null;
161
+ lastResult: unknown;
162
+
163
+ sortingMethod(x: 'sort' | 'linear' | 'mix'): this;
164
+ sortBy(
165
+ field: string,
166
+ direction?: 'asc' | 'desc',
167
+ field2?: string,
168
+ direction2?: 'asc' | 'desc'
169
+ ): this;
170
+ weight(field: string, w: number): this;
171
+ mix(field: string, direction: 'asc' | 'desc', percentage: number): this;
172
+ diversity(method: 'fields' | 'semantic'): this;
173
+ fields(arrayOrItem: string | string[]): this;
174
+ horizon(n: number): this;
175
+ lambda(value: number): this;
176
+ limitByField(): this;
177
+ every(n: number): this;
178
+ limit(field: string, max: number): this;
179
+ candidates(candidates: SearchHit[]): this;
180
+ execute(): Promise<RankingResult>;
181
+ log(string: string): void;
182
+ show(results?: unknown): void;
183
+ }
184
+
185
+ // --- Studio (main client) ---
186
+
187
+ export interface StudioOptions {
188
+ config?: StudioConfig;
189
+ apiKey?: string;
190
+ commonUrl?: string;
191
+ servicesUrl?: StudioConfigOptions['servicesUrl'];
192
+ log?: (msg: string) => void;
193
+ show?: (results?: unknown) => void;
194
+ origin?: string;
195
+ }
196
+
197
+ export class Studio {
198
+ constructor(options: StudioOptions);
199
+
200
+ version(): string;
201
+ forUser(index: string, userId: string): void;
202
+ search(): Search;
203
+ frequentValues(index: string, field: string, size?: number): Promise<FrequentValuesResult>;
204
+ addCandidates(array: SearchHit[]): void;
205
+ features(version?: string): Features;
206
+ addFeatures(featuresResult: FeaturesResult): void;
207
+ scoring(): Scoring;
208
+ addScores(scoringResult: string[], scoringKey: string): void;
209
+ ranking(): Ranking;
210
+ addRanking(rankingResult: { items?: RankingItem[] }): void;
211
+ log(string: string): void;
212
+ show(results?: SearchHit[]): void;
213
+ getFeed(): SearchHit[];
214
+ }
215
+
216
+ // --- Main export ---
217
+
218
+ export const StudioV1: typeof Studio;
package/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * MBD Studio SDK – search, features, scoring, and ranking for personalized feeds.
3
+ * @example
4
+ * const config = new StudioConfig({ apiKey, commonUrl });
5
+ * const studio = new StudioV1({ config });
6
+ * const hits = await studio.search().index('farcaster-items').include().term('type', 'cast').execute();
7
+ */
1
8
  import * as V1 from './V1/index.js';
2
9
  export { StudioConfig } from './StudioConfig.js';
3
10
  export const StudioV1 = V1.Studio;
package/llms.txt ADDED
@@ -0,0 +1,484 @@
1
+ # MBD Studio SDK – AI Agent Reference
2
+
3
+ ## Introduction
4
+
5
+ **What it is:** SDK for MBD Studio backend services — search, features, scoring, and ranking for personalized feeds (Polymarket, Farcaster, Zora, etc.).
6
+
7
+ **Entry points:**
8
+ - `import { StudioConfig, StudioV1 } from 'mbd-studio-sdk'`
9
+ - Main client: `new StudioV1({ config })` where `config = new StudioConfig({ apiKey })`
10
+
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
19
+
20
+ **Search endpoint selection (auto):** es_query > semantic (text/vector) > boost > filter_and_sort. Call `include()`/`exclude()`/`boost()` before adding filters.
21
+
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.
23
+
24
+ ---
25
+
26
+ ## index.js
27
+ import * as V1 from './V1/index.js';
28
+ export { StudioConfig } from './StudioConfig.js';
29
+ export const StudioV1 = V1.Studio;
30
+
31
+ ## V1/index.js
32
+ export { Studio } from './Studio.js';
33
+
34
+ ## StudioConfig.js
35
+ const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
36
+ export class StudioConfig {
37
+ constructor(options) {
38
+ if (!options || typeof options !== 'object') throw new Error('StudioConfig: options object is required');
39
+ const { apiKey, commonUrl, servicesUrl, log, show } = options;
40
+ if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('StudioConfig: apiKey is required and must be a non-empty string');
41
+ const hasCommonUrl = typeof commonUrl === 'string' && commonUrl.trim().length > 0;
42
+ if (hasCommonUrl) {
43
+ const url = commonUrl.trim().replace(/\/$/, '');
44
+ this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = url;
45
+ } else if (servicesUrl && typeof servicesUrl === 'object') {
46
+ const { searchService, storiesService, featuresService, scoringService, rankingService } = servicesUrl;
47
+ const services = { searchService, storiesService, featuresService, scoringService, rankingService };
48
+ const missing = Object.entries(services).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k);
49
+ 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
+ } else {
56
+ this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = defaultBaseUrl;
57
+ }
58
+ this.apiKey = apiKey.trim();
59
+ this.log = typeof log === 'function' ? log : console.log.bind(console);
60
+ this.show = typeof show === 'function' ? show : console.log.bind(console);
61
+ }
62
+ }
63
+
64
+ ## V1/Studio.js
65
+ import { StudioConfig } from '../StudioConfig.js';
66
+ import { Search } from './search/Search.js';
67
+ import { Features, sortAvailableFeatures } from './features/Features.js';
68
+ import { Scoring } from './scoring/Scoring.js';
69
+ import { Ranking } from './ranking/Ranking.js';
70
+ import { findIndex } from './utils/indexUtils.js';
71
+
72
+ export class Studio {
73
+ constructor(options) {
74
+ if (!options || typeof options !== 'object') throw new Error('Studio: options object is required');
75
+ const { config, apiKey, commonUrl, servicesUrl, log, show, origin } = options;
76
+ this._config = config instanceof StudioConfig ? config : new StudioConfig({ commonUrl, servicesUrl, apiKey });
77
+ this._log = typeof log === 'function' ? log : this._config.log;
78
+ this._show = typeof show === 'function' ? show : this._config.show;
79
+ this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
80
+ this._forUser = null;
81
+ this._candidates = [];
82
+ }
83
+ version() { return 'V1'; }
84
+ forUser(index, userId) { this._forUser = { index, id: userId }; }
85
+ search() { return new Search({ url: this._config.searchService, apiKey: this._config.apiKey, origin: this._origin, log: this._log, show: this._show }); }
86
+ async frequentValues(index, field, size = 25) { return this.search().index(index).frequentValues(field, size); }
87
+ addCandidates(array) { this._candidates.push(...array); }
88
+ features(version = 'v1') {
89
+ const items = (this._candidates && this._candidates.length > 0) ? this._candidates.map((hit) => ({ index: hit._index, id: hit._id })) : [];
90
+ 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
+ }
92
+ addFeatures(featuresResult) {
93
+ const hits = this._candidates || [];
94
+ const { features, scores, info } = featuresResult;
95
+ if (!features && !scores && !info) { this._log('No features, scores, or info found'); return; }
96
+ let availableFeatures = {}, availableScores = {};
97
+ for (const hit of hits) {
98
+ const hitIndex = findIndex(hit._index);
99
+ const hitFeatures = features?.[hitIndex]?.[hit._id], hitScores = scores?.[hitIndex]?.[hit._id], hitInfo = info?.[hitIndex]?.[hit._id];
100
+ hit._features = hit._features ? { ...hit._features, ...hitFeatures } : hitFeatures;
101
+ hit._scores = hit._scores ? { ...hit._scores, ...hitScores } : hitScores;
102
+ 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;
105
+ }
106
+ this._log(`Available features: ${sortAvailableFeatures(Object.keys(availableFeatures))}`);
107
+ this._log(`Available scores: ${sortAvailableFeatures(Object.keys(availableScores))}`);
108
+ }
109
+ scoring() {
110
+ 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) : [];
112
+ return new Scoring({ url: this._config.scoringService, apiKey: this._config.apiKey, log: this._log, show: this._show, userId, itemIds, origin: this._origin });
113
+ }
114
+ addScores(scoringResult, scoringKey) {
115
+ 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
+ }
123
+ }
124
+ 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
+ addRanking(rankingResult) {
126
+ 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; });
130
+ for (const hit of this._candidates) hit._ranking_score = scoreByItemId[hit._id];
131
+ this._candidates.sort((a, b) => (b._ranking_score ?? -Infinity) - (a._ranking_score ?? -Infinity));
132
+ }
133
+ log(string) { this._log(string); }
134
+ show(results) { this._show(results === undefined ? this._candidates : results); }
135
+ getFeed() { return this._candidates; }
136
+ }
137
+
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
145
+
146
+ ## V1/search/Search.js
147
+ import { Filter, TermFilter, TermsFilter, NumericFilter, MatchFilter, GeoFilter, DateFilter, IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter } from './filters/index.js';
148
+
149
+ 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;
152
+ constructor(options) {
153
+ if (!options || typeof options !== 'object') throw new Error('Search: options object is required');
154
+ const { url, apiKey, origin = 'sdk', log, show } = options;
155
+ if (typeof url !== 'string' || !url.trim()) throw new Error('Search: options.url is required');
156
+ if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Search: options.apiKey is required');
157
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
158
+ this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
159
+ this._log = typeof log === 'function' ? log : console.log.bind(console);
160
+ this._show = typeof show === 'function' ? show : console.log.bind(console);
161
+ }
162
+ getEndpoint() {
163
+ if (this._es_query != null) return '/search/es_query';
164
+ const hasTextOrVector = (typeof this._text === 'string' && this._text.length > 0) || (Array.isArray(this._vector) && this._vector.length > 0);
165
+ if (hasTextOrVector) return '/search/semantic';
166
+ if (this._boost.length > 0) return '/search/boost';
167
+ return '/search/filter_and_sort';
168
+ }
169
+ getPayload() {
170
+ const endpoint = this.getEndpoint();
171
+ if (endpoint === '/search/es_query') return { index: this._index, origin: this._origin, feed_type: 'es_query', query: this._es_query };
172
+ const feedType = endpoint === '/search/semantic' ? 'semantic' : endpoint === '/search/boost' ? 'boost' : 'filter_and_sort';
173
+ const serializeFilters = (arr) => arr.map((f) => ({ ...f }));
174
+ 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
+ if (feedType === 'boost') payload.boost = serializeFilters(this._boost);
176
+ 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; }
178
+ if (this._only_ids) payload.only_ids = true;
179
+ if (Array.isArray(this._select_fields) && this._select_fields.length > 0) payload.select_fields = this._select_fields;
180
+ return payload;
181
+ }
182
+ async execute() {
183
+ if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.execute: index must be set');
184
+ 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
+ const hasOnlyIds = this._only_ids === true, hasSelectFields = Array.isArray(this._select_fields) && this._select_fields.length > 0, hasIncludeVector = this._include_vector === true;
186
+ 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}`;
192
+ this.log(`Sending request to ${url}`);
193
+ const startTime = performance.now();
194
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(payload) });
195
+ if (!response.ok) { const text = await response.text(); throw new Error(`Search API error: ${response.status} — ${text}`); }
196
+ 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));
198
+ result.took_sdk = Math.round(performance.now() - startTime);
199
+ 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;
202
+ }
203
+ async frequentValues(field, size = 25) {
204
+ if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.frequentValues: index must be set');
205
+ const n = Number(size); if (!Number.isInteger(n) || n <= 0) throw new Error('Search.frequentValues: size must be positive integer');
206
+ const endpoint = `/search/frequent_values/${encodeURIComponent(this._index)}/${encodeURIComponent(field.trim())}?size=${n}`;
207
+ 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;
212
+ }
213
+ async lookup(docId) {
214
+ if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.lookup: index must be set');
215
+ const endpoint = `/search/document/${encodeURIComponent(this._index)}/${encodeURIComponent(docId.trim())}`;
216
+ 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;
221
+ }
222
+ 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
+ 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; }
224
+ onlyIds(value) { this._only_ids = value == null ? true : Boolean(value); return this; }
225
+ includeVectors(value) { this._include_vector = value == null ? true : Boolean(value); return this; }
226
+ 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
+ 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; }
229
+ 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
+ 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
+ include() { this._active_array = this._include; return this; }
232
+ exclude() { this._active_array = this._exclude; return this; }
233
+ boost() { this._active_array = this._boost; return this; }
234
+ _requireActiveArray() { if (this._active_array === null) throw new Error('Search: call include(), exclude(), or boost() before filters'); }
235
+ _requireBoostForBoostArray(boost) { if (this._active_array === this._boost && boost == null) throw new Error('Search: boost array requires non-null boost'); }
236
+ filter(filterInstance) {
237
+ this._requireActiveArray();
238
+ 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');
240
+ this._active_array.push(filterInstance); return this;
241
+ }
242
+ term(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermFilter(field, value, boost)); return this; }
243
+ terms(field, values, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermsFilter(field, values, boost)); return this; }
244
+ numeric(field, operator, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NumericFilter(field, operator, value, boost)); return this; }
245
+ date(field, dateFrom = null, dateTo = null, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new DateFilter(field, dateFrom, dateTo, boost)); return this; }
246
+ geo(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new GeoFilter(field, value, boost)); return this; }
247
+ match(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new MatchFilter(field, value, boost)); return this; }
248
+ isNull(field, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new IsNullFilter(field, boost)); return this; }
249
+ notNull(field, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NotNullFilter(field, boost)); return this; }
250
+ custom(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new CustomFilter(field, value, boost)); return this; }
251
+ 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; }
252
+ 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; }
253
+ consoleAccount(field, value, path, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new ConsoleAccountFilter(field, value, path, boost)); return this; }
254
+ log(string) { this._log(string); }
255
+ show(results) { this._show(results); }
256
+ }
257
+
258
+ ## V1/search/filters/Filter.js
259
+ export class Filter {
260
+ constructor(filterType, field, boost = null) {
261
+ if (new.target === Filter) throw new Error('Filter is abstract');
262
+ this.filter = filterType; this.field = field; this.boost = boost;
263
+ }
264
+ }
265
+
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
276
+
277
+ ## 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'];
279
+ export function sortAvailableFeatures(available) {
280
+ const preferred = PREFERRED_FEATURE_COLUMNS.filter((col) => available.includes(col));
281
+ 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();
285
+ return [...preferred, ...regular, ...aiColumns, ...tagColumns];
286
+ }
287
+ export class Features {
288
+ _version = 'v1'; _user = null; _items = []; lastCall = null; lastResult = null;
289
+ constructor(options) {
290
+ if (!options || typeof options !== 'object') throw new Error('Features: options required');
291
+ const { url, apiKey, version = 'v1', items = [], userIndex, userId, origin = 'sdk', log, show } = options;
292
+ if (typeof url !== 'string' || !url.trim()) throw new Error('Features: url required');
293
+ if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Features: apiKey required');
294
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim(); this._version = version;
295
+ this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
296
+ this._log = typeof log === 'function' ? log : console.log.bind(console);
297
+ this._show = typeof show === 'function' ? show : console.log.bind(console);
298
+ if (Array.isArray(items) && items.length > 0) this._items = items;
299
+ if (userIndex != null && userId != null) this._user = { index: userIndex, id: userId };
300
+ }
301
+ getEndpoint() { return `/features/${this._version}`; }
302
+ getPayload() { return { origin: this._origin, user: this._user, items: this._items.map((item) => ({ ...item })) }; }
303
+ version(v) { this._version = v; return this; }
304
+ items(items) { this._items = [...items]; return this; }
305
+ user(index, userId) { this._user = { index, id: userId }; return this; }
306
+ 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');
309
+ const url = `${this._url}${this.getEndpoint()}`;
310
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
311
+ if (!response.ok) throw new Error(`Features API error: ${response.status}`);
312
+ 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));
314
+ 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;
317
+ }
318
+ log(string) { this._log(string); }
319
+ show(results) { this._show(results); }
320
+ }
321
+
322
+ ## V1/scoring/Scoring.js
323
+ export class Scoring {
324
+ _userId = null; _itemIds = []; _modelEndpoint = null; lastCall = null; lastResult = null;
325
+ constructor(options) {
326
+ if (!options || typeof options !== 'object') throw new Error('Scoring: options required');
327
+ const { url, apiKey, userId = null, itemIds = [], origin = 'sdk', log, show } = options;
328
+ if (typeof url !== 'string' || !url.trim()) throw new Error('Scoring: url required');
329
+ if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Scoring: apiKey required');
330
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
331
+ this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
332
+ this._log = typeof log === 'function' ? log : console.log.bind(console);
333
+ this._show = typeof show === 'function' ? show : console.log.bind(console);
334
+ if (userId != null && typeof userId === 'string' && userId.trim()) this._userId = userId.trim();
335
+ if (Array.isArray(itemIds) && itemIds.length > 0) this._itemIds = itemIds.map((id) => (typeof id === 'string' ? id : String(id)));
336
+ }
337
+ getEndpoint() { if (!this._modelEndpoint || !this._modelEndpoint.trim()) throw new Error('Scoring: model(endpoint) required'); return this._modelEndpoint.startsWith('/') ? this._modelEndpoint : `/${this._modelEndpoint}`; }
338
+ getPayload() { return { origin: this._origin, user_id: this._userId, item_ids: [...this._itemIds] }; }
339
+ model(endpoint) { this._modelEndpoint = endpoint; return this; }
340
+ userId(userId) { this._userId = userId; return this; }
341
+ itemIds(itemIds) { this._itemIds = itemIds; return this; }
342
+ 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');
345
+ if (!Array.isArray(this._itemIds) || this._itemIds.length === 0) throw new Error('Scoring: item_ids required');
346
+ const url = `${this._url}${this.getEndpoint()}`;
347
+ const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` }, body: JSON.stringify(this.getPayload()) });
348
+ if (!response.ok) throw new Error(`Scoring API error: ${response.status}`);
349
+ 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));
351
+ 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;
354
+ }
355
+ log(string) { this._log(string); }
356
+ show(results) { this._show(results); }
357
+ }
358
+
359
+ ## V1/ranking/Ranking.js
360
+ export class Ranking {
361
+ _candidates = []; _sortMethod = 'sort'; _sortParams = null; _diversityMethod = null; _diversityParams = null; _limitsByFieldEnabled = false; _everyN = 10; _limitRules = []; lastCall = null; lastResult = null;
362
+ constructor(options) {
363
+ if (!options || typeof options !== 'object') throw new Error('Ranking: options required');
364
+ const { url, apiKey, candidates = [], origin = 'sdk', log, show } = options;
365
+ if (typeof url !== 'string' || !url.trim()) throw new Error('Ranking: url required');
366
+ if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Ranking: apiKey required');
367
+ this._url = url.trim().replace(/\/$/, ''); this._apiKey = apiKey.trim();
368
+ this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
369
+ this._log = typeof log === 'function' ? log : console.log.bind(console);
370
+ this._show = typeof show === 'function' ? show : console.log.bind(console);
371
+ this._candidates = candidates;
372
+ }
373
+ getEndpoint() { return '/ranking/feed'; }
374
+ _getUsefulFieldsAndNeedEmbedding() {
375
+ const useful = new Set(); let needEmbedding = false;
376
+ if (this._sortParams) {
377
+ if (this._sortMethod === 'sort' && Array.isArray(this._sortParams.fields)) this._sortParams.fields.forEach((f) => useful.add(f));
378
+ if ((this._sortMethod === 'linear' || this._sortMethod === 'mix') && Array.isArray(this._sortParams)) this._sortParams.forEach((p) => p.field && useful.add(p.field));
379
+ }
380
+ if (this._diversityMethod === 'fields' && this._diversityParams?.fields) this._diversityParams.fields.forEach((f) => useful.add(f));
381
+ if (this._diversityMethod === 'semantic') needEmbedding = true;
382
+ if (this._limitsByFieldEnabled && this._limitRules.length > 0) this._limitRules.forEach((r) => r.field && useful.add(r.field));
383
+ return { usefulFields: useful, needEmbedding };
384
+ }
385
+ getPayload() {
386
+ const { usefulFields, needEmbedding } = this._getUsefulFieldsAndNeedEmbedding();
387
+ const hits = this._candidates || [];
388
+ const items = hits.map((hit) => {
389
+ const item = { item_id: hit._id };
390
+ 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; }
392
+ return item;
393
+ });
394
+ const payload = { origin: this._origin, items };
395
+ const sortConfig = this._buildSortConfig(); if (sortConfig) payload.sort = sortConfig;
396
+ const diversityConfig = this._buildDiversityConfig(); if (diversityConfig) payload.diversity = diversityConfig;
397
+ const limitsByFieldConfig = this._buildLimitsByFieldConfig(); if (limitsByFieldConfig) payload.limits_by_field = limitsByFieldConfig;
398
+ return payload;
399
+ }
400
+ _buildSortConfig() {
401
+ 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 })) };
405
+ return undefined;
406
+ }
407
+ _buildDiversityConfig() {
408
+ if (!this._diversityMethod) return undefined;
409
+ if (this._diversityMethod === 'fields' && this._diversityParams?.fields?.length > 0) return { method: 'fields', params: { fields: [...this._diversityParams.fields] } };
410
+ if (this._diversityMethod === 'semantic') return { method: 'semantic', params: { lambda: Number(this._diversityParams?.lambda ?? 0.5), horizon: Number(this._diversityParams?.horizon ?? 20) } };
411
+ return undefined;
412
+ }
413
+ _buildLimitsByFieldConfig() {
414
+ if (!this._limitsByFieldEnabled || !this._limitRules.length) return undefined;
415
+ const everyN = Number(this._everyN); if (!Number.isInteger(everyN) || everyN < 2) return undefined;
416
+ return { every_n: everyN, rules: this._limitRules.map((r) => ({ field: r.field, limit: Number(r.limit) || 0 })) };
417
+ }
418
+ sortingMethod(x) {
419
+ if (x !== 'sort' && x !== 'linear' && x !== 'mix') throw new Error('Ranking.sortingMethod: sort|linear|mix');
420
+ this._sortMethod = x;
421
+ if (x === 'sort') this._sortParams = { fields: [], direction: [] };
422
+ if (x === 'linear' || x === 'mix') this._sortParams = [];
423
+ return this;
424
+ }
425
+ sortBy(field, direction = 'desc', field2, direction2 = 'desc') {
426
+ 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');
430
+ this._sortParams = { fields: [f], direction: [direction] };
431
+ if (typeof field2 === 'string' && field2.trim()) { this._sortParams.fields.push(field2.trim()); this._sortParams.direction.push(direction2 === 'asc' ? 'asc' : 'desc'); }
432
+ return this;
433
+ }
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; }
458
+ limitByField() { this._limitsByFieldEnabled = true; return this; }
459
+ 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; }
461
+ candidates(candidates) { this._candidates = candidates; return this; }
462
+ 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()) });
467
+ if (!response.ok) throw new Error(`Ranking API error: ${response.status}`);
468
+ 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;
473
+ }
474
+ log(string) { this._log(string); }
475
+ show(results) { this._show(results); }
476
+ }
477
+
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbd-studio-sdk",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "SDK for Embed Recommendation Engine APIs",
5
5
  "keywords": [
6
6
  "web3",
@@ -12,12 +12,19 @@
12
12
  ],
13
13
  "type": "module",
14
14
  "main": "index.js",
15
+ "types": "index.d.ts",
15
16
  "exports": {
16
- ".": "./index.js"
17
+ ".": {
18
+ "types": "./index.d.ts",
19
+ "import": "./index.js",
20
+ "default": "./index.js"
21
+ }
17
22
  },
18
23
  "files": [
19
24
  "index.js",
25
+ "index.d.ts",
20
26
  "StudioConfig.js",
21
- "V1"
27
+ "V1",
28
+ "llms.txt"
22
29
  ]
23
30
  }