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