mbd-studio-sdk 4.0.0 → 4.1.1
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 +1 -1
- package/V1/Studio.js +4 -0
- package/V1/features/Features.js +2 -1
- package/V1/ranking/Ranking.js +2 -0
- package/V1/search/Search.js +36 -0
- package/V1/search/filters/ConsoleAccountFilter.js +2 -0
- package/V1/search/filters/Filter.js +15 -0
- package/V1/search/filters/GroupBoostFilter.js +2 -0
- package/V1/search/filters/MatchFilter.js +2 -2
- package/V1/search/filters/TermsFilter.js +2 -2
- package/V1/search/filters/TermsLookupFilter.js +2 -0
- package/V1/utils/indexUtils.js +1 -0
- package/index.d.ts +218 -0
- package/index.js +7 -0
- package/llms.txt +581 -0
- package/package.json +10 -3
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;
|
package/V1/features/Features.js
CHANGED
|
@@ -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(`
|
|
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}`);
|
package/V1/ranking/Ranking.js
CHANGED
|
@@ -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 || [];
|
package/V1/search/Search.js
CHANGED
|
@@ -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,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures the value is an array. Backend expects array for terms/match filters.
|
|
3
|
+
* - Arrays are returned as-is
|
|
4
|
+
* - Strings are converted: comma-separated strings are split; single values wrapped in array
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeToArray(value) {
|
|
7
|
+
if (Array.isArray(value)) return value;
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
return value.includes(',')
|
|
10
|
+
? value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
11
|
+
: [value];
|
|
12
|
+
}
|
|
13
|
+
return value != null ? [value] : [];
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
export class Filter {
|
|
2
17
|
constructor(filterType, field, boost = null) {
|
|
3
18
|
if (new.target === Filter) throw new Error('Filter is abstract and cannot be instantiated directly');
|
|
@@ -1,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,7 +1,7 @@
|
|
|
1
|
-
import { Filter } from './Filter.js';
|
|
1
|
+
import { Filter, normalizeToArray } from './Filter.js';
|
|
2
2
|
export class MatchFilter extends Filter {
|
|
3
3
|
constructor(field, value, boost = null) {
|
|
4
4
|
super('match', field, boost);
|
|
5
|
-
this.value = value;
|
|
5
|
+
this.value = normalizeToArray(value);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Filter } from './Filter.js';
|
|
1
|
+
import { Filter, normalizeToArray } from './Filter.js';
|
|
2
2
|
export class TermsFilter extends Filter {
|
|
3
3
|
constructor(field, value, boost = null) {
|
|
4
4
|
super('terms', field, boost);
|
|
5
|
-
this.value = value;
|
|
5
|
+
this.value = normalizeToArray(value);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -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);
|
package/V1/utils/indexUtils.js
CHANGED
|
@@ -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,581 @@
|
|
|
1
|
+
# MBD Studio SDK – Reference for AI Coding Agents
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
SDK for MBD Studio backend services: **search** (candidate retrieval), **features** (signals/metadata), **scoring** (ML models for relevance/reranking), and **ranking** (final ranked feed with diversity).
|
|
6
|
+
|
|
7
|
+
## Entry Points
|
|
8
|
+
|
|
9
|
+
- `StudioConfig` – API config (apiKey required; commonUrl or servicesUrl for endpoints)
|
|
10
|
+
- `StudioV1` – Main client. Import: `import { StudioConfig, StudioV1 } from 'mbd-studio-sdk'`
|
|
11
|
+
|
|
12
|
+
## Typical Flow
|
|
13
|
+
|
|
14
|
+
1. `new StudioConfig({ apiKey })` → `new StudioV1({ config })`
|
|
15
|
+
2. `mbd.forUser(index, userId)` – set user for personalization (optional)
|
|
16
|
+
3. `mbd.search().index("...").include()/exclude()/boost().term(...).execute()` → hits
|
|
17
|
+
4. `mbd.addCandidates(hits)` – attach hits to context
|
|
18
|
+
5. `mbd.features("v1").execute()` → `mbd.addFeatures(result)` – enrich with signals
|
|
19
|
+
6. `mbd.scoring().model("/scoring/...").execute()` → `mbd.addScores(result, "key")` – ML scores
|
|
20
|
+
7. `mbd.ranking().sortingMethod(...).mix(...).execute()` → `mbd.addRanking(result)`
|
|
21
|
+
8. `mbd.getFeed()` – final ranked candidates
|
|
22
|
+
|
|
23
|
+
**Tip:** Search endpoints are chosen by payload: `es_query` > `semantic` (text/vector) > `boost` > `filter_and_sort`. Call `include()`/`exclude()`/`boost()` before adding filters. `findIndex()` maps index names (e.g. `farcaster-items-v2`) to canonical keys for feature/score lookups.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## index.js
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import * as V1 from './V1/index.js';
|
|
31
|
+
export { StudioConfig } from './StudioConfig.js';
|
|
32
|
+
export const StudioV1 = V1.Studio;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## StudioConfig.js
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const defaultBaseUrl = 'https://api.mbd.xyz/v3/studio';
|
|
41
|
+
export class StudioConfig {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
if (!options || typeof options !== 'object') throw new Error('StudioConfig: options object is required');
|
|
44
|
+
const { apiKey, commonUrl, servicesUrl, log, show } = options;
|
|
45
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('StudioConfig: apiKey is required and must be a non-empty string');
|
|
46
|
+
const hasCommonUrl = typeof commonUrl === 'string' && commonUrl.trim().length > 0;
|
|
47
|
+
if (hasCommonUrl) {
|
|
48
|
+
const url = commonUrl.trim().replace(/\/$/, '');
|
|
49
|
+
this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = url;
|
|
50
|
+
} else if (servicesUrl && typeof servicesUrl === 'object') {
|
|
51
|
+
const { searchService, storiesService, featuresService, scoringService, rankingService } = servicesUrl;
|
|
52
|
+
const services = { searchService, storiesService, featuresService, scoringService, rankingService };
|
|
53
|
+
const missing = Object.entries(services).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k);
|
|
54
|
+
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(/\/$/, ''));
|
|
57
|
+
} else {
|
|
58
|
+
this.searchService = this.storiesService = this.featuresService = this.scoringService = this.rankingService = defaultBaseUrl;
|
|
59
|
+
}
|
|
60
|
+
this.apiKey = apiKey.trim();
|
|
61
|
+
this.log = typeof log === 'function' ? log : console.log.bind(console);
|
|
62
|
+
this.show = typeof show === 'function' ? show : console.log.bind(console);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## V1/index.js
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
export { Studio } from './Studio.js';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## V1/utils/indexUtils.js
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
// Maps index names to canonical base (e.g. farcaster-items-v2 → farcaster-items). Used for feature/score keys.
|
|
81
|
+
export function findIndex(index) {
|
|
82
|
+
const indexOptions = ['farcaster-items', 'zora-coins', 'polymarket-items', 'polymarket-wallets'];
|
|
83
|
+
for (const option of indexOptions) if (index.startsWith(option)) return option;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## V1/Studio.js
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import { StudioConfig } from '../StudioConfig.js';
|
|
94
|
+
import { Search } from './search/Search.js';
|
|
95
|
+
import { Features, sortAvailableFeatures } from './features/Features.js';
|
|
96
|
+
import { Scoring } from './scoring/Scoring.js';
|
|
97
|
+
import { Ranking } from './ranking/Ranking.js';
|
|
98
|
+
import { findIndex } from './utils/indexUtils.js';
|
|
99
|
+
|
|
100
|
+
export class Studio {
|
|
101
|
+
constructor(options) {
|
|
102
|
+
if (!options || typeof options !== 'object') throw new Error('Studio: options object is required');
|
|
103
|
+
const { config, apiKey, commonUrl, servicesUrl, log, show, origin } = options;
|
|
104
|
+
this._config = config instanceof StudioConfig ? config : new StudioConfig({ commonUrl, servicesUrl, apiKey });
|
|
105
|
+
this._log = typeof log === 'function' ? log : this._config.log;
|
|
106
|
+
this._show = typeof show === 'function' ? show : this._config.show;
|
|
107
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
108
|
+
this._forUser = null;
|
|
109
|
+
this._candidates = [];
|
|
110
|
+
}
|
|
111
|
+
version() { return 'V1'; }
|
|
112
|
+
forUser(index, userId) { this._forUser = { index, id: userId }; }
|
|
113
|
+
search() { return new Search({ url: this._config.searchService, apiKey: this._config.apiKey, origin: this._origin, log: this._log, show: this._show }); }
|
|
114
|
+
async frequentValues(index, field, size = 25) { return this.search().index(index).frequentValues(field, size); }
|
|
115
|
+
addCandidates(array) { this._candidates.push(...array); }
|
|
116
|
+
features(version = 'v1') {
|
|
117
|
+
let items = [];
|
|
118
|
+
if (this._candidates?.length > 0) items = this._candidates.map((hit) => ({ index: hit._index, id: hit._id }));
|
|
119
|
+
return new Features({ url: this._config.featuresService, apiKey: this._config.apiKey, log: this._log, show: this._show, version, items, userIndex: this._forUser?.index, userId: this._forUser?.id, origin: this._origin });
|
|
120
|
+
}
|
|
121
|
+
addFeatures(featuresResult) {
|
|
122
|
+
const hits = this._candidates || [];
|
|
123
|
+
const { features, scores, info } = featuresResult;
|
|
124
|
+
if (!features && !scores && !info) { this._log('No features, scores, or info found'); return; }
|
|
125
|
+
let availableFeatures = {}, availableScores = {};
|
|
126
|
+
for (const hit of hits) {
|
|
127
|
+
const hitIndex = findIndex(hit._index);
|
|
128
|
+
const hitFeatures = features?.[hitIndex]?.[hit._id], hitScores = scores?.[hitIndex]?.[hit._id], hitInfo = info?.[hitIndex]?.[hit._id];
|
|
129
|
+
hit._features = hit._features ? { ...hit._features, ...hitFeatures } : hitFeatures;
|
|
130
|
+
hit._scores = hit._scores ? { ...hit._scores, ...hitScores } : hitScores;
|
|
131
|
+
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;
|
|
134
|
+
}
|
|
135
|
+
this._log(`Available features: ${sortAvailableFeatures(Object.keys(availableFeatures))}`);
|
|
136
|
+
this._log(`Available scores: ${sortAvailableFeatures(Object.keys(availableScores))}`);
|
|
137
|
+
}
|
|
138
|
+
scoring() {
|
|
139
|
+
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) : [];
|
|
141
|
+
return new Scoring({ url: this._config.scoringService, apiKey: this._config.apiKey, log: this._log, show: this._show, userId, itemIds, origin: this._origin });
|
|
142
|
+
}
|
|
143
|
+
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]; }
|
|
148
|
+
}
|
|
149
|
+
ranking() { return new Ranking({ url: this._config.rankingService, apiKey: this._config.apiKey, log: this._log, show: this._show, candidates: this._candidates, origin: this._origin }); }
|
|
150
|
+
addRanking(rankingResult) {
|
|
151
|
+
const rankedItems = rankingResult?.items;
|
|
152
|
+
if (!this._candidates || !Array.isArray(rankedItems)) return;
|
|
153
|
+
const scoreByItemId = {}; rankedItems.forEach(({ item_id, score }) => { scoreByItemId[item_id] = score; });
|
|
154
|
+
for (const hit of this._candidates) hit._ranking_score = scoreByItemId[hit._id];
|
|
155
|
+
this._candidates.sort((a, b) => (b._ranking_score ?? -Infinity) - (a._ranking_score ?? -Infinity));
|
|
156
|
+
}
|
|
157
|
+
log(string) { this._log(string); }
|
|
158
|
+
show(results) { this._show(results === undefined ? this._candidates : results); }
|
|
159
|
+
getFeed() { return this._candidates; }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## V1/search/Search.js
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
import { Filter, TermFilter, TermsFilter, NumericFilter, MatchFilter, GeoFilter, DateFilter, IsNullFilter, NotNullFilter, CustomFilter, GroupBoostFilter, TermsLookupFilter, ConsoleAccountFilter } from './filters/index.js';
|
|
169
|
+
|
|
170
|
+
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;
|
|
174
|
+
constructor(options) {
|
|
175
|
+
if (!options || typeof options !== 'object') throw new Error('Search: options object is required');
|
|
176
|
+
const { url, apiKey, origin = 'sdk', log, show } = options;
|
|
177
|
+
if (typeof url !== 'string' || !url.trim()) throw new Error('Search: options.url is required');
|
|
178
|
+
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);
|
|
183
|
+
}
|
|
184
|
+
getEndpoint() {
|
|
185
|
+
if (this._es_query != null) return '/search/es_query';
|
|
186
|
+
const hasTextOrVector = (typeof this._text === 'string' && this._text.length > 0) || (Array.isArray(this._vector) && this._vector.length > 0);
|
|
187
|
+
if (hasTextOrVector) return '/search/semantic';
|
|
188
|
+
if (this._boost.length > 0) return '/search/boost';
|
|
189
|
+
return '/search/filter_and_sort';
|
|
190
|
+
}
|
|
191
|
+
getPayload() {
|
|
192
|
+
const endpoint = this.getEndpoint();
|
|
193
|
+
if (endpoint === '/search/es_query') return { index: this._index, origin: this._origin, feed_type: 'es_query', query: this._es_query };
|
|
194
|
+
const feedType = endpoint === '/search/semantic' ? 'semantic' : endpoint === '/search/boost' ? 'boost' : 'filter_and_sort';
|
|
195
|
+
const serializeFilters = (arr) => arr.map((f) => ({ ...f }));
|
|
196
|
+
const payload = { index: this._index, origin: this._origin, feed_type: feedType, include_vector: this._include_vector, size: this._size, include: serializeFilters(this._include), exclude: serializeFilters(this._exclude) };
|
|
197
|
+
if (feedType === 'boost') payload.boost = serializeFilters(this._boost);
|
|
198
|
+
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
|
+
}
|
|
203
|
+
if (this._only_ids) payload.only_ids = true;
|
|
204
|
+
if (Array.isArray(this._select_fields) && this._select_fields.length > 0) payload.select_fields = this._select_fields;
|
|
205
|
+
return payload;
|
|
206
|
+
}
|
|
207
|
+
async execute() {
|
|
208
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.execute: index must be set');
|
|
209
|
+
if (this._es_query != null && (typeof this._es_query !== 'object' || Array.isArray(this._es_query))) throw new Error('Search.execute: esQuery() must be called with a plain object');
|
|
210
|
+
const hasOnlyIds = this._only_ids === true, hasSelectFields = Array.isArray(this._select_fields) && this._select_fields.length > 0, hasIncludeVector = this._include_vector === true;
|
|
211
|
+
if ((hasOnlyIds ? 1 : 0) + (hasSelectFields ? 1 : 0) + (hasIncludeVector ? 1 : 0) > 1) throw new Error('Search: onlyIds, selectFields, includeVectors are mutually exclusive');
|
|
212
|
+
const endpoint = this.getEndpoint();
|
|
213
|
+
if (endpoint === '/search/es_query' && (this._include.length || this._exclude.length || this._boost.length)) throw new Error('Search: esQuery() does not support include/exclude/boost');
|
|
214
|
+
if (endpoint === '/search/semantic') {
|
|
215
|
+
if (this._include.length || this._exclude.length || this._boost.length) throw new Error('Search: semantic does not support include/exclude/boost');
|
|
216
|
+
if (this._sort_by != null) throw new Error('Search: semantic does not support sortBy()');
|
|
217
|
+
}
|
|
218
|
+
if (endpoint === '/search/boost' && this._sort_by != null) throw new Error('Search: boost does not support sortBy()');
|
|
219
|
+
const payload = this.getPayload(), url = `${this._url}${endpoint}`;
|
|
220
|
+
this.log(`Sending request to ${url}`);
|
|
221
|
+
const startTime = performance.now();
|
|
222
|
+
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}`); }
|
|
224
|
+
const result = await response.json();
|
|
225
|
+
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;
|
|
230
|
+
}
|
|
231
|
+
async frequentValues(field, size = 25) {
|
|
232
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) throw new Error('Search.frequentValues: index must be set');
|
|
233
|
+
if (typeof field !== 'string' || !field.trim()) throw new Error('Search.frequentValues: field must be non-empty');
|
|
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}` } });
|
|
237
|
+
if (!response.ok) throw new Error(`frequentValues error: ${response.status}`);
|
|
238
|
+
const result = await response.json(); if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error)); return result;
|
|
239
|
+
}
|
|
240
|
+
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}` } });
|
|
245
|
+
if (!response.ok) throw new Error(`lookup error: ${response.status}`);
|
|
246
|
+
const result = await response.json(); if (result?.error != null) throw new Error(typeof result.error === 'string' ? result.error : String(result.error)); return result;
|
|
247
|
+
}
|
|
248
|
+
index(selected_index) { if (typeof selected_index !== 'string' || !selected_index.trim()) throw new Error('Search.index: selected_index required'); this._index = selected_index.trim(); return this; }
|
|
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; }
|
|
250
|
+
onlyIds(value) { this._only_ids = value == null ? true : Boolean(value); return this; }
|
|
251
|
+
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; }
|
|
253
|
+
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; }
|
|
257
|
+
include() { this._active_array = this._include; return this; }
|
|
258
|
+
exclude() { this._active_array = this._exclude; return this; }
|
|
259
|
+
boost() { this._active_array = this._boost; return this; }
|
|
260
|
+
_requireActiveArray() { if (this._active_array === null) throw new Error('Search: call include(), exclude(), or boost() before filters'); }
|
|
261
|
+
_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
|
+
}
|
|
268
|
+
term(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermFilter(field, value, boost)); return this; }
|
|
269
|
+
terms(field, values, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new TermsFilter(field, values, boost)); return this; }
|
|
270
|
+
numeric(field, operator, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NumericFilter(field, operator, value, boost)); return this; }
|
|
271
|
+
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; }
|
|
272
|
+
geo(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new GeoFilter(field, value, boost)); return this; }
|
|
273
|
+
match(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new MatchFilter(field, value, boost)); return this; }
|
|
274
|
+
isNull(field, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new IsNullFilter(field, boost)); return this; }
|
|
275
|
+
notNull(field, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new NotNullFilter(field, boost)); return this; }
|
|
276
|
+
custom(field, value, boost = null) { this._requireActiveArray(); this._requireBoostForBoostArray(boost); this._active_array.push(new CustomFilter(field, value, boost)); return this; }
|
|
277
|
+
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
|
+
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
|
+
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); }
|
|
281
|
+
show(results) { this._show(results); }
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## V1/search/filters/Filter.js
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
export function normalizeToArray(value) {
|
|
291
|
+
if (Array.isArray(value)) return value;
|
|
292
|
+
if (typeof value === 'string') return value.includes(',') ? value.split(',').map((s) => s.trim()).filter(Boolean) : [value];
|
|
293
|
+
return value != null ? [value] : [];
|
|
294
|
+
}
|
|
295
|
+
export class Filter {
|
|
296
|
+
constructor(filterType, field, boost = null) {
|
|
297
|
+
if (new.target === Filter) throw new Error('Filter is abstract');
|
|
298
|
+
this.filter = filterType; this.field = field; this.boost = boost;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## V1/search/filters/index.js
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
export { Filter } from './Filter.js';
|
|
309
|
+
export { TermFilter } from './TermFilter.js';
|
|
310
|
+
export { TermsFilter } from './TermsFilter.js';
|
|
311
|
+
export { NumericFilter } from './NumericFilter.js';
|
|
312
|
+
export { MatchFilter } from './MatchFilter.js';
|
|
313
|
+
export { GeoFilter } from './GeoFilter.js';
|
|
314
|
+
export { DateFilter } from './DateFilter.js';
|
|
315
|
+
export { IsNullFilter } from './IsNullFilter.js';
|
|
316
|
+
export { NotNullFilter } from './NotNullFilter.js';
|
|
317
|
+
export { CustomFilter } from './CustomFilter.js';
|
|
318
|
+
export { GroupBoostFilter } from './GroupBoostFilter.js';
|
|
319
|
+
export { TermsLookupFilter } from './TermsLookupFilter.js';
|
|
320
|
+
export { ConsoleAccountFilter } from './ConsoleAccountFilter.js';
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## V1/search/filters (implementations)
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
// TermFilter.js
|
|
329
|
+
import { Filter } from './Filter.js';
|
|
330
|
+
export class TermFilter extends Filter { constructor(field, value, boost = null) { super('term', field, boost); this.value = value; } }
|
|
331
|
+
|
|
332
|
+
// TermsFilter.js
|
|
333
|
+
import { Filter, normalizeToArray } from './Filter.js';
|
|
334
|
+
export class TermsFilter extends Filter { constructor(field, value, boost = null) { super('terms', field, boost); this.value = normalizeToArray(value); } }
|
|
335
|
+
|
|
336
|
+
// NumericFilter.js
|
|
337
|
+
import { Filter } from './Filter.js';
|
|
338
|
+
export class NumericFilter extends Filter { constructor(field, operator, value, boost = null) { super('numeric', field, boost); this.operator = operator; this.value = value; } }
|
|
339
|
+
|
|
340
|
+
// MatchFilter.js
|
|
341
|
+
import { Filter, normalizeToArray } from './Filter.js';
|
|
342
|
+
export class MatchFilter extends Filter { constructor(field, value, boost = null) { super('match', field, boost); this.value = normalizeToArray(value); } }
|
|
343
|
+
|
|
344
|
+
// DateFilter.js (dateFrom or dateTo required)
|
|
345
|
+
import { Filter } from './Filter.js';
|
|
346
|
+
export class DateFilter extends Filter { constructor(field, dateFrom = null, dateTo = null, boost = null) { super('date', field, boost); if (dateFrom == null && dateTo == null) throw new Error('DateFilter: dateFrom or dateTo required'); const value = {}; if (dateFrom != null) value.date_from = dateFrom; if (dateTo != null) value.date_to = dateTo; this.value = value; } }
|
|
347
|
+
|
|
348
|
+
// GeoFilter.js
|
|
349
|
+
import { Filter } from './Filter.js';
|
|
350
|
+
export class GeoFilter extends Filter { constructor(field, value, boost = null) { super('geo', field, boost); this.value = value; } }
|
|
351
|
+
|
|
352
|
+
// IsNullFilter.js
|
|
353
|
+
import { Filter } from './Filter.js';
|
|
354
|
+
export class IsNullFilter extends Filter { constructor(field, boost = null) { super('is_null', field, boost); } }
|
|
355
|
+
|
|
356
|
+
// NotNullFilter.js
|
|
357
|
+
import { Filter } from './Filter.js';
|
|
358
|
+
export class NotNullFilter extends Filter { constructor(field, boost = null) { super('not_null', field, boost); } }
|
|
359
|
+
|
|
360
|
+
// CustomFilter.js
|
|
361
|
+
import { Filter } from './Filter.js';
|
|
362
|
+
export class CustomFilter extends Filter { constructor(field, value, boost = null) { super('custom', field, boost); this.value = value; } }
|
|
363
|
+
|
|
364
|
+
// GroupBoostFilter.js (lookup_index, field, value, group, min_boost, max_boost, n)
|
|
365
|
+
import { Filter } from './Filter.js';
|
|
366
|
+
export class GroupBoostFilter extends Filter { constructor(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) { super('group_boost', field, null); this.lookup_index = lookup_index; this.value = value; this.group = group; this.min_boost = min_boost; this.max_boost = max_boost; this.n = n; } }
|
|
367
|
+
|
|
368
|
+
// TermsLookupFilter.js (path: dot-notation in lookup doc, e.g. "followers.ids")
|
|
369
|
+
import { Filter } from './Filter.js';
|
|
370
|
+
export class TermsLookupFilter extends Filter { constructor(lookup_index, field, value, path, boost = null) { super('terms_lookup', field, boost); this.lookup_index = lookup_index; this.value = value; this.path = path; } }
|
|
371
|
+
|
|
372
|
+
// ConsoleAccountFilter.js (path: dot-notation in account doc)
|
|
373
|
+
import { Filter } from './Filter.js';
|
|
374
|
+
export class ConsoleAccountFilter extends Filter { constructor(field, value, path, boost = null) { super('console_account', field, boost); this.value = value; this.path = path; } }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## V1/features/Features.js
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
export const PREFERRED_FEATURE_COLUMNS = ['found', 'original_rank', 'sem_sim_fuzzy', 'sem_sim_closest', 'usr_primary_labels', 'usr_secondary_labels', 'usr_primary_tags', 'usr_secondary_tags', 'user_affinity_avg', 'user_affinity_usdc', 'user_affinity_count', 'cluster_1', 'cluster_2', 'cluster_3', 'cluster_4', 'cluster_5', 'cluster_6', 'cluster_7', 'cluster_8', 'cluster_9', 'cluster_10', 'sem_sim_cluster1', 'sem_sim_cluster2', 'sem_sim_cluster3', 'sem_sim_cluster4', 'sem_sim_cluster5'];
|
|
383
|
+
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];
|
|
390
|
+
}
|
|
391
|
+
export class Features {
|
|
392
|
+
_version = 'v1'; _user = null; _items = []; lastCall = null; lastResult = null;
|
|
393
|
+
constructor(options) {
|
|
394
|
+
if (!options || typeof options !== 'object') throw new Error('Features: options required');
|
|
395
|
+
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 };
|
|
404
|
+
}
|
|
405
|
+
getEndpoint() { return `/features/${this._version}`; }
|
|
406
|
+
getPayload() { return { origin: this._origin, user: this._user, items: this._items.map((item) => ({ ...item })) }; }
|
|
407
|
+
version(v) { this._version = v; return this; }
|
|
408
|
+
items(items) { this._items = [...items]; return this; }
|
|
409
|
+
user(index, userId) { this._user = { index, id: userId }; return this; }
|
|
410
|
+
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;
|
|
420
|
+
}
|
|
421
|
+
log(string) { this._log(string); }
|
|
422
|
+
show(results) { this._show(results); }
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## V1/scoring/Scoring.js
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
export class Scoring {
|
|
432
|
+
_userId = null; _itemIds = []; _modelEndpoint = null; lastCall = null; lastResult = null;
|
|
433
|
+
constructor(options) {
|
|
434
|
+
if (!options || typeof options !== 'object') throw new Error('Scoring: options required');
|
|
435
|
+
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)));
|
|
444
|
+
}
|
|
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
|
+
model(endpoint) { this._modelEndpoint = endpoint; return this; }
|
|
448
|
+
userId(userId) { this._userId = userId; return this; }
|
|
449
|
+
itemIds(itemIds) { this._itemIds = itemIds; return this; }
|
|
450
|
+
async execute() {
|
|
451
|
+
if (!this._modelEndpoint?.trim()) throw new Error('Scoring: model(endpoint) required');
|
|
452
|
+
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;
|
|
461
|
+
}
|
|
462
|
+
log(string) { this._log(string); }
|
|
463
|
+
show(results) { this._show(results); }
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## V1/ranking/Ranking.js
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
export class Ranking {
|
|
473
|
+
_candidates = []; _sortMethod = 'sort'; _sortParams = null; _diversityMethod = null; _diversityParams = null;
|
|
474
|
+
_limitsByFieldEnabled = false; _everyN = 10; _limitRules = []; lastCall = null; lastResult = null;
|
|
475
|
+
constructor(options) {
|
|
476
|
+
if (!options || typeof options !== 'object') throw new Error('Ranking: options required');
|
|
477
|
+
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;
|
|
485
|
+
}
|
|
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 };
|
|
497
|
+
}
|
|
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;
|
|
512
|
+
}
|
|
513
|
+
_buildSortConfig() {
|
|
514
|
+
if (!this._sortParams) return undefined;
|
|
515
|
+
if (this._sortMethod === 'sort' && this._sortParams.fields?.length) return { method: 'sort', params: { ...this._sortParams } };
|
|
516
|
+
if (this._sortMethod === 'linear' && Array.isArray(this._sortParams) && this._sortParams.length) return { method: 'linear', params: this._sortParams.map((p) => ({ field: p.field, weight: p.weight })) };
|
|
517
|
+
if (this._sortMethod === 'mix' && Array.isArray(this._sortParams) && this._sortParams.length) return { method: 'mix', params: this._sortParams.map((p) => ({ field: p.field, direction: p.direction, percentage: p.percentage })) };
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
_buildDiversityConfig() {
|
|
521
|
+
if (!this._diversityMethod) return undefined;
|
|
522
|
+
if (this._diversityMethod === 'fields' && this._diversityParams?.fields?.length) return { method: 'fields', params: { fields: [...this._diversityParams.fields] } };
|
|
523
|
+
if (this._diversityMethod === 'semantic') return { method: 'semantic', params: { lambda: Number(this._diversityParams?.lambda ?? 0.5), horizon: Number(this._diversityParams?.horizon ?? 20) } };
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
_buildLimitsByFieldConfig() {
|
|
527
|
+
if (!this._limitsByFieldEnabled || !this._limitRules.length) return undefined;
|
|
528
|
+
const everyN = Number(this._everyN); if (!Number.isInteger(everyN) || everyN < 2) return undefined;
|
|
529
|
+
return { every_n: everyN, rules: this._limitRules.map((r) => ({ field: r.field, limit: Number(r.limit) || 0 })) };
|
|
530
|
+
}
|
|
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;
|
|
548
|
+
}
|
|
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;
|
|
568
|
+
}
|
|
569
|
+
log(string) { this._log(string); }
|
|
570
|
+
show(results) { this._show(results); }
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Debugging Tips
|
|
577
|
+
|
|
578
|
+
- **lastCall / lastResult**: `Search`, `Features`, `Scoring`, and `Ranking` expose `lastCall` (endpoint + payload) and `lastResult` for debugging failed requests.
|
|
579
|
+
- **Semantic diversity**: Ranking with `diversity('semantic')` needs `item_sem_embed2` or `text_vector` on hits; ensure `includeVectors(true)` when searching if you use semantic diversity.
|
|
580
|
+
- **Index canonicalization**: `addFeatures`/addScores use `findIndex(hit._index)`; if your index variant (e.g. `polymarket-items-v2`) is not in `indexOptions`, features/scores may not merge.
|
|
581
|
+
- **Filter boost**: When adding to `boost()` array, all filters except `groupBoost` require a non-null `boost` argument.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbd-studio-sdk",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
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
|
-
".":
|
|
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
|
}
|