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