mbd-studio-sdk 2.0.3
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 +262 -0
- package/V1/features/Features.js +249 -0
- package/V1/index.js +1 -0
- package/V1/ranking/Ranking.js +477 -0
- package/V1/scoring/Scoring.js +187 -0
- package/V1/search/Search.js +673 -0
- package/V1/search/filters/ConsoleAccountFilter.js +18 -0
- package/V1/search/filters/CustomFilter.js +16 -0
- package/V1/search/filters/DateFilter.js +23 -0
- package/V1/search/filters/Filter.js +20 -0
- package/V1/search/filters/GeoFilter.js +16 -0
- package/V1/search/filters/GroupBoostFilter.js +26 -0
- package/V1/search/filters/IsNullFilter.js +14 -0
- package/V1/search/filters/MatchFilter.js +16 -0
- package/V1/search/filters/NotNullFilter.js +14 -0
- package/V1/search/filters/NumericFilter.js +18 -0
- package/V1/search/filters/TermFilter.js +16 -0
- package/V1/search/filters/TermsFilter.js +16 -0
- package/V1/search/filters/TermsLookupFilter.js +20 -0
- package/V1/search/filters/index.js +13 -0
- package/V1/utils/indexUtils.js +12 -0
- package/index.js +4 -0
- package/package.json +14 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{ field: string, order: "asc" | "desc" }} SortBy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Filter,
|
|
7
|
+
TermFilter,
|
|
8
|
+
TermsFilter,
|
|
9
|
+
NumericFilter,
|
|
10
|
+
MatchFilter,
|
|
11
|
+
GeoFilter,
|
|
12
|
+
DateFilter,
|
|
13
|
+
IsNullFilter,
|
|
14
|
+
NotNullFilter,
|
|
15
|
+
CustomFilter,
|
|
16
|
+
GroupBoostFilter,
|
|
17
|
+
TermsLookupFilter,
|
|
18
|
+
ConsoleAccountFilter,
|
|
19
|
+
} from './filters/index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Search client for MBD Studio SDK.
|
|
23
|
+
* Fluent API: methods return the instance for chaining.
|
|
24
|
+
*/
|
|
25
|
+
export class Search {
|
|
26
|
+
/** @type {string} */
|
|
27
|
+
_url;
|
|
28
|
+
|
|
29
|
+
/** @type {string} */
|
|
30
|
+
_apiKey;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} options
|
|
35
|
+
* @param {string} options.url - Search service base URL
|
|
36
|
+
* @param {string} options.apiKey - API key for authentication
|
|
37
|
+
* @param {string} [options.origin='sdk'] - origin sent in backend call payloads
|
|
38
|
+
* @param {function(string): void} [options.log] - Optional override for log(); defaults to console.log
|
|
39
|
+
* @param {function(*): void} [options.show] - Optional override for show(); defaults to console.log
|
|
40
|
+
*/
|
|
41
|
+
constructor(options) {
|
|
42
|
+
if (!options || typeof options !== 'object') {
|
|
43
|
+
throw new Error('Search: options object is required');
|
|
44
|
+
}
|
|
45
|
+
const { url, apiKey, origin = 'sdk', log, show } = options;
|
|
46
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
47
|
+
throw new Error('Search: options.url is required and must be a non-empty string');
|
|
48
|
+
}
|
|
49
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) {
|
|
50
|
+
throw new Error('Search: options.apiKey is required and must be a non-empty string');
|
|
51
|
+
}
|
|
52
|
+
this._url = url.trim().replace(/\/$/, '');
|
|
53
|
+
this._apiKey = apiKey.trim();
|
|
54
|
+
this._origin = typeof origin === 'string' && origin.trim() ? origin.trim() : 'sdk';
|
|
55
|
+
this._log = typeof log === 'function' ? log : console.log.bind(console);
|
|
56
|
+
this._show = typeof show === 'function' ? show : console.log.bind(console);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @type {string | null} */
|
|
60
|
+
_index = null;
|
|
61
|
+
|
|
62
|
+
/** @type {object | null} - Raw ES query for es_query endpoint */
|
|
63
|
+
_es_query = null;
|
|
64
|
+
|
|
65
|
+
/** @type {number} */
|
|
66
|
+
_size = 100;
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} */
|
|
69
|
+
_only_ids = false;
|
|
70
|
+
|
|
71
|
+
/** @type {boolean} */
|
|
72
|
+
_include_vector = false;
|
|
73
|
+
|
|
74
|
+
/** @type {string[] | null} */
|
|
75
|
+
_select_fields = null;
|
|
76
|
+
|
|
77
|
+
/** @type {string | null} */
|
|
78
|
+
_text = null;
|
|
79
|
+
|
|
80
|
+
/** @type {number[] | null} */
|
|
81
|
+
_vector = null;
|
|
82
|
+
|
|
83
|
+
/** @type {SortBy | null} */
|
|
84
|
+
_sort_by = null;
|
|
85
|
+
|
|
86
|
+
/** @type {Filter[]} */
|
|
87
|
+
_include = [];
|
|
88
|
+
|
|
89
|
+
/** @type {Filter[]} */
|
|
90
|
+
_exclude = [];
|
|
91
|
+
|
|
92
|
+
/** @type {Filter[]} */
|
|
93
|
+
_boost = [];
|
|
94
|
+
|
|
95
|
+
/** @type {Filter[] | null} */
|
|
96
|
+
_active_array = null;
|
|
97
|
+
|
|
98
|
+
/** @type {{ endpoint: string, payload: object } | null} */
|
|
99
|
+
lastCall = null;
|
|
100
|
+
|
|
101
|
+
/** @type {object | null} */
|
|
102
|
+
lastResult = null;
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Return the endpoint path for the current query configuration.
|
|
107
|
+
* es_query if set; semantic if text or vector is set; boost if any boost filter; otherwise filter_and_sort.
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
getEndpoint() {
|
|
111
|
+
if (this._es_query != null) return '/search/es_query';
|
|
112
|
+
const hasTextOrVector =
|
|
113
|
+
(typeof this._text === 'string' && this._text.length > 0) ||
|
|
114
|
+
(Array.isArray(this._vector) && this._vector.length > 0);
|
|
115
|
+
if (hasTextOrVector) return '/search/semantic';
|
|
116
|
+
if (this._boost.length > 0) return '/search/boost';
|
|
117
|
+
return '/search/filter_and_sort';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the request payload for the current query.
|
|
122
|
+
* @returns {object}
|
|
123
|
+
*/
|
|
124
|
+
getPayload() {
|
|
125
|
+
const endpoint = this.getEndpoint();
|
|
126
|
+
if (endpoint === '/search/es_query') {
|
|
127
|
+
return {
|
|
128
|
+
index: this._index,
|
|
129
|
+
origin: this._origin,
|
|
130
|
+
feed_type: 'es_query',
|
|
131
|
+
query: this._es_query,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const feedType =
|
|
135
|
+
endpoint === '/search/semantic'
|
|
136
|
+
? 'semantic'
|
|
137
|
+
: endpoint === '/search/boost'
|
|
138
|
+
? 'boost'
|
|
139
|
+
: 'filter_and_sort';
|
|
140
|
+
|
|
141
|
+
const serializeFilters = (arr) => arr.map((f) => ({ ...f }));
|
|
142
|
+
|
|
143
|
+
const payload = {
|
|
144
|
+
index: this._index,
|
|
145
|
+
origin: this._origin,
|
|
146
|
+
feed_type: feedType,
|
|
147
|
+
include_vector: this._include_vector,
|
|
148
|
+
size: this._size,
|
|
149
|
+
include: serializeFilters(this._include),
|
|
150
|
+
exclude: serializeFilters(this._exclude),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (feedType === 'boost') {
|
|
154
|
+
payload.boost = serializeFilters(this._boost);
|
|
155
|
+
}
|
|
156
|
+
if (feedType === 'filter_and_sort' && this._sort_by) {
|
|
157
|
+
payload.sort_by = this._sort_by;
|
|
158
|
+
}
|
|
159
|
+
if (feedType === 'semantic') {
|
|
160
|
+
if (typeof this._text === 'string' && this._text.length > 0) payload.text = this._text;
|
|
161
|
+
if (Array.isArray(this._vector) && this._vector.length > 0) payload.vector = this._vector;
|
|
162
|
+
}
|
|
163
|
+
if (this._only_ids) payload.only_ids = true;
|
|
164
|
+
if (Array.isArray(this._select_fields) && this._select_fields.length > 0) {
|
|
165
|
+
payload.select_fields = this._select_fields;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return payload;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Execute the search request. Sets lastCall and lastResult on success; throws on error.
|
|
173
|
+
* @returns {Promise<object>}
|
|
174
|
+
*/
|
|
175
|
+
async execute() {
|
|
176
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) {
|
|
177
|
+
throw new Error('Search.execute: index must be set (call index(name) first)');
|
|
178
|
+
}
|
|
179
|
+
if (this._es_query != null && (typeof this._es_query !== 'object' || Array.isArray(this._es_query))) {
|
|
180
|
+
throw new Error('Search.execute: esQuery() must be called with a plain object (e.g. { query: {...}, sort: [...] })');
|
|
181
|
+
}
|
|
182
|
+
const endpoint = this.getEndpoint();
|
|
183
|
+
const payload = this.getPayload();
|
|
184
|
+
const url = `${this._url}${endpoint}`;
|
|
185
|
+
this.log(`Sending request to ${url}`);
|
|
186
|
+
this.log(`Payload:\n${JSON.stringify(payload, null, 2)}`);
|
|
187
|
+
const startTime = performance.now();
|
|
188
|
+
const response = await fetch(url, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
'Content-Type': 'application/json',
|
|
192
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(payload),
|
|
195
|
+
});
|
|
196
|
+
const frontendTime = Math.round(performance.now() - startTime);
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const text = await response.text();
|
|
199
|
+
let message = `Search API error: ${response.status} ${response.statusText}`;
|
|
200
|
+
if (text) message += ` — ${text}`;
|
|
201
|
+
this.log(message);
|
|
202
|
+
throw new Error(message);
|
|
203
|
+
}
|
|
204
|
+
const result = await response.json();
|
|
205
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) {
|
|
206
|
+
const msg = typeof result.error === 'string' ? result.error : String(result.error);
|
|
207
|
+
this.log(msg);
|
|
208
|
+
throw new Error(msg);
|
|
209
|
+
}
|
|
210
|
+
result.took_frontend = frontendTime;
|
|
211
|
+
this.lastCall = { endpoint, payload };
|
|
212
|
+
this.lastResult = result;
|
|
213
|
+
|
|
214
|
+
if (!result.result) {
|
|
215
|
+
throw new Error('Search.execute: result.result is undefined');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Log query infos (same as InfoColumn.js)
|
|
219
|
+
const res = result.result ;
|
|
220
|
+
const infos = {
|
|
221
|
+
total_hits: res?.total_hits ?? 0,
|
|
222
|
+
fetched_hits: res?.hits?.length ?? 0,
|
|
223
|
+
took_es_ms: res?.took_es ?? 0,
|
|
224
|
+
took_backend_ms: res?.took_backend ?? 0,
|
|
225
|
+
took_frontend_ms: result.took_frontend,
|
|
226
|
+
max_score: res?.max_score ?? 0,
|
|
227
|
+
};
|
|
228
|
+
this._log('Search result:');
|
|
229
|
+
this._log(` total_hits: ${infos.total_hits}`);
|
|
230
|
+
this._log(` fetched_hits: ${infos.fetched_hits}`);
|
|
231
|
+
this._log(` took_es_ms: ${infos.took_es_ms}`);
|
|
232
|
+
this._log(` took_backend_ms: ${infos.took_backend_ms}`);
|
|
233
|
+
this._log(` took_frontend_ms: ${infos.took_frontend_ms}`);
|
|
234
|
+
this._log(` max_score: ${infos.max_score}`);
|
|
235
|
+
return res.hits;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Look up a document by ID. Uses the index set by index().
|
|
240
|
+
* @param {string} docId - Document ID
|
|
241
|
+
* @returns {Promise<object>} Full response (use result.result for the document)
|
|
242
|
+
*/
|
|
243
|
+
async lookup(docId) {
|
|
244
|
+
if (!this._index || typeof this._index !== 'string' || !this._index.trim()) {
|
|
245
|
+
throw new Error('Search.lookup: index must be set (call index(name) first)');
|
|
246
|
+
}
|
|
247
|
+
if (typeof docId !== 'string' || !docId.trim()) {
|
|
248
|
+
throw new Error('Search.lookup: docId must be a non-empty string');
|
|
249
|
+
}
|
|
250
|
+
const endpoint = `/search/document/${encodeURIComponent(this._index)}/${encodeURIComponent(docId.trim())}`;
|
|
251
|
+
const url = `${this._url}${endpoint}`;
|
|
252
|
+
this.log(`GET ${url}`);
|
|
253
|
+
const startTime = performance.now();
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: 'GET',
|
|
256
|
+
headers: {
|
|
257
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const frontendTime = Math.round(performance.now() - startTime);
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
const text = await response.text();
|
|
263
|
+
let message = `Search lookup error: ${response.status} ${response.statusText}`;
|
|
264
|
+
if (text) message += ` — ${text}`;
|
|
265
|
+
this.log(message);
|
|
266
|
+
throw new Error(message);
|
|
267
|
+
}
|
|
268
|
+
const result = await response.json();
|
|
269
|
+
if (result && typeof result.error !== 'undefined' && result.error !== null) {
|
|
270
|
+
const msg = typeof result.error === 'string' ? result.error : String(result.error);
|
|
271
|
+
this.log(msg);
|
|
272
|
+
throw new Error(msg);
|
|
273
|
+
}
|
|
274
|
+
result.took_frontend = frontendTime;
|
|
275
|
+
this.lastCall = { endpoint, payload: null };
|
|
276
|
+
this.lastResult = result;
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Set the index to search.
|
|
282
|
+
* @param {string} selected_index - Index name to search
|
|
283
|
+
* @returns {this}
|
|
284
|
+
*/
|
|
285
|
+
index(selected_index) {
|
|
286
|
+
if (typeof selected_index !== 'string' || !selected_index.trim()) {
|
|
287
|
+
throw new Error('Search.index: selected_index must be a non-empty string');
|
|
288
|
+
}
|
|
289
|
+
this._index = selected_index.trim();
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Set the number of results to return.
|
|
295
|
+
* @param {number} size - Number of results (must be > 0 and < 2000)
|
|
296
|
+
* @returns {this}
|
|
297
|
+
*/
|
|
298
|
+
size(size) {
|
|
299
|
+
const n = Number(size);
|
|
300
|
+
if (!Number.isInteger(n) || n <= 0 || n >= 2000) {
|
|
301
|
+
throw new Error('Search.size: size must be an integer > 0 and < 2000');
|
|
302
|
+
}
|
|
303
|
+
this._size = n;
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Set whether to return only IDs in results. Call with no argument or true to enable, false to disable.
|
|
309
|
+
* @param {boolean} [value=true] - When omitted, sets to true
|
|
310
|
+
* @returns {this}
|
|
311
|
+
*/
|
|
312
|
+
onlyIds(value) {
|
|
313
|
+
this._only_ids = value == null ? true : Boolean(value);
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Set whether to include vectors in results. Call with no argument or true to enable, false to disable.
|
|
319
|
+
* @param {boolean} [value=true] - When omitted, sets to true
|
|
320
|
+
* @returns {this}
|
|
321
|
+
*/
|
|
322
|
+
includeVectors(value) {
|
|
323
|
+
this._include_vector = value == null ? true : Boolean(value);
|
|
324
|
+
return this;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Set which fields to include in results. Pass an array of field names; pass null to clear.
|
|
329
|
+
* @param {string[] | null} fields - Array of field names, or null
|
|
330
|
+
* @returns {this}
|
|
331
|
+
*/
|
|
332
|
+
selectFields(fields) {
|
|
333
|
+
if (fields === null) {
|
|
334
|
+
this._select_fields = null;
|
|
335
|
+
return this;
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(fields)) {
|
|
338
|
+
throw new Error('Search.selectFields: fields must be an array of strings or null');
|
|
339
|
+
}
|
|
340
|
+
const normalized = fields.map((f) => (typeof f === 'string' ? f.trim() : String(f)));
|
|
341
|
+
if (normalized.some((f) => !f)) {
|
|
342
|
+
throw new Error('Search.selectFields: each field must be a non-empty string');
|
|
343
|
+
}
|
|
344
|
+
this._select_fields = normalized;
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Set the text query for search.
|
|
350
|
+
* @param {string} text - Text to search
|
|
351
|
+
* @returns {this}
|
|
352
|
+
*/
|
|
353
|
+
text(text) {
|
|
354
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
355
|
+
throw new Error('Search.text: text must be a non-empty string');
|
|
356
|
+
}
|
|
357
|
+
this._text = text.trim();
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Set the vector for vector search.
|
|
363
|
+
* @param {number[]} vector - Vector embedding
|
|
364
|
+
* @returns {this}
|
|
365
|
+
*/
|
|
366
|
+
vector(vector) {
|
|
367
|
+
if (!Array.isArray(vector)) {
|
|
368
|
+
throw new Error('Search.vector: vector must be an array of numbers');
|
|
369
|
+
}
|
|
370
|
+
this._vector = vector;
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Set a raw Elasticsearch query for the es_query endpoint. Call index() before execute().
|
|
376
|
+
* @param {object} rawQuery - Raw ES query object (e.g. { query: {...}, sort: [...], size: 10 })
|
|
377
|
+
* @returns {this}
|
|
378
|
+
*/
|
|
379
|
+
esQuery(rawQuery) {
|
|
380
|
+
if (rawQuery == null || typeof rawQuery !== 'object' || Array.isArray(rawQuery)) {
|
|
381
|
+
throw new Error('Search.esQuery: rawQuery must be a plain object');
|
|
382
|
+
}
|
|
383
|
+
this._es_query = rawQuery;
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Set sort by field and direction.
|
|
389
|
+
* @param {string} field - Field name to sort by
|
|
390
|
+
* @param {"asc" | "desc"} [direction="desc"] - Sort direction
|
|
391
|
+
* @returns {this}
|
|
392
|
+
*/
|
|
393
|
+
sortBy(field, direction = 'desc') {
|
|
394
|
+
if (typeof field !== 'string' || !field.trim()) {
|
|
395
|
+
throw new Error('Search.sortBy: field must be a non-empty string');
|
|
396
|
+
}
|
|
397
|
+
if (direction !== 'asc' && direction !== 'desc') {
|
|
398
|
+
throw new Error('Search.sortBy: direction must be "asc" or "desc"');
|
|
399
|
+
}
|
|
400
|
+
this._sort_by = { field: field.trim(), order: direction };
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Set the active array to include. Subsequent add operations will target _include.
|
|
406
|
+
* @returns {this}
|
|
407
|
+
*/
|
|
408
|
+
include() {
|
|
409
|
+
this._active_array = this._include;
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Set the active array to exclude. Subsequent add operations will target _exclude.
|
|
415
|
+
* @returns {this}
|
|
416
|
+
*/
|
|
417
|
+
exclude() {
|
|
418
|
+
this._active_array = this._exclude;
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Set the active array to boost. Subsequent add operations will target _boost.
|
|
424
|
+
* @returns {this}
|
|
425
|
+
*/
|
|
426
|
+
boost() {
|
|
427
|
+
this._active_array = this._boost;
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Ensure an active array is set. Throws if include(), exclude() or boost() was not called.
|
|
433
|
+
* @private
|
|
434
|
+
*/
|
|
435
|
+
_requireActiveArray() {
|
|
436
|
+
if (this._active_array === null) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
'Search: call include(), exclude(), or boost() before adding filters'
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* When the active array is _boost, require a non-null boost (group_boost filter type is exempt).
|
|
445
|
+
* @private
|
|
446
|
+
* @param {number | null} boost - Boost value for convenience methods
|
|
447
|
+
*/
|
|
448
|
+
_requireBoostForBoostArray(boost) {
|
|
449
|
+
if (this._active_array === this._boost && boost == null) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
'Search: when adding to boost array, a non-null boost is required'
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Add any Filter instance to the active array (include, exclude, or boost).
|
|
458
|
+
* @param {Filter} filterInstance - A filter instance (TermFilter, NumericFilter, etc.)
|
|
459
|
+
* @returns {this}
|
|
460
|
+
*/
|
|
461
|
+
filter(filterInstance) {
|
|
462
|
+
this._requireActiveArray();
|
|
463
|
+
if (filterInstance == null || !(filterInstance instanceof Filter)) {
|
|
464
|
+
throw new Error('Search.filter: argument must be a Filter instance');
|
|
465
|
+
}
|
|
466
|
+
if (
|
|
467
|
+
this._active_array === this._boost &&
|
|
468
|
+
filterInstance.filter !== 'group_boost' &&
|
|
469
|
+
filterInstance.boost == null
|
|
470
|
+
) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
'Search: when adding to boost array, the filter must have a non-null boost (group_boost is exempt)'
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
this._active_array.push(filterInstance);
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Add a term filter (exact match on a single value).
|
|
481
|
+
* @param {string} field - Field to filter on
|
|
482
|
+
* @param {string} value - Exact value to match
|
|
483
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
484
|
+
* @returns {this}
|
|
485
|
+
*/
|
|
486
|
+
term(field, value, boost = null) {
|
|
487
|
+
this._requireActiveArray();
|
|
488
|
+
this._requireBoostForBoostArray(boost);
|
|
489
|
+
this._active_array.push(new TermFilter(field, value, boost));
|
|
490
|
+
return this;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Add a terms filter (at least one of the values must match).
|
|
495
|
+
* @param {string} field - Field to filter on
|
|
496
|
+
* @param {string[]} values - Values to match
|
|
497
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
498
|
+
* @returns {this}
|
|
499
|
+
*/
|
|
500
|
+
terms(field, values, boost = null) {
|
|
501
|
+
this._requireActiveArray();
|
|
502
|
+
this._requireBoostForBoostArray(boost);
|
|
503
|
+
this._active_array.push(new TermsFilter(field, values, boost));
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Add a numeric filter (range comparison).
|
|
509
|
+
* @param {string} field - Field to filter on
|
|
510
|
+
* @param {">" | ">=" | "<" | "<="} operator - Comparison operator
|
|
511
|
+
* @param {number} value - Numeric threshold
|
|
512
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
513
|
+
* @returns {this}
|
|
514
|
+
*/
|
|
515
|
+
numeric(field, operator, value, boost = null) {
|
|
516
|
+
this._requireActiveArray();
|
|
517
|
+
this._requireBoostForBoostArray(boost);
|
|
518
|
+
this._active_array.push(new NumericFilter(field, operator, value, boost));
|
|
519
|
+
return this;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Add a date range filter.
|
|
524
|
+
* @param {string} field - Date field to filter on
|
|
525
|
+
* @param {string | null} [dateFrom=null] - Start date (ISO 8601)
|
|
526
|
+
* @param {string | null} [dateTo=null] - End date (ISO 8601)
|
|
527
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
528
|
+
* @returns {this}
|
|
529
|
+
*/
|
|
530
|
+
date(field, dateFrom = null, dateTo = null, boost = null) {
|
|
531
|
+
this._requireActiveArray();
|
|
532
|
+
this._requireBoostForBoostArray(boost);
|
|
533
|
+
this._active_array.push(new DateFilter(field, dateFrom, dateTo, boost));
|
|
534
|
+
return this;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Add a geo filter (distance from coordinates).
|
|
539
|
+
* @param {string} field - Geo point field
|
|
540
|
+
* @param {string[]} value - Coordinates, e.g. ["geo:40.7128,-74.0060"]
|
|
541
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
542
|
+
* @returns {this}
|
|
543
|
+
*/
|
|
544
|
+
geo(field, value, boost = null) {
|
|
545
|
+
this._requireActiveArray();
|
|
546
|
+
this._requireBoostForBoostArray(boost);
|
|
547
|
+
this._active_array.push(new GeoFilter(field, value, boost));
|
|
548
|
+
return this;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Add a match filter (full-text keywords).
|
|
553
|
+
* @param {string} field - Text field to match against
|
|
554
|
+
* @param {string[]} value - Keywords to match
|
|
555
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
556
|
+
* @returns {this}
|
|
557
|
+
*/
|
|
558
|
+
match(field, value, boost = null) {
|
|
559
|
+
this._requireActiveArray();
|
|
560
|
+
this._requireBoostForBoostArray(boost);
|
|
561
|
+
this._active_array.push(new MatchFilter(field, value, boost));
|
|
562
|
+
return this;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Add an is_null filter (field must be missing/null).
|
|
567
|
+
* @param {string} field - Field that must be empty
|
|
568
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
569
|
+
* @returns {this}
|
|
570
|
+
*/
|
|
571
|
+
isNull(field, boost = null) {
|
|
572
|
+
this._requireActiveArray();
|
|
573
|
+
this._requireBoostForBoostArray(boost);
|
|
574
|
+
this._active_array.push(new IsNullFilter(field, boost));
|
|
575
|
+
return this;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Add a not_null filter (field must exist and have a value).
|
|
580
|
+
* @param {string} field - Field that must exist
|
|
581
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
582
|
+
* @returns {this}
|
|
583
|
+
*/
|
|
584
|
+
notNull(field, boost = null) {
|
|
585
|
+
this._requireActiveArray();
|
|
586
|
+
this._requireBoostForBoostArray(boost);
|
|
587
|
+
this._active_array.push(new NotNullFilter(field, boost));
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Add a custom filter (raw Elasticsearch clause).
|
|
593
|
+
* @param {string} field - Ignored; use empty string if required by schema
|
|
594
|
+
* @param {Record<string, unknown>} value - Custom Elasticsearch query clause
|
|
595
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
596
|
+
* @returns {this}
|
|
597
|
+
*/
|
|
598
|
+
custom(field, value, boost = null) {
|
|
599
|
+
this._requireActiveArray();
|
|
600
|
+
this._requireBoostForBoostArray(boost);
|
|
601
|
+
this._active_array.push(new CustomFilter(field, value, boost));
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Add a group_boost filter (hierarchical boosting from lookup index).
|
|
607
|
+
* @param {string} lookup_index - Lookup index name
|
|
608
|
+
* @param {string} field - Target field in the lookup index
|
|
609
|
+
* @param {string} value - Lookup document ID
|
|
610
|
+
* @param {string} group - Group path prefix (e.g. "label")
|
|
611
|
+
* @param {number | null} [min_boost=null] - Minimum boost
|
|
612
|
+
* @param {number | null} [max_boost=null] - Maximum boost
|
|
613
|
+
* @param {number | null} [n=null] - Number of groups
|
|
614
|
+
* @returns {this}
|
|
615
|
+
*/
|
|
616
|
+
groupBoost(lookup_index, field, value, group, min_boost = null, max_boost = null, n = null) {
|
|
617
|
+
this._requireActiveArray();
|
|
618
|
+
this._active_array.push(
|
|
619
|
+
new GroupBoostFilter(lookup_index, field, value, group, min_boost, max_boost, n)
|
|
620
|
+
);
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Add a terms_lookup filter (match from another ES document).
|
|
626
|
+
* @param {string} lookup_index - Index to look up terms from
|
|
627
|
+
* @param {string} field - Target field in the current index
|
|
628
|
+
* @param {string} value - Document ID in lookup_index
|
|
629
|
+
* @param {string} path - JSON path in the lookup document
|
|
630
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
631
|
+
* @returns {this}
|
|
632
|
+
*/
|
|
633
|
+
termsLookup(lookup_index, field, value, path, boost = null) {
|
|
634
|
+
this._requireActiveArray();
|
|
635
|
+
this._requireBoostForBoostArray(boost);
|
|
636
|
+
this._active_array.push(
|
|
637
|
+
new TermsLookupFilter(lookup_index, field, value, path, boost)
|
|
638
|
+
);
|
|
639
|
+
return this;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Add a console_account filter (match from console-accounts document).
|
|
644
|
+
* @param {string} field - Target field to match
|
|
645
|
+
* @param {string} value - Console account ID
|
|
646
|
+
* @param {string} path - JSON path in the account document
|
|
647
|
+
* @param {number | null} [boost=null] - Optional boost multiplier
|
|
648
|
+
* @returns {this}
|
|
649
|
+
*/
|
|
650
|
+
consoleAccount(field, value, path, boost = null) {
|
|
651
|
+
this._requireActiveArray();
|
|
652
|
+
this._requireBoostForBoostArray(boost);
|
|
653
|
+
this._active_array.push(new ConsoleAccountFilter(field, value, path, boost));
|
|
654
|
+
return this;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Log a string.
|
|
659
|
+
* @param {string} string
|
|
660
|
+
*/
|
|
661
|
+
log(string) {
|
|
662
|
+
this._log(string);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Show results.
|
|
667
|
+
* @param {*} results
|
|
668
|
+
*/
|
|
669
|
+
show(results) {
|
|
670
|
+
this._show(results);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Filter } from './Filter.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Match field values against data from a console account document (index: console-accounts).
|
|
5
|
+
*/
|
|
6
|
+
export class ConsoleAccountFilter extends Filter {
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} field - Target field to match (e.g. "user_id", "item_id")
|
|
9
|
+
* @param {string} value - Console account ID
|
|
10
|
+
* @param {string} path - JSON path in the account document (e.g. "include_user_ids", "exclude_item_ids")
|
|
11
|
+
* @param {number | null} [boost=null] - Optional boost multiplier (0.1–100)
|
|
12
|
+
*/
|
|
13
|
+
constructor(field, value, path, boost = null) {
|
|
14
|
+
super('console_account', field, boost);
|
|
15
|
+
this.value = value;
|
|
16
|
+
this.path = path;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Filter } from './Filter.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom Elasticsearch query clause. field is ignored by the API but required by the schema.
|
|
5
|
+
*/
|
|
6
|
+
export class CustomFilter extends Filter {
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} field - Ignored for custom filters; use empty string or placeholder if required
|
|
9
|
+
* @param {Record<string, unknown>} value - Custom Elasticsearch query clause
|
|
10
|
+
* @param {number | null} [boost=null] - Optional boost multiplier (0.1–100)
|
|
11
|
+
*/
|
|
12
|
+
constructor(field, value, boost = null) {
|
|
13
|
+
super('custom', field, boost);
|
|
14
|
+
this.value = value;
|
|
15
|
+
}
|
|
16
|
+
}
|