From 1a3e033c90eb9217168073abddcca2e11220009a Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Sun, 18 Apr 2021 12:50:02 +0300 Subject: [PATCH] [Partial Results] Move other bucket into Search Source (#96384) * Move inspector adapter integration into search source * docs and ts * Move other bucket to search source * test ts + delete unused tabilfy function * hierarchical param in aggconfig. ts improvements more inspector tests * fix jest * separate inspect more tests * jest * inspector * Error handling and more tests * put the fun in functional tests * code review * Add functional test for other bucket in search example app * test * test * ts * test * test * ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ins-data-public.aggconfigs.hierarchical.md | 11 + ...a-plugin-plugins-data-public.aggconfigs.md | 3 +- ...in-plugins-data-public.aggconfigs.todsl.md | 9 +- ...s-data-public.isearchoptions.inspector.md} | 8 +- ...ugin-plugins-data-public.isearchoptions.md | 2 +- ...-plugins-data-public.searchsource.fetch.md | 4 +- ...plugins-data-public.searchsource.fetch_.md | 4 +- ...ins-data-public.searchsourcefields.aggs.md | 2 +- ...-plugins-data-public.searchsourcefields.md | 2 +- ...s-data-server.isearchoptions.inspector.md} | 8 +- ...ugin-plugins-data-server.isearchoptions.md | 2 +- examples/search_examples/public/index.scss | 6 + .../search_examples/public/search/app.tsx | 274 ++++++---- .../common/search/aggs/agg_configs.test.ts | 8 +- .../data/common/search/aggs/agg_configs.ts | 8 +- .../data/common/search/aggs/agg_type.ts | 31 +- .../_terms_other_bucket_helper.test.ts | 6 +- .../buckets/_terms_other_bucket_helper.ts | 13 +- .../data/common/search/aggs/buckets/terms.ts | 26 +- .../esaggs/request_handler.test.ts | 43 +- .../expressions/esaggs/request_handler.ts | 62 +-- .../search_source/inspect/inspector_stats.ts | 2 +- .../search_source/search_source.test.ts | 482 +++++++++++++++--- .../search/search_source/search_source.ts | 179 ++++++- .../data/common/search/search_source/types.ts | 3 +- .../data/common/search/tabify/index.ts | 23 +- src/plugins/data/common/search/types.ts | 15 +- src/plugins/data/public/public.api.md | 17 +- .../public/search/expressions/esaggs.test.ts | 7 +- .../data/public/search/expressions/esaggs.ts | 7 +- .../server/search/expressions/esaggs.test.ts | 6 +- .../data/server/search/expressions/esaggs.ts | 8 +- src/plugins/data/server/server.api.md | 8 +- .../public/application/angular/discover.js | 22 +- .../embeddable/search_embeddable.ts | 22 +- .../classes/sources/es_source/es_source.ts | 13 +- x-pack/test/examples/search_examples/index.ts | 3 +- .../search_examples/search_example.ts | 38 ++ 38 files changed, 964 insertions(+), 423 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md => kibana-plugin-plugins-data-public.isearchoptions.inspector.md} (52%) rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md => kibana-plugin-plugins-data-server.isearchoptions.inspector.md} (52%) create mode 100644 x-pack/test/examples/search_examples/search_example.ts 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 000000000000..66d540c48c3b --- /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 22f8994747aa..02e9a63d95ba 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 055c4113ca3e..1327e976db0c 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 b4431b9467b7..9961292aaf21 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 cc0cb538be61..21fb7e3dfc7e 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 623d6366d4d1..e6ba1a51a867 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 d5641107a88a..4369cf7c087d 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 f6bab8e42485..12011f824299 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 d0f53936eb56..981d956a9e89 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 7440f5a9d26c..ab755334643a 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 413a59be3d42..cdb5664f96cd 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 e69de29bb2d1..b623fecf7864 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 8f31d242faf5..b2a4991d0717 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 3ce528e6ed89..28102544ae05 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 4d5d49754387..2932ef7325ae 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 33fdc45a605b..f0f3912bf64f 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 56e720d237c4..2aa0d346afe3 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 742615bc49d8..6230ae897b17 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 77c9c6e391c0..03cf14a577a5 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 c2566535916a..b30e5740fa3f 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 5620698a4753..173b2067cad6 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 24507a7e1305..e5a3acc23eee 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 3726e5d0c33e..7f8a4fceff05 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 e1e7a8292d67..1c1c32228703 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 a178b38693d9..99f3f67a5e25 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 168d4cf9d4c3..74fbc7ba4cfa 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 37de8dc49d3c..e3ec499a0020 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 35f13fc855e9..0dd06691d68b 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 d7a6446781c4..e75bd7be219d 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 45d24af3a6eb..1e3d56c71e42 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 124a171de637..15287e9d8cf5 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 61fd320d89b9..bb22a491b157 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 622356c4441a..3316e8102e50 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 35a89eb45f35..4099d5e8ef7e 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 237da72ae3a5..dbaf07fed18c 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 2915eaec8ac7..50043772af95 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 13eac7566525..65e214cda4cf 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 000000000000..19a9535ebb95 --- /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; + }); + }); + }); +}