@havesomecode/kibana-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/config/sources.example.json +97 -0
- package/dist/src/cli.js +160 -0
- package/dist/src/config.js +204 -0
- package/dist/src/index.js +10 -0
- package/dist/src/kibana_client.js +441 -0
- package/dist/src/mcp_entry.js +6 -0
- package/dist/src/mcp_runtime.js +23 -0
- package/dist/src/profile_paths.js +36 -0
- package/dist/src/profile_store.js +136 -0
- package/dist/src/query/compiler.js +430 -0
- package/dist/src/query/cursor.js +7 -0
- package/dist/src/query/normalize.js +383 -0
- package/dist/src/schema_catalog.js +91 -0
- package/dist/src/secret_store.js +261 -0
- package/dist/src/server.js +87 -0
- package/dist/src/setup_flow.js +160 -0
- package/dist/src/source_catalog.js +47 -0
- package/dist/src/tools/configure.js +53 -0
- package/dist/src/tools/describe_fields.js +55 -0
- package/dist/src/tools/discover.js +47 -0
- package/dist/src/tools/filter.js +141 -0
- package/dist/src/tools/query.js +287 -0
- package/dist/src/types.js +1 -0
- package/docs/project/compatibility-matrix.md +22 -0
- package/docs/project/distribution-strategy.md +78 -0
- package/docs/project/npm-publishing.md +57 -0
- package/docs/project/release-checklist.md +51 -0
- package/docs/project/support-policy.md +52 -0
- package/package.json +65 -0
- package/plugins/kibana-log-investigation/.codex-plugin/plugin.json +31 -0
- package/plugins/kibana-log-investigation/.mcp.json +10 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { decodeQueryCursor } from "./cursor.js";
|
|
2
|
+
function findFieldDescriptor(fields, requestedField) {
|
|
3
|
+
const normalized = requestedField.toLowerCase();
|
|
4
|
+
return fields.find((field) => field.name.toLowerCase() === normalized);
|
|
5
|
+
}
|
|
6
|
+
function resolveRequestedFieldAlias(source, requestedField, options) {
|
|
7
|
+
if (options.resolveFieldAliases === false) {
|
|
8
|
+
return requestedField;
|
|
9
|
+
}
|
|
10
|
+
const normalized = requestedField.toLowerCase();
|
|
11
|
+
const exactMatch = source.fieldHints.find((fieldHint) => fieldHint.name.toLowerCase() === normalized ||
|
|
12
|
+
(fieldHint.aliases ?? []).some((alias) => alias.toLowerCase() === normalized));
|
|
13
|
+
return exactMatch?.name ?? requestedField;
|
|
14
|
+
}
|
|
15
|
+
function resolveFieldName(source, requestedField, purpose, options) {
|
|
16
|
+
const aliasResolvedField = resolveRequestedFieldAlias(source, requestedField, options);
|
|
17
|
+
const sourceSchema = options.sourceSchemas?.get(source.id);
|
|
18
|
+
if (options.resolvePreferredExactFields === false) {
|
|
19
|
+
return { resolvedField: aliasResolvedField };
|
|
20
|
+
}
|
|
21
|
+
if (!sourceSchema) {
|
|
22
|
+
const schemaError = options.sourceSchemaErrors?.get(source.id);
|
|
23
|
+
if (schemaError && purpose === "filter") {
|
|
24
|
+
return {
|
|
25
|
+
resolvedField: aliasResolvedField,
|
|
26
|
+
advisory: {
|
|
27
|
+
kind: "schema_unavailable",
|
|
28
|
+
source_id: source.id,
|
|
29
|
+
purpose,
|
|
30
|
+
requested_field: requestedField,
|
|
31
|
+
resolved_field: aliasResolvedField,
|
|
32
|
+
reason: `Schema-backed exact-field resolution is unavailable for source '${source.id}': ${schemaError}`,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { resolvedField: aliasResolvedField };
|
|
37
|
+
}
|
|
38
|
+
const requestedDescriptor = findFieldDescriptor(sourceSchema, aliasResolvedField);
|
|
39
|
+
const fallbackKeyword = findFieldDescriptor(sourceSchema, `${aliasResolvedField}.keyword`);
|
|
40
|
+
const preferredExactField = requestedDescriptor?.preferred_exact_field ??
|
|
41
|
+
(fallbackKeyword?.aggregatable ? fallbackKeyword.name : undefined);
|
|
42
|
+
if (preferredExactField && preferredExactField !== aliasResolvedField) {
|
|
43
|
+
return {
|
|
44
|
+
resolvedField: preferredExactField,
|
|
45
|
+
advisory: {
|
|
46
|
+
kind: "preferred_exact_field",
|
|
47
|
+
source_id: source.id,
|
|
48
|
+
purpose,
|
|
49
|
+
requested_field: requestedField,
|
|
50
|
+
resolved_field: preferredExactField,
|
|
51
|
+
reason: `Resolved ${aliasResolvedField} to its preferred exact field for ${purpose}`,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { resolvedField: aliasResolvedField };
|
|
56
|
+
}
|
|
57
|
+
function compileFilters(source, filters, options) {
|
|
58
|
+
const resolvedFilters = [];
|
|
59
|
+
const advisories = [];
|
|
60
|
+
for (const filter of filters) {
|
|
61
|
+
const { resolvedField, advisory } = resolveFieldName(source, filter.field, "filter", options);
|
|
62
|
+
resolvedFilters.push({
|
|
63
|
+
...filter,
|
|
64
|
+
resolved_field: resolvedField,
|
|
65
|
+
});
|
|
66
|
+
if (advisory) {
|
|
67
|
+
advisories.push(advisory);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { resolvedFilters, advisories };
|
|
71
|
+
}
|
|
72
|
+
function resolveNestedFieldName(source, nestedFilter, options) {
|
|
73
|
+
const requestedField = nestedFilter.field.startsWith(`${nestedFilter.path}.`)
|
|
74
|
+
? nestedFilter.field
|
|
75
|
+
: `${nestedFilter.path}.${nestedFilter.field}`;
|
|
76
|
+
return resolveFieldName(source, requestedField, "filter", options);
|
|
77
|
+
}
|
|
78
|
+
function getNestedChildFields(sourceSchema, nestedPath) {
|
|
79
|
+
return sourceSchema
|
|
80
|
+
.map((field) => field.name)
|
|
81
|
+
.filter((fieldName) => fieldName.startsWith(`${nestedPath}.`))
|
|
82
|
+
.sort((left, right) => left.localeCompare(right));
|
|
83
|
+
}
|
|
84
|
+
function formatFieldList(fields, limit = 3) {
|
|
85
|
+
if (fields.length <= limit) {
|
|
86
|
+
return fields.join(", ");
|
|
87
|
+
}
|
|
88
|
+
return `${fields.slice(0, limit).join(", ")} (+${fields.length - limit} more)`;
|
|
89
|
+
}
|
|
90
|
+
function isFieldWithinPath(fieldName, path) {
|
|
91
|
+
return fieldName === path || fieldName.startsWith(`${path}.`);
|
|
92
|
+
}
|
|
93
|
+
function describeNonNestedPathKind(sourceSchema, path) {
|
|
94
|
+
const fieldsWithinPath = sourceSchema.filter((field) => isFieldWithinPath(field.name, path));
|
|
95
|
+
if (fieldsWithinPath.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (fieldsWithinPath.some((field) => field.object_array_path === path)) {
|
|
99
|
+
return "object_array";
|
|
100
|
+
}
|
|
101
|
+
return "object_like";
|
|
102
|
+
}
|
|
103
|
+
function supportsFlatObjectPathTermFallback(descriptor, resolvedField) {
|
|
104
|
+
if (!descriptor) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (descriptor.type === "text" && !resolvedField.endsWith(".keyword")) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function buildNonNestedPathError(source, path, pathKind, reason) {
|
|
113
|
+
const pathDescription = pathKind === "object_array"
|
|
114
|
+
? "an inferred array of objects"
|
|
115
|
+
: "a non-nested object path with child fields";
|
|
116
|
+
return new Error(`Path '${path}' for source '${source.id}' is ${pathDescription}, not a true nested mapping. ${reason}`);
|
|
117
|
+
}
|
|
118
|
+
function compileNestedFilters(source, nestedFilters, options, request) {
|
|
119
|
+
const resolvedNestedFilters = [];
|
|
120
|
+
const advisories = [];
|
|
121
|
+
const sourceSchema = options.sourceSchemas?.get(source.id);
|
|
122
|
+
if (nestedFilters.length === 0) {
|
|
123
|
+
return { resolvedNestedFilters, advisories };
|
|
124
|
+
}
|
|
125
|
+
if (!sourceSchema) {
|
|
126
|
+
const schemaError = options.sourceSchemaErrors?.get(source.id) ??
|
|
127
|
+
"schema metadata is unavailable for this source";
|
|
128
|
+
throw new Error(`Nested filters require schema metadata for source '${source.id}': ${schemaError}`);
|
|
129
|
+
}
|
|
130
|
+
const filtersByPath = new Map();
|
|
131
|
+
for (const nestedFilter of nestedFilters) {
|
|
132
|
+
const filters = filtersByPath.get(nestedFilter.path) ?? [];
|
|
133
|
+
filters.push(nestedFilter);
|
|
134
|
+
filtersByPath.set(nestedFilter.path, filters);
|
|
135
|
+
}
|
|
136
|
+
for (const [path, pathFilters] of filtersByPath.entries()) {
|
|
137
|
+
const nestedPathExists = sourceSchema.some((field) => field.nested_path === path);
|
|
138
|
+
const nonNestedPathKind = nestedPathExists
|
|
139
|
+
? null
|
|
140
|
+
: describeNonNestedPathKind(sourceSchema, path);
|
|
141
|
+
if (!nestedPathExists && !nonNestedPathKind) {
|
|
142
|
+
throw new Error(`Nested path '${path}' is not known as nested for source '${source.id}'`);
|
|
143
|
+
}
|
|
144
|
+
if (!nestedPathExists && request.extract_nested) {
|
|
145
|
+
throw buildNonNestedPathError(source, path, nonNestedPathKind ?? "object_like", "inner_hits are only available for fields mapped as nested, so extract_nested cannot be used here.");
|
|
146
|
+
}
|
|
147
|
+
if (!nestedPathExists && pathFilters.length > 1) {
|
|
148
|
+
throw buildNonNestedPathError(source, path, nonNestedPathKind ?? "object_like", "Multiple nested_filters on the same path are unsafe because Elasticsearch flattens non-nested object values and may match conditions across different objects. Remap the path as nested if you need same-object matching.");
|
|
149
|
+
}
|
|
150
|
+
for (const nestedFilter of pathFilters) {
|
|
151
|
+
const requestedField = nestedFilter.field.startsWith(`${path}.`)
|
|
152
|
+
? nestedFilter.field
|
|
153
|
+
: `${path}.${nestedFilter.field}`;
|
|
154
|
+
const nestedFieldExists = sourceSchema.some((field) => isFieldWithinPath(field.name, requestedField));
|
|
155
|
+
if (!nestedFieldExists) {
|
|
156
|
+
throw new Error(`Nested field '${nestedFilter.field}' was not found under path '${path}' for source '${source.id}'`);
|
|
157
|
+
}
|
|
158
|
+
const { resolvedField, advisory } = resolveNestedFieldName(source, nestedFilter, options);
|
|
159
|
+
if (!nestedPathExists) {
|
|
160
|
+
const resolvedDescriptor = findFieldDescriptor(sourceSchema, resolvedField);
|
|
161
|
+
if (!supportsFlatObjectPathTermFallback(resolvedDescriptor, resolvedField)) {
|
|
162
|
+
throw buildNonNestedPathError(source, path, nonNestedPathKind ?? "object_like", `Field '${resolvedField}' does not resolve to an exact-match field, so a plain term-query fallback would not be predictable. Use a keyword/exact field or remap the path as nested.`);
|
|
163
|
+
}
|
|
164
|
+
advisories.push({
|
|
165
|
+
kind: "non_nested_object_array",
|
|
166
|
+
source_id: source.id,
|
|
167
|
+
purpose: "filter",
|
|
168
|
+
requested_field: requestedField,
|
|
169
|
+
resolved_field: resolvedField,
|
|
170
|
+
reason: `Nested path '${path}' is represented by child fields (${formatFieldList(getNestedChildFields(sourceSchema, path))}) but is not mapped as nested for source '${source.id}'. ` +
|
|
171
|
+
`Applied a flat term filter on '${resolvedField}' instead of a nested query. ` +
|
|
172
|
+
`This can match any object in the array and cannot return inner_hits${request.extract_nested
|
|
173
|
+
? "; requested extract_nested could not be honored without nested mapping"
|
|
174
|
+
: ""}.`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
resolvedNestedFilters.push({
|
|
178
|
+
...nestedFilter,
|
|
179
|
+
resolved_field: resolvedField,
|
|
180
|
+
query_strategy: nestedPathExists ? "nested" : "flat_object_path",
|
|
181
|
+
});
|
|
182
|
+
if (advisory) {
|
|
183
|
+
advisories.push(advisory);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { resolvedNestedFilters, advisories };
|
|
188
|
+
}
|
|
189
|
+
function buildElasticBody(source, request, resolvedFilters, resolvedNestedFilters, resolvedSortBy, options) {
|
|
190
|
+
const must = [
|
|
191
|
+
{
|
|
192
|
+
range: {
|
|
193
|
+
[source.timeField]: {
|
|
194
|
+
gte: request.start_time,
|
|
195
|
+
lte: request.end_time,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
if (request.text) {
|
|
201
|
+
must.push({
|
|
202
|
+
simple_query_string: {
|
|
203
|
+
query: request.text,
|
|
204
|
+
...(source.defaultTextFields.length > 0 ? { fields: source.defaultTextFields } : {}),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
for (const filter of resolvedFilters) {
|
|
209
|
+
must.push({
|
|
210
|
+
term: {
|
|
211
|
+
[filter.resolved_field]: filter.value,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (resolvedNestedFilters.length > 0) {
|
|
216
|
+
const filtersByPath = new Map();
|
|
217
|
+
for (const nestedFilter of resolvedNestedFilters) {
|
|
218
|
+
if (nestedFilter.query_strategy === "flat_object_path") {
|
|
219
|
+
must.push({
|
|
220
|
+
term: {
|
|
221
|
+
[nestedFilter.resolved_field]: nestedFilter.value,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const filters = filtersByPath.get(nestedFilter.path) ?? [];
|
|
227
|
+
filters.push(nestedFilter);
|
|
228
|
+
filtersByPath.set(nestedFilter.path, filters);
|
|
229
|
+
}
|
|
230
|
+
for (const [path, nestedFilters] of filtersByPath.entries()) {
|
|
231
|
+
must.push({
|
|
232
|
+
nested: {
|
|
233
|
+
path,
|
|
234
|
+
query: {
|
|
235
|
+
bool: {
|
|
236
|
+
must: nestedFilters.map((nestedFilter) => ({
|
|
237
|
+
term: {
|
|
238
|
+
[nestedFilter.resolved_field]: nestedFilter.value,
|
|
239
|
+
},
|
|
240
|
+
})),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
...(request.extract_nested
|
|
244
|
+
? {
|
|
245
|
+
inner_hits: {
|
|
246
|
+
name: path,
|
|
247
|
+
size: request.limit,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
: {}),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const baseBody = {
|
|
256
|
+
query: {
|
|
257
|
+
bool: { must },
|
|
258
|
+
},
|
|
259
|
+
track_total_hits: true,
|
|
260
|
+
};
|
|
261
|
+
if (request.mode === "hits") {
|
|
262
|
+
const cursor = request.cursor ? decodeQueryCursor(request.cursor) : undefined;
|
|
263
|
+
return {
|
|
264
|
+
...baseBody,
|
|
265
|
+
size: request.limit,
|
|
266
|
+
sort: [{ [resolvedSortBy]: { order: request.sort } }],
|
|
267
|
+
...(cursor ? { search_after: cursor.values } : {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (request.mode === "count") {
|
|
271
|
+
return {
|
|
272
|
+
...baseBody,
|
|
273
|
+
size: 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (request.mode === "histogram") {
|
|
277
|
+
return {
|
|
278
|
+
...baseBody,
|
|
279
|
+
size: 0,
|
|
280
|
+
aggs: {
|
|
281
|
+
histogram: {
|
|
282
|
+
date_histogram: {
|
|
283
|
+
field: source.timeField,
|
|
284
|
+
fixed_interval: request.histogram_interval ?? "1m",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (request.mode === "stats") {
|
|
291
|
+
if (!request.stats_field) {
|
|
292
|
+
throw new Error("stats_field is required when mode is 'stats'");
|
|
293
|
+
}
|
|
294
|
+
const statsField = resolveRequestedFieldAlias(source, request.stats_field, options);
|
|
295
|
+
if (request.group_by) {
|
|
296
|
+
return {
|
|
297
|
+
...baseBody,
|
|
298
|
+
size: 0,
|
|
299
|
+
aggs: {
|
|
300
|
+
groups: {
|
|
301
|
+
terms: {
|
|
302
|
+
field: resolveFieldName(source, request.group_by, "group_by", options).resolvedField,
|
|
303
|
+
size: request.limit,
|
|
304
|
+
},
|
|
305
|
+
aggs: {
|
|
306
|
+
stats_summary: {
|
|
307
|
+
stats: {
|
|
308
|
+
field: statsField,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
stats_percentiles: {
|
|
312
|
+
percentiles: {
|
|
313
|
+
field: statsField,
|
|
314
|
+
percents: [50, 95, 99],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
...baseBody,
|
|
324
|
+
size: 0,
|
|
325
|
+
aggs: {
|
|
326
|
+
stats_summary: {
|
|
327
|
+
stats: {
|
|
328
|
+
field: statsField,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
stats_percentiles: {
|
|
332
|
+
percentiles: {
|
|
333
|
+
field: statsField,
|
|
334
|
+
percents: [50, 95, 99],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (request.mode === "grouped_top_hits") {
|
|
341
|
+
if (!request.group_by) {
|
|
342
|
+
throw new Error("group_by is required when mode is 'grouped_top_hits'");
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...baseBody,
|
|
346
|
+
size: 0,
|
|
347
|
+
aggs: {
|
|
348
|
+
groups: {
|
|
349
|
+
terms: {
|
|
350
|
+
field: resolveFieldName(source, request.group_by, "group_by", options).resolvedField,
|
|
351
|
+
size: request.limit,
|
|
352
|
+
},
|
|
353
|
+
aggs: {
|
|
354
|
+
top_hits: {
|
|
355
|
+
top_hits: {
|
|
356
|
+
size: request.top_hits_size ?? 1,
|
|
357
|
+
sort: [{ [resolvedSortBy]: { order: request.sort } }],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!request.group_by) {
|
|
366
|
+
throw new Error("group_by is required when mode is 'terms'");
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
...baseBody,
|
|
370
|
+
size: 0,
|
|
371
|
+
aggs: {
|
|
372
|
+
groups: {
|
|
373
|
+
terms: {
|
|
374
|
+
field: resolveFieldName(source, request.group_by, "group_by", options).resolvedField,
|
|
375
|
+
size: request.limit,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function compileSourceQuery(source, request, options) {
|
|
382
|
+
const { resolvedFilters, advisories } = compileFilters(source, request.filters ?? [], options);
|
|
383
|
+
const nestedCompilation = compileNestedFilters(source, request.nested_filters ?? [], options, {
|
|
384
|
+
extract_nested: request.extract_nested,
|
|
385
|
+
});
|
|
386
|
+
advisories.push(...nestedCompilation.advisories);
|
|
387
|
+
const sortResolution = resolveFieldName(source, request.sort_by ?? source.timeField, "sort", options);
|
|
388
|
+
const resolvedSortBy = sortResolution.resolvedField;
|
|
389
|
+
if (sortResolution.advisory && request.sort_by) {
|
|
390
|
+
advisories.push(sortResolution.advisory);
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
source,
|
|
394
|
+
resolvedFilters,
|
|
395
|
+
resolvedNestedFilters: nestedCompilation.resolvedNestedFilters,
|
|
396
|
+
resolvedSortBy,
|
|
397
|
+
advisories,
|
|
398
|
+
request: {
|
|
399
|
+
body: buildElasticBody(source, request, resolvedFilters, nestedCompilation.resolvedNestedFilters, resolvedSortBy, options),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
export function compileQueryPlan(request, sources, options = {}) {
|
|
404
|
+
const mode = request.mode ?? "hits";
|
|
405
|
+
const sort = request.sort ?? "desc";
|
|
406
|
+
const limit = request.limit ?? (mode === "count" ? 1 : 100);
|
|
407
|
+
const sourceQueries = sources.map((source) => compileSourceQuery(source, {
|
|
408
|
+
...request,
|
|
409
|
+
mode,
|
|
410
|
+
sort,
|
|
411
|
+
limit,
|
|
412
|
+
}, options));
|
|
413
|
+
return {
|
|
414
|
+
mode,
|
|
415
|
+
startTime: request.start_time,
|
|
416
|
+
endTime: request.end_time,
|
|
417
|
+
sort,
|
|
418
|
+
sortBy: request.sort_by,
|
|
419
|
+
cursor: request.cursor,
|
|
420
|
+
limit,
|
|
421
|
+
text: request.text,
|
|
422
|
+
nestedFilters: sourceQueries.flatMap((sourceQuery) => sourceQuery.resolvedNestedFilters),
|
|
423
|
+
statsField: request.stats_field,
|
|
424
|
+
topHitsSize: request.top_hits_size,
|
|
425
|
+
histogramInterval: request.histogram_interval,
|
|
426
|
+
groupBy: request.group_by,
|
|
427
|
+
sourceIds: sources.map((source) => source.id),
|
|
428
|
+
sourceQueries,
|
|
429
|
+
};
|
|
430
|
+
}
|