diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md
new file mode 100644
index 00000000000000..66d540c48c3bc8
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md)
+
+## AggConfigs.hierarchical property
+
+Signature:
+
+```typescript
+hierarchical?: boolean;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
index 22f8994747aa29..02e9a63d95ba37 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
@@ -22,6 +22,7 @@ export declare class AggConfigs
| --- | --- | --- | --- |
| [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) | | IAggConfig[]
| |
| [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) | | <T extends AggConfig = AggConfig>(params: CreateAggConfigParams, { addToAggConfigs }?: {
addToAggConfigs?: boolean | undefined;
}) => T
| |
+| [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md) | | boolean
| |
| [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) | | IndexPattern
| |
| [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) | | string[]
| |
| [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) | | TimeRange
| |
@@ -46,5 +47,5 @@ export declare class AggConfigs
| [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) | | |
| [setTimeFields(timeFields)](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) | | |
| [setTimeRange(timeRange)](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) | | |
-| [toDsl(hierarchical)](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | |
+| [toDsl()](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md
index 055c4113ca3e46..1327e976db0ce3 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md
@@ -7,15 +7,8 @@
Signature:
```typescript
-toDsl(hierarchical?: boolean): Record;
+toDsl(): Record;
```
-
-## Parameters
-
-| Parameter | Type | Description |
-| --- | --- | --- |
-| hierarchical | boolean
| |
-
Returns:
`Record`
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.inspector.md
similarity index 52%
rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md
rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.inspector.md
index b4431b9467b71e..9961292aaf2177 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.inspector.md
@@ -1,11 +1,13 @@
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md)
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md)
-## ISearchOptions.requestResponder property
+## ISearchOptions.inspector property
+
+Inspector integration options
Signature:
```typescript
-requestResponder?: RequestResponder;
+inspector?: IInspectorInfo;
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md
index cc0cb538be6113..21fb7e3dfc7e87 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md
@@ -16,10 +16,10 @@ export interface ISearchOptions
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal
| An AbortSignal
that allows the caller of search
to abort a search request. |
| [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) | IndexPattern
| Index pattern reference is used for better error messages |
+| [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md) | IInspectorInfo
| Inspector integration options |
| [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | boolean
| Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) |
| [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | boolean
| Whether the session is already saved (i.e. sent to background) |
| [legacyHitsTotal](./kibana-plugin-plugins-data-public.isearchoptions.legacyhitstotal.md) | boolean
| Request the legacy format for the total number of hits. If sending rest_total_hits_as_int
to something other than true
, this should be set to false
. |
-| [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md) | RequestResponder
| |
| [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string
| A session ID, grouping multiple search requests into a single session. |
| [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string
| Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md
index 623d6366d4d131..e6ba1a51a867d2 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md
@@ -14,7 +14,7 @@ Fetch this source and reject the returned Promise on error
Signature:
```typescript
-fetch(options?: ISearchOptions): Promise>;
+fetch(options?: ISearchOptions): Promise>;
```
## Parameters
@@ -25,5 +25,5 @@ fetch(options?: ISearchOptions): PromiseReturns:
-`Promise>`
+`Promise>`
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md
index d5641107a88aa1..4369cf7c087da7 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md
@@ -9,7 +9,7 @@ Fetch this source from Elasticsearch, returning an observable over the response(
Signature:
```typescript
-fetch$(options?: ISearchOptions): import("rxjs").Observable>;
+fetch$(options?: ISearchOptions): Observable>;
```
## Parameters
@@ -20,5 +20,5 @@ fetch$(options?: ISearchOptions): import("rxjs").ObservableReturns:
-`import("rxjs").Observable>`
+`Observable>`
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md
index f6bab8e424857d..12011f82429969 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md
@@ -9,5 +9,5 @@
Signature:
```typescript
-aggs?: any;
+aggs?: object | IAggConfigs | (() => object);
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md
index d0f53936eb56aa..981d956a9e89be 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md
@@ -16,7 +16,7 @@ export interface SearchSourceFields
| Property | Type | Description |
| --- | --- | --- |
-| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | any
| [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) |
+| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | object | IAggConfigs | (() => object)
| [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) |
| [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | SearchFieldValue[]
| Retrieve fields via the search Fields API |
| [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md) | NameList
| Retreive fields directly from \_source (legacy behavior) |
| [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | Filter[] | Filter | (() => Filter[] | Filter | undefined)
| [Filter](./kibana-plugin-plugins-data-public.filter.md) |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.inspector.md
similarity index 52%
rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md
rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.inspector.md
index 7440f5a9d26cfc..ab755334643aae 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.inspector.md
@@ -1,11 +1,13 @@
-[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md)
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md)
-## ISearchOptions.requestResponder property
+## ISearchOptions.inspector property
+
+Inspector integration options
Signature:
```typescript
-requestResponder?: RequestResponder;
+inspector?: IInspectorInfo;
```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md
index 413a59be3d4278..cdb5664f96cddb 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md
@@ -16,10 +16,10 @@ export interface ISearchOptions
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal
| An AbortSignal
that allows the caller of search
to abort a search request. |
| [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) | IndexPattern
| Index pattern reference is used for better error messages |
+| [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md) | IInspectorInfo
| Inspector integration options |
| [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | boolean
| Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) |
| [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | boolean
| Whether the session is already saved (i.e. sent to background) |
| [legacyHitsTotal](./kibana-plugin-plugins-data-server.isearchoptions.legacyhitstotal.md) | boolean
| Request the legacy format for the total number of hits. If sending rest_total_hits_as_int
to something other than true
, this should be set to false
. |
-| [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md) | RequestResponder
| |
| [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string
| A session ID, grouping multiple search requests into a single session. |
| [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string
| Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. |
diff --git a/examples/search_examples/public/index.scss b/examples/search_examples/public/index.scss
index e69de29bb2d1d6..b623fecf78640f 100644
--- a/examples/search_examples/public/index.scss
+++ b/examples/search_examples/public/index.scss
@@ -0,0 +1,6 @@
+@import '@elastic/eui/src/global_styling/variables/header';
+
+.searchExampleStepDsc {
+ padding-left: $euiSizeXL;
+ font-style: italic;
+}
diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx
index 8f31d242faf5ea..b2a4991d0717b6 100644
--- a/examples/search_examples/public/search/app.tsx
+++ b/examples/search_examples/public/search/app.tsx
@@ -20,13 +20,13 @@ import {
EuiTitle,
EuiText,
EuiFlexGrid,
- EuiFlexGroup,
EuiFlexItem,
EuiCheckbox,
EuiSpacer,
EuiCode,
EuiComboBox,
EuiFormLabel,
+ EuiTabbedContent,
} from '@elastic/eui';
import { CoreStart } from '../../../../src/core/public';
@@ -60,6 +60,11 @@ function getNumeric(fields?: IndexPatternField[]) {
return fields?.filter((f) => f.type === 'number' && f.aggregatable);
}
+function getAggregatableStrings(fields?: IndexPatternField[]) {
+ if (!fields) return [];
+ return fields?.filter((f) => f.type === 'string' && f.aggregatable);
+}
+
function formatFieldToComboBox(field?: IndexPatternField | null) {
if (!field) return [];
return formatFieldsToComboBox([field]);
@@ -90,6 +95,9 @@ export const SearchExamplesApp = ({
const [selectedNumericField, setSelectedNumericField] = useState<
IndexPatternField | null | undefined
>();
+ const [selectedBucketField, setSelectedBucketField] = useState<
+ IndexPatternField | null | undefined
+ >();
const [request, setRequest] = useState>({});
const [response, setResponse] = useState>({});
@@ -108,6 +116,7 @@ export const SearchExamplesApp = ({
setFields(indexPattern?.fields);
}, [indexPattern]);
useEffect(() => {
+ setSelectedBucketField(fields?.length ? getAggregatableStrings(fields)[0] : null);
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
}, [fields]);
@@ -203,28 +212,40 @@ export const SearchExamplesApp = ({
.setField('index', indexPattern)
.setField('filter', filters)
.setField('query', query)
- .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*'])
+ .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : [''])
+ .setField('size', selectedFields.length ? 100 : 0)
.setField('trackTotalHits', 100);
- if (selectedNumericField) {
- searchSource.setField('aggs', () => {
- return data.search.aggs
- .createAggConfigs(indexPattern, [
- { type: 'avg', params: { field: selectedNumericField.name } },
- ])
- .toDsl();
+ const aggDef = [];
+ if (selectedBucketField) {
+ aggDef.push({
+ type: 'terms',
+ schema: 'split',
+ params: { field: selectedBucketField.name, size: 2, otherBucket: true },
});
}
+ if (selectedNumericField) {
+ aggDef.push({ type: 'avg', params: { field: selectedNumericField.name } });
+ }
+ if (aggDef.length > 0) {
+ const ac = data.search.aggs.createAggConfigs(indexPattern, aggDef);
+ searchSource.setField('aggs', ac);
+ }
setRequest(searchSource.getSearchRequestBody());
const res = await searchSource.fetch$().toPromise();
setResponse(res);
const message = Searched {res.hits.total} documents.;
- notifications.toasts.addSuccess({
- title: 'Query result',
- text: mountReactNode(message),
- });
+ notifications.toasts.addSuccess(
+ {
+ title: 'Query result',
+ text: mountReactNode(message),
+ },
+ {
+ toastLifeTimeMs: 300000,
+ }
+ );
} catch (e) {
setResponse(e.body);
notifications.toasts.addWarning(`An error has occurred: ${e.message}`);
@@ -263,6 +284,55 @@ export const SearchExamplesApp = ({
doSearchSourceSearch();
};
+ const reqTabs = [
+ {
+ id: 'request',
+ name: Request,
+ content: (
+ <>
+
+ Search body sent to ES
+
+ {JSON.stringify(request, null, 2)}
+
+ >
+ ),
+ },
+ {
+ id: 'response',
+ name: Response,
+ content: (
+ <>
+
+
+
+
+
+ {JSON.stringify(response, null, 2)}
+
+ >
+ ),
+ },
+ ];
+
return (
@@ -284,59 +354,75 @@ export const SearchExamplesApp = ({
useDefaultBehaviors={true}
indexPatterns={indexPattern ? [indexPattern] : undefined}
/>
-
+
+
+ Index Pattern
+ {
+ const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
+ setIndexPattern(newIndexPattern);
+ }}
+ isClearable={false}
+ />
+
+
+ Field (bucket)
+ {
+ if (option.length) {
+ const fld = indexPattern?.getFieldByName(option[0].label);
+ setSelectedBucketField(fld || null);
+ } else {
+ setSelectedBucketField(null);
+ }
+ }}
+ sortMatchesBy="startsWith"
+ data-test-subj="searchBucketField"
+ />
+
+
+ Numeric Field (metric)
+ {
+ if (option.length) {
+ const fld = indexPattern?.getFieldByName(option[0].label);
+ setSelectedNumericField(fld || null);
+ } else {
+ setSelectedNumericField(null);
+ }
+ }}
+ sortMatchesBy="startsWith"
+ data-test-subj="searchMetricField"
+ />
+
+
+ Fields to queryString
+ {
+ const flds = option
+ .map((opt) => indexPattern?.getFieldByName(opt?.label))
+ .filter((f) => f);
+ setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []);
+ }}
+ sortMatchesBy="startsWith"
+ />
+
+
+
-
-
-
- Index Pattern
- {
- const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
- setIndexPattern(newIndexPattern);
- }}
- isClearable={false}
- />
-
-
- Numeric Field to Aggregate
- {
- const fld = indexPattern?.getFieldByName(option[0].label);
- setSelectedNumericField(fld || null);
- }}
- sortMatchesBy="startsWith"
- />
-
-
-
-
- Fields to query (leave blank to include all fields)
- {
- const flds = option
- .map((opt) => indexPattern?.getFieldByName(opt?.label))
- .filter((f) => f);
- setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []);
- }}
- sortMatchesBy="startsWith"
- />
-
-
-
@@ -352,15 +438,32 @@ export const SearchExamplesApp = ({
-
+
+
+
+
+
+
+
@@ -446,41 +549,8 @@ export const SearchExamplesApp = ({
-
-
- Request
-
- Search body sent to ES
-
- {JSON.stringify(request, null, 2)}
-
-
-
-
- Response
-
-
-
-
-
- {JSON.stringify(response, null, 2)}
-
+
+
diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts
index 3ce528e6ed8932..28102544ae0553 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.test.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts
@@ -342,8 +342,8 @@ describe('AggConfigs', () => {
{ enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } },
];
- const ac = new AggConfigs(indexPattern, configStates, { typesRegistry });
- const topLevelDsl = ac.toDsl(true);
+ const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true });
+ const topLevelDsl = ac.toDsl();
const buckets = ac.bySchemaName('buckets');
const metrics = ac.bySchemaName('metrics');
@@ -412,8 +412,8 @@ describe('AggConfigs', () => {
},
];
- const ac = new AggConfigs(indexPattern, configStates, { typesRegistry });
- const topLevelDsl = ac.toDsl(true)['2'];
+ const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true });
+ const topLevelDsl = ac.toDsl()['2'];
expect(Object.keys(topLevelDsl.aggs)).toContain('1');
expect(Object.keys(topLevelDsl.aggs)).toContain('1-bucket');
diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts
index 4d5d49754387d6..2932ef7325aed8 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.ts
@@ -43,6 +43,7 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) {
export interface AggConfigsOptions {
typesRegistry: AggTypesRegistryStart;
+ hierarchical?: boolean;
}
export type CreateAggConfigParams = Assign;
@@ -65,6 +66,8 @@ export class AggConfigs {
public indexPattern: IndexPattern;
public timeRange?: TimeRange;
public timeFields?: string[];
+ public hierarchical?: boolean = false;
+
private readonly typesRegistry: AggTypesRegistryStart;
aggs: IAggConfig[];
@@ -80,6 +83,7 @@ export class AggConfigs {
this.aggs = [];
this.indexPattern = indexPattern;
+ this.hierarchical = opts.hierarchical;
configStates.forEach((params: any) => this.createAggConfig(params));
}
@@ -174,12 +178,12 @@ export class AggConfigs {
return true;
}
- toDsl(hierarchical: boolean = false): Record {
+ toDsl(): Record {
const dslTopLvl = {};
let dslLvlCursor: Record;
let nestedMetrics: Array<{ config: AggConfig; dsl: Record }> | [];
- if (hierarchical) {
+ if (this.hierarchical) {
// collect all metrics, and filter out the ones that we won't be copying
nestedMetrics = this.aggs
.filter(function (agg) {
diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts
index 33fdc45a605b71..f0f3912bf64fea 100644
--- a/src/plugins/data/common/search/aggs/agg_type.ts
+++ b/src/plugins/data/common/search/aggs/agg_type.ts
@@ -13,12 +13,23 @@ import { ISearchSource } from 'src/plugins/data/public';
import { DatatableColumnType, SerializedFieldFormat } from 'src/plugins/expressions/common';
import type { RequestAdapter } from 'src/plugins/inspector/common';
+import { estypes } from '@elastic/elasticsearch';
import { initParams } from './agg_params';
import { AggConfig } from './agg_config';
import { IAggConfigs } from './agg_configs';
import { BaseParamType } from './param_types/base';
import { AggParamType } from './param_types/agg';
+type PostFlightRequestFn = (
+ resp: estypes.SearchResponse,
+ aggConfigs: IAggConfigs,
+ aggConfig: TAggConfig,
+ searchSource: ISearchSource,
+ inspectorRequestAdapter?: RequestAdapter,
+ abortSignal?: AbortSignal,
+ searchSessionId?: string
+) => Promise>;
+
export interface AggTypeConfig<
TAggConfig extends AggConfig = AggConfig,
TParam extends AggParamType = AggParamType
@@ -40,15 +51,7 @@ export interface AggTypeConfig<
customLabels?: boolean;
json?: boolean;
decorateAggConfig?: () => any;
- postFlightRequest?: (
- resp: any,
- aggConfigs: IAggConfigs,
- aggConfig: TAggConfig,
- searchSource: ISearchSource,
- inspectorRequestAdapter?: RequestAdapter,
- abortSignal?: AbortSignal,
- searchSessionId?: string
- ) => Promise;
+ postFlightRequest?: PostFlightRequestFn;
getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat;
getValue?: (agg: TAggConfig, bucket: any) => any;
getKey?: (bucket: any, key: any, agg: TAggConfig) => any;
@@ -188,15 +191,7 @@ export class AggType<
* @param searchSessionId - searchSessionId to be used for grouping requests into a single search session
* @return {Promise}
*/
- postFlightRequest: (
- resp: any,
- aggConfigs: IAggConfigs,
- aggConfig: TAggConfig,
- searchSource: ISearchSource,
- inspectorRequestAdapter?: RequestAdapter,
- abortSignal?: AbortSignal,
- searchSessionId?: string
- ) => Promise;
+ postFlightRequest: PostFlightRequestFn;
/**
* Get the serialized format for the values produced by this agg type,
* overridden by several metrics that always output a simple number.
diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
index 56e720d237c455..2aa0d346afe343 100644
--- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
+++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
@@ -433,7 +433,7 @@ describe('Terms Agg Other bucket helper', () => {
aggConfigs.aggs[0] as IBucketAggConfig,
otherAggConfig()
);
- expect(mergedResponse.aggregations['1'].buckets[3].key).toEqual('__other__');
+ expect((mergedResponse!.aggregations!['1'] as any).buckets[3].key).toEqual('__other__');
}
});
@@ -455,7 +455,7 @@ describe('Terms Agg Other bucket helper', () => {
otherAggConfig()
);
- expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).toEqual(
+ expect((mergedResponse!.aggregations!['1'] as any).buckets[1]['2'].buckets[3].key).toEqual(
'__other__'
);
}
@@ -471,7 +471,7 @@ describe('Terms Agg Other bucket helper', () => {
aggConfigs.aggs[0] as IBucketAggConfig
);
expect(
- updatedResponse.aggregations['1'].buckets.find(
+ (updatedResponse!.aggregations!['1'] as any).buckets.find(
(bucket: Record) => bucket.key === '__missing__'
)
).toBeDefined();
diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts
index 742615bc49d8fa..6230ae897b1702 100644
--- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts
+++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts
@@ -7,6 +7,7 @@
*/
import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash';
+import { estypes } from '@elastic/elasticsearch';
import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '../../../../common';
import { AggGroupNames } from '../agg_groups';
import { IAggConfigs } from '../agg_configs';
@@ -42,7 +43,7 @@ const getNestedAggDSL = (aggNestedDsl: Record, startFromAggId: stri
*/
const getAggResultBuckets = (
aggConfigs: IAggConfigs,
- response: any,
+ response: estypes.SearchResponse['aggregations'],
aggWithOtherBucket: IBucketAggConfig,
key: string
) => {
@@ -72,8 +73,8 @@ const getAggResultBuckets = (
}
}
}
- if (responseAgg[aggWithOtherBucket.id]) {
- return responseAgg[aggWithOtherBucket.id].buckets;
+ if (responseAgg?.[aggWithOtherBucket.id]) {
+ return (responseAgg[aggWithOtherBucket.id] as any).buckets;
}
return [];
};
@@ -235,11 +236,11 @@ export const buildOtherBucketAgg = (
export const mergeOtherBucketAggResponse = (
aggsConfig: IAggConfigs,
- response: any,
+ response: estypes.SearchResponse,
otherResponse: any,
otherAgg: IBucketAggConfig,
requestAgg: Record
-) => {
+): estypes.SearchResponse => {
const updatedResponse = cloneDeep(response);
each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => {
if (!bucket.doc_count || key === undefined) return;
@@ -276,7 +277,7 @@ export const mergeOtherBucketAggResponse = (
};
export const updateMissingBucket = (
- response: any,
+ response: estypes.SearchResponse,
aggConfigs: IAggConfigs,
agg: IBucketAggConfig
) => {
diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts
index 77c9c6e391c0a0..03cf14a577a509 100644
--- a/src/plugins/data/common/search/aggs/buckets/terms.ts
+++ b/src/plugins/data/common/search/aggs/buckets/terms.ts
@@ -101,25 +101,21 @@ export const getTermsBucketAgg = () =>
nestedSearchSource.setField('aggs', filterAgg);
- const requestResponder = inspectorRequestAdapter?.start(
- i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
- defaultMessage: 'Other bucket',
- }),
- {
- description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
- defaultMessage:
- 'This request counts the number of documents that fall ' +
- 'outside the criterion of the data buckets.',
- }),
- searchSessionId,
- }
- );
-
const response = await nestedSearchSource
.fetch$({
abortSignal,
sessionId: searchSessionId,
- requestResponder,
+ inspector: {
+ adapter: inspectorRequestAdapter,
+ title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
+ defaultMessage: 'Other bucket',
+ }),
+ description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
+ defaultMessage:
+ 'This request counts the number of documents that fall ' +
+ 'outside the criterion of the data buckets.',
+ }),
+ },
})
.toPromise();
diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts
index c2566535916a8b..b30e5740fa3fb0 100644
--- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts
+++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts
@@ -9,7 +9,7 @@
import type { MockedKeys } from '@kbn/utility-types/jest';
import type { Filter } from '../../../es_query';
import type { IndexPattern } from '../../../index_patterns';
-import type { IAggConfig, IAggConfigs } from '../../aggs';
+import type { IAggConfigs } from '../../aggs';
import type { ISearchSource } from '../../search_source';
import { searchSourceCommonMock } from '../../search_source/mocks';
@@ -38,7 +38,6 @@ describe('esaggs expression function - public', () => {
filters: undefined,
indexPattern: ({ id: 'logstash-*' } as unknown) as jest.Mocked,
inspectorAdapters: {},
- metricsAtAllLevels: false,
partialRows: false,
query: undefined,
searchSessionId: 'abc123',
@@ -76,21 +75,7 @@ describe('esaggs expression function - public', () => {
test('setField(aggs)', async () => {
expect(searchSource.setField).toHaveBeenCalledTimes(5);
- expect(typeof (searchSource.setField as jest.Mock).mock.calls[2][1]).toBe('function');
- expect((searchSource.setField as jest.Mock).mock.calls[2][1]()).toEqual(
- mockParams.aggs.toDsl()
- );
- expect(mockParams.aggs.toDsl).toHaveBeenCalledWith(mockParams.metricsAtAllLevels);
-
- // make sure param is passed through
- jest.clearAllMocks();
- await handleRequest({
- ...mockParams,
- metricsAtAllLevels: true,
- });
- searchSource = await mockParams.searchSourceService.create();
- (searchSource.setField as jest.Mock).mock.calls[2][1]();
- expect(mockParams.aggs.toDsl).toHaveBeenCalledWith(true);
+ expect((searchSource.setField as jest.Mock).mock.calls[2][1]).toEqual(mockParams.aggs);
});
test('setField(filter)', async () => {
@@ -133,36 +118,24 @@ describe('esaggs expression function - public', () => {
test('calls searchSource.fetch', async () => {
await handleRequest(mockParams);
const searchSource = await mockParams.searchSourceService.create();
+
expect(searchSource.fetch$).toHaveBeenCalledWith({
abortSignal: mockParams.abortSignal,
sessionId: mockParams.searchSessionId,
+ inspector: {
+ title: 'Data',
+ description: 'This request queries Elasticsearch to fetch the data for the visualization.',
+ adapter: undefined,
+ },
});
});
- test('calls agg.postFlightRequest if it exiests and agg is enabled', async () => {
- mockParams.aggs.aggs[0].enabled = true;
- await handleRequest(mockParams);
- expect(mockParams.aggs.aggs[0].type.postFlightRequest).toHaveBeenCalledTimes(1);
-
- // ensure it works if the function doesn't exist
- jest.clearAllMocks();
- mockParams.aggs.aggs[0] = ({ type: { name: 'count' } } as unknown) as IAggConfig;
- expect(async () => await handleRequest(mockParams)).not.toThrowError();
- });
-
- test('should skip agg.postFlightRequest call if the agg is disabled', async () => {
- mockParams.aggs.aggs[0].enabled = false;
- await handleRequest(mockParams);
- expect(mockParams.aggs.aggs[0].type.postFlightRequest).toHaveBeenCalledTimes(0);
- });
-
test('tabifies response data', async () => {
await handleRequest(mockParams);
expect(tabifyAggResponse).toHaveBeenCalledWith(
mockParams.aggs,
{},
{
- metricsAtAllLevels: mockParams.metricsAtAllLevels,
partialRows: mockParams.partialRows,
timeRange: mockParams.timeRange,
}
diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts
index 5620698a475386..173b2067cad6bc 100644
--- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts
+++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts
@@ -40,28 +40,12 @@ export interface RequestHandlerParams {
getNow?: () => Date;
}
-function getRequestMainResponder(inspectorAdapters: Adapters, searchSessionId?: string) {
- return inspectorAdapters.requests?.start(
- i18n.translate('data.functions.esaggs.inspector.dataRequest.title', {
- defaultMessage: 'Data',
- }),
- {
- description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', {
- defaultMessage:
- 'This request queries Elasticsearch to fetch the data for the visualization.',
- }),
- searchSessionId,
- }
- );
-}
-
export const handleRequest = async ({
abortSignal,
aggs,
filters,
indexPattern,
inspectorAdapters,
- metricsAtAllLevels,
partialRows,
query,
searchSessionId,
@@ -100,9 +84,7 @@ export const handleRequest = async ({
},
});
- requestSearchSource.setField('aggs', function () {
- return aggs.toDsl(metricsAtAllLevels);
- });
+ requestSearchSource.setField('aggs', aggs);
requestSearchSource.onRequestStart((paramSearchSource, options) => {
return aggs.onSearchRequestStart(paramSearchSource, options);
@@ -128,35 +110,27 @@ export const handleRequest = async ({
requestSearchSource.setField('query', query);
inspectorAdapters.requests?.reset();
- const requestResponder = getRequestMainResponder(inspectorAdapters, searchSessionId);
- const response$ = await requestSearchSource.fetch$({
- abortSignal,
- sessionId: searchSessionId,
- requestResponder,
- });
-
- // Note that rawResponse is not deeply cloned here, so downstream applications using courier
- // must take care not to mutate it, or it could have unintended side effects, e.g. displaying
- // response data incorrectly in the inspector.
- let response = await response$.toPromise();
- for (const agg of aggs.aggs) {
- if (agg.enabled && typeof agg.type.postFlightRequest === 'function') {
- response = await agg.type.postFlightRequest(
- response,
- aggs,
- agg,
- requestSearchSource,
- inspectorAdapters.requests,
- abortSignal,
- searchSessionId
- );
- }
- }
+ const response = await requestSearchSource
+ .fetch$({
+ abortSignal,
+ sessionId: searchSessionId,
+ inspector: {
+ adapter: inspectorAdapters.requests,
+ title: i18n.translate('data.functions.esaggs.inspector.dataRequest.title', {
+ defaultMessage: 'Data',
+ }),
+ description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', {
+ defaultMessage:
+ 'This request queries Elasticsearch to fetch the data for the visualization.',
+ }),
+ },
+ })
+ .toPromise();
const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null;
const tabifyParams = {
- metricsAtAllLevels,
+ metricsAtAllLevels: aggs.hierarchical,
partialRows,
timeRange: parsedTimeRange
? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
diff --git a/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts b/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts
index 24507a7e13058e..e5a3acc23eee89 100644
--- a/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts
+++ b/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts
@@ -50,7 +50,7 @@ export function getRequestInspectorStats(searchSource: ISearchSource) {
/** @public */
export function getResponseInspectorStats(
- resp: estypes.SearchResponse,
+ resp?: estypes.SearchResponse,
searchSource?: ISearchSource
) {
const lastRequest =
diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts
index 3726e5d0c33e8c..7f8a4fceff05db 100644
--- a/src/plugins/data/common/search/search_source/search_source.test.ts
+++ b/src/plugins/data/common/search/search_source/search_source.test.ts
@@ -11,6 +11,10 @@ import { IndexPattern } from '../../index_patterns';
import { GetConfigFn } from '../../types';
import { fetchSoon } from './legacy';
import { SearchSource, SearchSourceDependencies, SortDirection } from './';
+import { AggConfigs, AggTypesRegistryStart } from '../../';
+import { mockAggTypesRegistry } from '../aggs/test_helpers';
+import { RequestResponder } from 'src/plugins/inspector/common';
+import { switchMap } from 'rxjs/operators';
jest.mock('./legacy', () => ({
fetchSoon: jest.fn().mockResolvedValue({}),
@@ -39,6 +43,21 @@ const indexPattern2 = ({
getSourceFiltering: () => mockSource2,
} as unknown) as IndexPattern;
+const fields3 = [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }];
+const indexPattern3 = ({
+ title: 'foo',
+ fields: {
+ getByName: (name: string) => {
+ return fields3.find((field) => field.name === name);
+ },
+ filter: () => {
+ return fields3;
+ },
+ },
+ getComputedFields,
+ getSourceFiltering: () => mockSource,
+} as unknown) as IndexPattern;
+
const runtimeFieldDef = {
type: 'keyword',
script: {
@@ -61,8 +80,8 @@ describe('SearchSource', () => {
.fn()
.mockReturnValue(
of(
- { rawResponse: { isPartial: true, isRunning: true } },
- { rawResponse: { isPartial: false, isRunning: false } }
+ { rawResponse: { test: 1 }, isPartial: true, isRunning: true },
+ { rawResponse: { test: 2 }, isPartial: false, isRunning: false }
)
);
@@ -81,17 +100,19 @@ describe('SearchSource', () => {
describe('#getField()', () => {
test('gets the value for the property', () => {
- searchSource.setField('aggs', 5);
- expect(searchSource.getField('aggs')).toBe(5);
+ searchSource.setField('aggs', { i: 5 });
+ expect(searchSource.getField('aggs')).toStrictEqual({ i: 5 });
});
});
describe('#getFields()', () => {
test('gets the value for the property', () => {
- searchSource.setField('aggs', 5);
+ searchSource.setField('aggs', { i: 5 });
expect(searchSource.getFields()).toMatchInlineSnapshot(`
Object {
- "aggs": 5,
+ "aggs": Object {
+ "i": 5,
+ },
}
`);
});
@@ -100,7 +121,7 @@ describe('SearchSource', () => {
describe('#removeField()', () => {
test('remove property', () => {
searchSource = new SearchSource({}, searchSourceDependencies);
- searchSource.setField('aggs', 5);
+ searchSource.setField('aggs', { i: 5 });
searchSource.removeField('aggs');
expect(searchSource.getField('aggs')).toBeFalsy();
});
@@ -108,8 +129,20 @@ describe('SearchSource', () => {
describe('#setField() / #flatten', () => {
test('sets the value for the property', () => {
- searchSource.setField('aggs', 5);
- expect(searchSource.getField('aggs')).toBe(5);
+ searchSource.setField('aggs', { i: 5 });
+ expect(searchSource.getField('aggs')).toStrictEqual({ i: 5 });
+ });
+
+ test('sets the value for the property with AggConfigs', () => {
+ const typesRegistry = mockAggTypesRegistry();
+
+ const ac = new AggConfigs(indexPattern3, [{ type: 'avg', params: { field: 'field1' } }], {
+ typesRegistry,
+ });
+
+ searchSource.setField('aggs', ac);
+ const request = searchSource.getSearchRequestBody();
+ expect(request.aggs).toStrictEqual({ '1': { avg: { field: 'field1' } } });
});
describe('computed fields handling', () => {
@@ -631,7 +664,7 @@ describe('SearchSource', () => {
const fn = jest.fn();
searchSource.onRequestStart(fn);
const options = {};
- await searchSource.fetch(options);
+ await searchSource.fetch$(options).toPromise();
expect(fn).toBeCalledWith(searchSource, options);
});
@@ -644,7 +677,7 @@ describe('SearchSource', () => {
const parentFn = jest.fn();
parent.onRequestStart(parentFn);
const options = {};
- await searchSource.fetch(options);
+ await searchSource.fetch$(options).toPromise();
expect(fn).toBeCalledWith(searchSource, options);
expect(parentFn).not.toBeCalled();
@@ -664,69 +697,13 @@ describe('SearchSource', () => {
const parentFn = jest.fn();
parent.onRequestStart(parentFn);
const options = {};
- await searchSource.fetch(options);
+ await searchSource.fetch$(options).toPromise();
expect(fn).toBeCalledWith(searchSource, options);
expect(parentFn).toBeCalledWith(searchSource, options);
});
});
- describe('#legacy fetch()', () => {
- beforeEach(() => {
- searchSourceDependencies = {
- ...searchSourceDependencies,
- getConfig: jest.fn(() => {
- return true; // batchSearches = true
- }) as GetConfigFn,
- };
- });
-
- test('should call msearch', async () => {
- searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
- const options = {};
- await searchSource.fetch(options);
- expect(fetchSoon).toBeCalledTimes(1);
- });
- });
-
- describe('#search service fetch()', () => {
- test('should call msearch', async () => {
- searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
- const options = {};
-
- await searchSource.fetch(options);
- expect(mockSearchMethod).toBeCalledTimes(1);
- });
-
- test('should return partial results', (done) => {
- searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
- const options = {};
-
- const next = jest.fn();
- const complete = () => {
- expect(next).toBeCalledTimes(2);
- expect(next.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "isPartial": true,
- "isRunning": true,
- },
- ]
- `);
- expect(next.mock.calls[1]).toMatchInlineSnapshot(`
- Array [
- Object {
- "isPartial": false,
- "isRunning": false,
- },
- ]
- `);
- done();
- };
- searchSource.fetch$(options).subscribe({ next, complete });
- });
- });
-
describe('#serialize', () => {
test('should reference index patterns', () => {
const indexPattern123 = { id: '123' } as IndexPattern;
@@ -884,4 +861,373 @@ describe('SearchSource', () => {
);
});
});
+
+ describe('fetch$', () => {
+ describe('#legacy fetch()', () => {
+ beforeEach(() => {
+ searchSourceDependencies = {
+ ...searchSourceDependencies,
+ getConfig: jest.fn(() => {
+ return true; // batchSearches = true
+ }) as GetConfigFn,
+ };
+ });
+
+ test('should call msearch', async () => {
+ searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
+ const options = {};
+ await searchSource.fetch$(options).toPromise();
+ expect(fetchSoon).toBeCalledTimes(1);
+ });
+ });
+
+ describe('responses', () => {
+ test('should return partial results', async () => {
+ searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
+ const options = {};
+
+ const next = jest.fn();
+ const complete = jest.fn();
+ const res$ = searchSource.fetch$(options);
+ res$.subscribe({ next, complete });
+ await res$.toPromise();
+
+ expect(next).toBeCalledTimes(2);
+ expect(complete).toBeCalledTimes(1);
+ expect(next.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "test": 1,
+ },
+ ]
+ `);
+ expect(next.mock.calls[1]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "test": 2,
+ },
+ ]
+ `);
+ });
+
+ test('shareReplays result', async () => {
+ searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
+ const options = {};
+
+ const next = jest.fn();
+ const complete = jest.fn();
+ const next2 = jest.fn();
+ const complete2 = jest.fn();
+ const res$ = searchSource.fetch$(options);
+ res$.subscribe({ next, complete });
+ res$.subscribe({ next: next2, complete: complete2 });
+ await res$.toPromise();
+
+ expect(next).toBeCalledTimes(2);
+ expect(next2).toBeCalledTimes(2);
+ expect(complete).toBeCalledTimes(1);
+ expect(complete2).toBeCalledTimes(1);
+ expect(searchSourceDependencies.search).toHaveBeenCalledTimes(1);
+ });
+
+ test('should emit error on empty response', async () => {
+ searchSourceDependencies.search = mockSearchMethod = jest
+ .fn()
+ .mockReturnValue(
+ of({ rawResponse: { test: 1 }, isPartial: true, isRunning: true }, undefined)
+ );
+
+ searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
+ const options = {};
+
+ const next = jest.fn();
+ const error = jest.fn();
+ const complete = jest.fn();
+ const res$ = searchSource.fetch$(options);
+ res$.subscribe({ next, error, complete });
+ await res$.toPromise().catch((e) => {});
+
+ expect(next).toBeCalledTimes(1);
+ expect(error).toBeCalledTimes(1);
+ expect(complete).toBeCalledTimes(0);
+ expect(next.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "test": 1,
+ },
+ ]
+ `);
+ expect(error.mock.calls[0][0]).toBe(undefined);
+ });
+ });
+
+ describe('inspector', () => {
+ let requestResponder: RequestResponder;
+ beforeEach(() => {
+ requestResponder = ({
+ stats: jest.fn(),
+ ok: jest.fn(),
+ error: jest.fn(),
+ json: jest.fn(),
+ } as unknown) as RequestResponder;
+ });
+
+ test('calls inspector if provided', async () => {
+ const options = {
+ inspector: {
+ title: 'a',
+ adapter: {
+ start: jest.fn().mockReturnValue(requestResponder),
+ } as any,
+ },
+ };
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ await searchSource.fetch$(options).toPromise();
+
+ expect(options.inspector.adapter.start).toBeCalledTimes(1);
+ expect(requestResponder.error).not.toBeCalled();
+ expect(requestResponder.json).toBeCalledTimes(1);
+ expect(requestResponder.ok).toBeCalledTimes(1);
+ // First and last
+ expect(requestResponder.stats).toBeCalledTimes(2);
+ });
+
+ test('calls inspector only once, with multiple subs (shareReplay)', async () => {
+ const options = {
+ inspector: {
+ title: 'a',
+ adapter: {
+ start: jest.fn().mockReturnValue(requestResponder),
+ } as any,
+ },
+ };
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ const res$ = searchSource.fetch$(options);
+
+ const complete1 = jest.fn();
+ const complete2 = jest.fn();
+
+ res$.subscribe({
+ complete: complete1,
+ });
+ res$.subscribe({
+ complete: complete2,
+ });
+
+ await res$.toPromise();
+
+ expect(complete1).toBeCalledTimes(1);
+ expect(complete2).toBeCalledTimes(1);
+ expect(options.inspector.adapter.start).toBeCalledTimes(1);
+ });
+
+ test('calls error on inspector', async () => {
+ const options = {
+ inspector: {
+ title: 'a',
+ adapter: {
+ start: jest.fn().mockReturnValue(requestResponder),
+ } as any,
+ },
+ };
+
+ searchSourceDependencies.search = jest.fn().mockReturnValue(of(Promise.reject('aaaaa')));
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ await searchSource
+ .fetch$(options)
+ .toPromise()
+ .catch(() => {});
+
+ expect(options.inspector.adapter.start).toBeCalledTimes(1);
+ expect(requestResponder.json).toBeCalledTimes(1);
+ expect(requestResponder.error).toBeCalledTimes(1);
+ expect(requestResponder.ok).toBeCalledTimes(0);
+ expect(requestResponder.stats).toBeCalledTimes(0);
+ });
+ });
+
+ describe('postFlightRequest', () => {
+ let fetchSub: any;
+
+ function getAggConfigs(typesRegistry: AggTypesRegistryStart, enabled: boolean) {
+ return new AggConfigs(
+ indexPattern3,
+ [
+ {
+ type: 'avg',
+ enabled,
+ params: { field: 'field1' },
+ },
+ ],
+ {
+ typesRegistry,
+ }
+ );
+ }
+
+ beforeEach(() => {
+ fetchSub = {
+ next: jest.fn(),
+ complete: jest.fn(),
+ error: jest.fn(),
+ };
+ });
+
+ test('doesnt call any post flight requests if disabled', async () => {
+ const typesRegistry = mockAggTypesRegistry();
+ typesRegistry.get('avg').postFlightRequest = jest.fn();
+ const ac = getAggConfigs(typesRegistry, false);
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ searchSource.setField('aggs', ac);
+ const fetch$ = searchSource.fetch$({});
+ fetch$.subscribe(fetchSub);
+ await fetch$.toPromise();
+
+ expect(fetchSub.next).toHaveBeenCalledTimes(2);
+ expect(fetchSub.complete).toHaveBeenCalledTimes(1);
+ expect(fetchSub.error).toHaveBeenCalledTimes(0);
+
+ expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(0);
+ });
+
+ test('doesnt call any post flight if searchsource has error', async () => {
+ const typesRegistry = mockAggTypesRegistry();
+ typesRegistry.get('avg').postFlightRequest = jest.fn();
+ const ac = getAggConfigs(typesRegistry, true);
+
+ searchSourceDependencies.search = jest.fn().mockImplementation(() =>
+ of(1).pipe(
+ switchMap((r) => {
+ throw r;
+ })
+ )
+ );
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ searchSource.setField('aggs', ac);
+ const fetch$ = searchSource.fetch$({});
+ fetch$.subscribe(fetchSub);
+ await fetch$.toPromise().catch((e) => {});
+
+ expect(fetchSub.next).toHaveBeenCalledTimes(0);
+ expect(fetchSub.complete).toHaveBeenCalledTimes(0);
+ expect(fetchSub.error).toHaveBeenNthCalledWith(1, 1);
+
+ expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(0);
+ });
+
+ test('calls post flight requests, fires 1 extra response, returns last response', async () => {
+ const typesRegistry = mockAggTypesRegistry();
+ typesRegistry.get('avg').postFlightRequest = jest.fn().mockResolvedValue({
+ other: 5,
+ });
+
+ const allac = new AggConfigs(
+ indexPattern3,
+ [
+ {
+ type: 'avg',
+ enabled: true,
+ params: { field: 'field1' },
+ },
+ {
+ type: 'avg',
+ enabled: true,
+ params: { field: 'field2' },
+ },
+ {
+ type: 'avg',
+ enabled: true,
+ params: { field: 'foo-bar' },
+ },
+ ],
+ {
+ typesRegistry,
+ }
+ );
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ searchSource.setField('aggs', allac);
+ const fetch$ = searchSource.fetch$({});
+ fetch$.subscribe(fetchSub);
+
+ const resp = await fetch$.toPromise();
+
+ expect(fetchSub.next).toHaveBeenCalledTimes(3);
+ expect(fetchSub.complete).toHaveBeenCalledTimes(1);
+ expect(fetchSub.error).toHaveBeenCalledTimes(0);
+ expect(resp).toStrictEqual({ other: 5 });
+ expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(3);
+ });
+
+ test('calls post flight requests only once, with multiple subs (shareReplay)', async () => {
+ const typesRegistry = mockAggTypesRegistry();
+ typesRegistry.get('avg').postFlightRequest = jest.fn().mockResolvedValue({
+ other: 5,
+ });
+
+ const allac = new AggConfigs(
+ indexPattern3,
+ [
+ {
+ type: 'avg',
+ enabled: true,
+ params: { field: 'field1' },
+ },
+ ],
+ {
+ typesRegistry,
+ }
+ );
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ searchSource.setField('aggs', allac);
+ const fetch$ = searchSource.fetch$({});
+ fetch$.subscribe(fetchSub);
+
+ const fetchSub2 = {
+ next: jest.fn(),
+ complete: jest.fn(),
+ error: jest.fn(),
+ };
+ fetch$.subscribe(fetchSub2);
+
+ await fetch$.toPromise();
+
+ expect(fetchSub.next).toHaveBeenCalledTimes(3);
+ expect(fetchSub.complete).toHaveBeenCalledTimes(1);
+ expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(1);
+ });
+
+ test('calls post flight requests, handles error', async () => {
+ const typesRegistry = mockAggTypesRegistry();
+ typesRegistry.get('avg').postFlightRequest = jest.fn().mockRejectedValue(undefined);
+ const ac = getAggConfigs(typesRegistry, true);
+
+ searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('index', indexPattern);
+ searchSource.setField('aggs', ac);
+ const fetch$ = searchSource.fetch$({});
+ fetch$.subscribe(fetchSub);
+
+ await fetch$.toPromise().catch(() => {});
+
+ expect(fetchSub.next).toHaveBeenCalledTimes(2);
+ expect(fetchSub.complete).toHaveBeenCalledTimes(0);
+ expect(fetchSub.error).toHaveBeenCalledTimes(1);
+ expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
});
diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts
index e1e7a8292d6773..1c1c32228703f1 100644
--- a/src/plugins/data/common/search/search_source/search_source.ts
+++ b/src/plugins/data/common/search/search_source/search_source.ts
@@ -60,12 +60,22 @@
import { setWith } from '@elastic/safer-lodash-set';
import { uniqueId, keyBy, pick, difference, isFunction, isEqual, uniqWith, isObject } from 'lodash';
-import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators';
-import { defer, from } from 'rxjs';
+import {
+ catchError,
+ finalize,
+ first,
+ last,
+ map,
+ shareReplay,
+ switchMap,
+ tap,
+} from 'rxjs/operators';
+import { defer, EMPTY, from, Observable } from 'rxjs';
+import { estypes } from '@elastic/elasticsearch';
import { normalizeSortRequest } from './normalize_sort_request';
import { fieldWildcardFilter } from '../../../../kibana_utils/common';
import { IIndexPattern, IndexPattern, IndexPatternField } from '../../index_patterns';
-import { ISearchGeneric, ISearchOptions } from '../..';
+import { AggConfigs, ISearchGeneric, ISearchOptions } from '../..';
import type {
ISearchSource,
SearchFieldValue,
@@ -75,7 +85,15 @@ import type {
import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch';
import { getRequestInspectorStats, getResponseInspectorStats } from './inspect';
-import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common';
+import {
+ getEsQueryConfig,
+ buildEsQuery,
+ Filter,
+ UI_SETTINGS,
+ isErrorResponse,
+ isPartialResponse,
+ IKibanaSearchResponse,
+} from '../../../common';
import { getHighlightRequest } from '../../../common/field_formats';
import { fetchSoon } from './legacy';
import { extractReferences } from './extract_references';
@@ -256,10 +274,8 @@ export class SearchSource {
*/
fetch$(options: ISearchOptions = {}) {
const { getConfig } = this.dependencies;
- return defer(() => this.requestIsStarting(options)).pipe(
- tap(() => {
- options.requestResponder?.stats(getRequestInspectorStats(this));
- }),
+
+ const s$ = defer(() => this.requestIsStarting(options)).pipe(
switchMap(() => {
const searchRequest = this.flatten();
this.history = [searchRequest];
@@ -273,21 +289,14 @@ export class SearchSource {
}),
tap((response) => {
// TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved
- if ((response as any).error) {
+ if (!response || (response as any).error) {
throw new RequestFailure(null, response);
- } else {
- options.requestResponder?.stats(getResponseInspectorStats(response, this));
- options.requestResponder?.ok({ json: response });
}
}),
- catchError((e) => {
- options.requestResponder?.error({ json: e });
- throw e;
- }),
- finalize(() => {
- options.requestResponder?.json(this.getSearchRequestBody());
- })
+ shareReplay()
);
+
+ return this.inspectSearch(s$, options);
}
/**
@@ -328,9 +337,96 @@ export class SearchSource {
* PRIVATE APIS
******/
+ private inspectSearch(s$: Observable>, options: ISearchOptions) {
+ const { id, title, description, adapter } = options.inspector || { title: '' };
+
+ const requestResponder = adapter?.start(title, {
+ id,
+ description,
+ searchSessionId: options.sessionId,
+ });
+
+ const trackRequestBody = () => {
+ try {
+ requestResponder?.json(this.getSearchRequestBody());
+ } catch (e) {} // eslint-disable-line no-empty
+ };
+
+ // Track request stats on first emit, swallow errors
+ const first$ = s$
+ .pipe(
+ first(undefined, null),
+ tap(() => {
+ requestResponder?.stats(getRequestInspectorStats(this));
+ trackRequestBody();
+ }),
+ catchError(() => {
+ trackRequestBody();
+ return EMPTY;
+ }),
+ finalize(() => {
+ first$.unsubscribe();
+ })
+ )
+ .subscribe();
+
+ // Track response stats on last emit, as well as errors
+ const last$ = s$
+ .pipe(
+ catchError((e) => {
+ requestResponder?.error({ json: e });
+ return EMPTY;
+ }),
+ last(undefined, null),
+ tap((finalResponse) => {
+ if (finalResponse) {
+ requestResponder?.stats(getResponseInspectorStats(finalResponse, this));
+ requestResponder?.ok({ json: finalResponse });
+ }
+ }),
+ finalize(() => {
+ last$.unsubscribe();
+ })
+ )
+ .subscribe();
+
+ return s$;
+ }
+
+ private hasPostFlightRequests() {
+ const aggs = this.getField('aggs');
+ if (aggs instanceof AggConfigs) {
+ return aggs.aggs.some(
+ (agg) => agg.enabled && typeof agg.type.postFlightRequest === 'function'
+ );
+ } else {
+ return false;
+ }
+ }
+
+ private async fetchOthers(response: estypes.SearchResponse, options: ISearchOptions) {
+ const aggs = this.getField('aggs');
+ if (aggs instanceof AggConfigs) {
+ for (const agg of aggs.aggs) {
+ if (agg.enabled && typeof agg.type.postFlightRequest === 'function') {
+ response = await agg.type.postFlightRequest(
+ response,
+ aggs,
+ agg,
+ this,
+ options.inspector?.adapter,
+ options.abortSignal,
+ options.sessionId
+ );
+ }
+ }
+ return response;
+ }
+ }
+
/**
* Run a search using the search service
- * @return {Promise>}
+ * @return {Observable>}
*/
private fetchSearch$(searchRequest: SearchRequest, options: ISearchOptions) {
const { search, getConfig, onResponse } = this.dependencies;
@@ -340,6 +436,43 @@ export class SearchSource {
});
return search({ params, indexType: searchRequest.indexType }, options).pipe(
+ switchMap((response) => {
+ return new Observable>((obs) => {
+ if (isErrorResponse(response)) {
+ obs.error(response);
+ } else if (isPartialResponse(response)) {
+ obs.next(response);
+ } else {
+ if (!this.hasPostFlightRequests()) {
+ obs.next(response);
+ obs.complete();
+ } else {
+ // Treat the complete response as partial, then run the postFlightRequests.
+ obs.next({
+ ...response,
+ isPartial: true,
+ isRunning: true,
+ });
+ const sub = from(this.fetchOthers(response.rawResponse, options)).subscribe({
+ next: (responseWithOther) => {
+ obs.next({
+ ...response,
+ rawResponse: responseWithOther,
+ });
+ },
+ error: (e) => {
+ obs.error(e);
+ sub.unsubscribe();
+ },
+ complete: () => {
+ obs.complete();
+ sub.unsubscribe();
+ },
+ });
+ }
+ }
+ });
+ }),
map(({ rawResponse }) => onResponse(searchRequest, rawResponse))
);
}
@@ -452,6 +585,12 @@ export class SearchSource {
getConfig(UI_SETTINGS.SORT_OPTIONS)
);
return addToBody(key, sort);
+ case 'aggs':
+ if ((val as any) instanceof AggConfigs) {
+ return addToBody('aggs', val.toDsl());
+ } else {
+ return addToBody('aggs', val);
+ }
default:
return addToBody(key, val);
}
diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts
index a178b38693d92e..99f3f67a5e257c 100644
--- a/src/plugins/data/common/search/search_source/types.ts
+++ b/src/plugins/data/common/search/search_source/types.ts
@@ -7,6 +7,7 @@
*/
import { NameList } from 'elasticsearch';
+import { IAggConfigs } from 'src/plugins/data/public';
import { Query } from '../..';
import { Filter } from '../../es_query';
import { IndexPattern } from '../../index_patterns';
@@ -78,7 +79,7 @@ export interface SearchSourceFields {
/**
* {@link AggConfigs}
*/
- aggs?: any;
+ aggs?: object | IAggConfigs | (() => object);
from?: number;
size?: number;
source?: NameList;
diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts
index 168d4cf9d4c370..74fbc7ba4cfa4a 100644
--- a/src/plugins/data/common/search/tabify/index.ts
+++ b/src/plugins/data/common/search/tabify/index.ts
@@ -6,27 +6,6 @@
* Side Public License, v 1.
*/
-import { SearchResponse } from 'elasticsearch';
-import { SearchSource } from '../search_source';
-import { tabifyAggResponse } from './tabify';
-import { tabifyDocs, TabifyDocsOptions } from './tabify_docs';
-import { TabbedResponseWriterOptions } from './types';
-
-export const tabify = (
- searchSource: SearchSource,
- esResponse: SearchResponse,
- opts: Partial | TabifyDocsOptions
-) => {
- return !esResponse.aggregations
- ? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions)
- : tabifyAggResponse(
- searchSource.getField('aggs'),
- esResponse,
- opts as Partial
- );
-};
-
-export { tabifyDocs };
-
+export { tabifyDocs } from './tabify_docs';
export { tabifyAggResponse } from './tabify';
export { tabifyGetColumns } from './get_columns';
diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts
index 37de8dc49d3c6a..e3ec499a0020db 100644
--- a/src/plugins/data/common/search/types.ts
+++ b/src/plugins/data/common/search/types.ts
@@ -9,7 +9,7 @@
import { Observable } from 'rxjs';
import { IEsSearchRequest, IEsSearchResponse } from './es_search';
import { IndexPattern } from '..';
-import type { RequestResponder } from '../../../inspector/common';
+import type { RequestAdapter } from '../../../inspector/common';
export type ISearchGeneric = <
SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest,
@@ -81,6 +81,13 @@ export interface IKibanaSearchRequest {
params?: Params;
}
+export interface IInspectorInfo {
+ adapter?: RequestAdapter;
+ title: string;
+ id?: string;
+ description?: string;
+}
+
export interface ISearchOptions {
/**
* An `AbortSignal` that allows the caller of `search` to abort a search request.
@@ -117,10 +124,12 @@ export interface ISearchOptions {
/**
* Index pattern reference is used for better error messages
*/
-
indexPattern?: IndexPattern;
- requestResponder?: RequestResponder;
+ /**
+ * Inspector integration options
+ */
+ inspector?: IInspectorInfo;
}
/**
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 35f13fc855e998..0dd06691d68bbd 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -46,6 +46,7 @@ import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_
import { History } from 'history';
import { Href } from 'history';
import { HttpSetup } from 'kibana/public';
+import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public';
import { IconType } from '@elastic/eui';
import { IncomingHttpHeaders } from 'http';
import { InjectedIntl } from '@kbn/i18n/react';
@@ -254,6 +255,8 @@ export class AggConfigs {
getResponseAggById(id: string): AggConfig | undefined;
getResponseAggs(): AggConfig[];
// (undocumented)
+ hierarchical?: boolean;
+ // (undocumented)
indexPattern: IndexPattern;
jsonDataEquals(aggConfigs: AggConfig[]): boolean;
// (undocumented)
@@ -267,7 +270,7 @@ export class AggConfigs {
// (undocumented)
timeRange?: TimeRange;
// (undocumented)
- toDsl(hierarchical?: boolean): Record;
+ toDsl(): Record;
}
// @internal (undocumented)
@@ -1672,13 +1675,11 @@ export type ISearchGeneric = >;
+ fetch$(options?: ISearchOptions): Observable>;
// @deprecated
- fetch(options?: ISearchOptions): Promise>;
+ fetch(options?: ISearchOptions): Promise>;
getField(field: K, recurse?: boolean): SearchSourceFields[K];
getFields(): SearchSourceFields;
getId(): string;
@@ -2462,7 +2463,7 @@ export class SearchSource {
// @public
export interface SearchSourceFields {
// (undocumented)
- aggs?: any;
+ aggs?: object | IAggConfigs_2 | (() => object);
// Warning: (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts
fields?: SearchFieldValue[];
// @deprecated
diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts
index d7a6446781c437..e75bd7be219de9 100644
--- a/src/plugins/data/public/search/expressions/esaggs.test.ts
+++ b/src/plugins/data/public/search/expressions/esaggs.test.ts
@@ -100,17 +100,20 @@ describe('esaggs expression function - public', () => {
expect(handleEsaggsRequest).toHaveBeenCalledWith({
abortSignal: mockHandlers.abortSignal,
- aggs: { foo: 'bar' },
+ aggs: {
+ foo: 'bar',
+ hierarchical: true,
+ },
filters: undefined,
indexPattern: {},
inspectorAdapters: mockHandlers.inspectorAdapters,
- metricsAtAllLevels: args.metricsAtAllLevels,
partialRows: args.partialRows,
query: undefined,
searchSessionId: 'abc123',
searchSourceService: startDependencies.searchSource,
timeFields: args.timeFields,
timeRange: undefined,
+ getNow: undefined,
});
});
diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts
index 45d24af3a6ebb5..1e3d56c71e423b 100644
--- a/src/plugins/data/public/search/expressions/esaggs.ts
+++ b/src/plugins/data/public/search/expressions/esaggs.ts
@@ -8,7 +8,6 @@
import { get } from 'lodash';
import { StartServicesAccessor } from 'src/core/public';
-import { Adapters } from 'src/plugins/inspector/common';
import {
EsaggsExpressionFunctionDefinition,
EsaggsStartDependencies,
@@ -44,14 +43,14 @@ export function getFunctionDefinition({
indexPattern,
args.aggs!.map((agg) => agg.value)
);
+ aggConfigs.hierarchical = args.metricsAtAllLevels;
return await handleEsaggsRequest({
- abortSignal: (abortSignal as unknown) as AbortSignal,
+ abortSignal,
aggs: aggConfigs,
filters: get(input, 'filters', undefined),
indexPattern,
- inspectorAdapters: inspectorAdapters as Adapters,
- metricsAtAllLevels: args.metricsAtAllLevels,
+ inspectorAdapters,
partialRows: args.partialRows,
query: get(input, 'query', undefined) as any,
searchSessionId: getSearchSessionId(),
diff --git a/src/plugins/data/server/search/expressions/esaggs.test.ts b/src/plugins/data/server/search/expressions/esaggs.test.ts
index 124a171de63782..15287e9d8cf5bd 100644
--- a/src/plugins/data/server/search/expressions/esaggs.test.ts
+++ b/src/plugins/data/server/search/expressions/esaggs.test.ts
@@ -108,11 +108,13 @@ describe('esaggs expression function - server', () => {
expect(handleEsaggsRequest).toHaveBeenCalledWith({
abortSignal: mockHandlers.abortSignal,
- aggs: { foo: 'bar' },
+ aggs: {
+ foo: 'bar',
+ hierarchical: args.metricsAtAllLevels,
+ },
filters: undefined,
indexPattern: {},
inspectorAdapters: mockHandlers.inspectorAdapters,
- metricsAtAllLevels: args.metricsAtAllLevels,
partialRows: args.partialRows,
query: undefined,
searchSessionId: 'abc123',
diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts
index 61fd320d89b951..bb22a491b157e9 100644
--- a/src/plugins/data/server/search/expressions/esaggs.ts
+++ b/src/plugins/data/server/search/expressions/esaggs.ts
@@ -9,7 +9,6 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { KibanaRequest, StartServicesAccessor } from 'src/core/server';
-import { Adapters } from 'src/plugins/inspector/common';
import {
EsaggsExpressionFunctionDefinition,
EsaggsStartDependencies,
@@ -61,13 +60,14 @@ export function getFunctionDefinition({
args.aggs!.map((agg) => agg.value)
);
+ aggConfigs.hierarchical = args.metricsAtAllLevels;
+
return await handleEsaggsRequest({
- abortSignal: (abortSignal as unknown) as AbortSignal,
+ abortSignal,
aggs: aggConfigs,
filters: get(input, 'filters', undefined),
indexPattern,
- inspectorAdapters: inspectorAdapters as Adapters,
- metricsAtAllLevels: args.metricsAtAllLevels,
+ inspectorAdapters,
partialRows: args.partialRows,
query: get(input, 'query', undefined) as any,
searchSessionId: getSearchSessionId(),
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 622356c4441ac3..3316e8102e50ac 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -26,12 +26,14 @@ import { Ensure } from '@kbn/utility-types';
import { EnvironmentMode } from '@kbn/config';
import { ErrorToastOptions } from 'src/core/public/notifications';
import { estypes } from '@elastic/elasticsearch';
+import { EventEmitter } from 'events';
import { ExecutionContext } from 'src/plugins/expressions/common';
import { ExpressionAstExpression } from 'src/plugins/expressions/common';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { ExpressionValueBoxed } from 'src/plugins/expressions/common';
import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils';
+import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public';
import { ISavedObjectsRepository } from 'src/core/server';
import { IScopedClusterClient } from 'src/core/server';
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';
@@ -999,13 +1001,11 @@ export interface IScopedSearchClient extends ISearchClient {
export interface ISearchOptions {
abortSignal?: AbortSignal;
indexPattern?: IndexPattern;
+ // Warning: (ae-forgotten-export) The symbol "IInspectorInfo" needs to be exported by the entry point index.d.ts
+ inspector?: IInspectorInfo;
isRestore?: boolean;
isStored?: boolean;
legacyHitsTotal?: boolean;
- // Warning: (ae-forgotten-export) The symbol "RequestResponder" needs to be exported by the entry point index.d.ts
- //
- // (undocumented)
- requestResponder?: RequestResponder;
sessionId?: string;
strategy?: string;
}
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 35a89eb45f35ee..4099d5e8ef7e29 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -415,11 +415,20 @@ function discoverController($route, $scope) {
$scope.fetchStatus = fetchStatuses.LOADING;
$scope.resultState = getResultState($scope.fetchStatus, $scope.rows);
+ inspectorAdapters.requests.reset();
return $scope.volatileSearchSource
.fetch$({
abortSignal: abortController.signal,
sessionId: searchSessionId,
- requestResponder: getRequestResponder({ searchSessionId }),
+ inspector: {
+ adapter: inspectorAdapters.requests,
+ title: i18n.translate('discover.inspectorRequestDataTitle', {
+ defaultMessage: 'data',
+ }),
+ description: i18n.translate('discover.inspectorRequestDescription', {
+ defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
+ }),
+ },
})
.toPromise()
.then(onResults)
@@ -465,17 +474,6 @@ function discoverController($route, $scope) {
await refetch$.next();
};
- function getRequestResponder({ searchSessionId = null } = { searchSessionId: null }) {
- inspectorAdapters.requests.reset();
- const title = i18n.translate('discover.inspectorRequestDataTitle', {
- defaultMessage: 'data',
- });
- const description = i18n.translate('discover.inspectorRequestDescription', {
- defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
- });
- return inspectorAdapters.requests.start(title, { description, searchSessionId });
- }
-
$scope.resetQuery = function () {
history.push(
$route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/'
diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts
index 237da72ae3a523..dbaf07fed18c29 100644
--- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts
+++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts
@@ -317,17 +317,6 @@ export class SearchEmbeddable
// Log request to inspector
this.inspectorAdapters.requests!.reset();
- const title = i18n.translate('discover.embeddable.inspectorRequestDataTitle', {
- defaultMessage: 'Data',
- });
- const description = i18n.translate('discover.embeddable.inspectorRequestDescription', {
- defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
- });
-
- const requestResponder = this.inspectorAdapters.requests!.start(title, {
- description,
- searchSessionId,
- });
this.searchScope.$apply(() => {
this.searchScope!.isLoading = true;
@@ -340,7 +329,16 @@ export class SearchEmbeddable
.fetch$({
abortSignal: this.abortController.signal,
sessionId: searchSessionId,
- requestResponder,
+ inspector: {
+ adapter: this.inspectorAdapters.requests,
+ title: i18n.translate('discover.embeddable.inspectorRequestDataTitle', {
+ defaultMessage: 'Data',
+ }),
+ description: i18n.translate('discover.embeddable.inspectorRequestDescription', {
+ defaultMessage:
+ 'This request queries Elasticsearch to fetch the data for the search.',
+ }),
+ },
})
.toPromise();
this.updateOutput({ loading: false, error: undefined });
diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts
index 2915eaec8ac776..50043772af95bb 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts
@@ -167,12 +167,6 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
const abortController = new AbortController();
registerCancelCallback(() => abortController.abort());
- const requestResponder = this.getInspectorAdapters()?.requests?.start(requestName, {
- id: requestId,
- description: requestDescription,
- searchSessionId,
- });
-
let resp;
try {
resp = await searchSource
@@ -180,7 +174,12 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
abortSignal: abortController.signal,
sessionId: searchSessionId,
legacyHitsTotal: false,
- requestResponder,
+ inspector: {
+ adapter: this.getInspectorAdapters()?.requests,
+ id: requestId,
+ title: requestName,
+ description: requestDescription,
+ },
})
.toPromise();
} catch (error) {
diff --git a/x-pack/test/examples/search_examples/index.ts b/x-pack/test/examples/search_examples/index.ts
index 13eac7566525e2..65e214cda4cf8c 100644
--- a/x-pack/test/examples/search_examples/index.ts
+++ b/x-pack/test/examples/search_examples/index.ts
@@ -23,7 +23,8 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC
await esArchiver.unload('lens/basic');
});
- loadTestFile(require.resolve('./search_sessions_cache'));
loadTestFile(require.resolve('./search_session_example'));
+ loadTestFile(require.resolve('./search_example'));
+ loadTestFile(require.resolve('./search_sessions_cache'));
});
}
diff --git a/x-pack/test/examples/search_examples/search_example.ts b/x-pack/test/examples/search_examples/search_example.ts
new file mode 100644
index 00000000000000..19a9535ebb9510
--- /dev/null
+++ b/x-pack/test/examples/search_examples/search_example.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../functional/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const testSubjects = getService('testSubjects');
+ const PageObjects = getPageObjects(['common', 'timePicker']);
+ const retry = getService('retry');
+
+ describe.skip('Search session example', () => {
+ const appId = 'searchExamples';
+
+ before(async function () {
+ await PageObjects.common.navigateToApp(appId, { insertTimestamp: false });
+ });
+
+ it('should have an other bucket', async () => {
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Jan 1, 2014 @ 00:00:00.000',
+ 'Jan 1, 2016 @ 00:00:00.000'
+ );
+ await testSubjects.click('searchSourceWithOther');
+
+ await retry.waitFor('has other bucket', async () => {
+ await testSubjects.click('responseTab');
+ const codeBlock = await testSubjects.find('responseCodeBlock');
+ const visibleText = await codeBlock.getVisibleText();
+ return visibleText.indexOf('__other__') > -1;
+ });
+ });
+ });
+}