[data.search] Simplify poll logic and improve types (#82545)

* [Search] Add request context and asScoped pattern

* Update docs

* Unify interface for getting search client

* Update examples/search_examples/server/my_strategy.ts

Co-authored-by: Anton Dosov <dosantappdev@gmail.com>

* Review feedback

* Fix checks

* Fix CI

* Fix security search

* Fix test

* Fix test for reals

* Fix types

* [data.search] Refactor search polling and improve types

* Fix & update tests & types

* eql totals

* doc

* Revert "eql totals"

This reverts commit 01e8a06847.

* lint

* response type

* shim inside strategies

* shim for security

* fix eql params

Co-authored-by: Anton Dosov <dosantappdev@gmail.com>
Co-authored-by: Liza K <liza.katz@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Lukas Olson 2020-11-24 15:38:12 -07:00 committed by GitHub
parent dc15aa8ea2
commit f80da6cc39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 843 additions and 856 deletions

View file

@ -9,7 +9,7 @@ Constructs a new instance of the `PainlessError` class
<b>Signature:</b>
```typescript
constructor(err: IEsError, request: IKibanaSearchRequest);
constructor(err: IEsError);
```
## Parameters
@ -17,5 +17,4 @@ constructor(err: IEsError, request: IKibanaSearchRequest);
| Parameter | Type | Description |
| --- | --- | --- |
| err | <code>IEsError</code> | |
| request | <code>IKibanaSearchRequest</code> | |

View file

@ -14,7 +14,7 @@ export declare class PainlessError extends EsError
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(err, request)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the <code>PainlessError</code> class |
| [(constructor)(err)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the <code>PainlessError</code> class |
## Properties

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
```
## Parameters
@ -15,7 +15,6 @@ protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal
| Parameter | Type | Description |
| --- | --- | --- |
| e | <code>any</code> | |
| request | <code>IKibanaSearchRequest</code> | |
| timeoutSignal | <code>AbortSignal</code> | |
| options | <code>ISearchOptions</code> | |

View file

@ -27,7 +27,7 @@ export declare class SearchInterceptor
| Method | Modifiers | Description |
| --- | --- | --- |
| [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | |
| [handleSearchError(e, request, timeoutSignal, options)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | |
| [handleSearchError(e, timeoutSignal, options)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | |
| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given <code>search</code> method. Overrides the <code>AbortSignal</code> with one that will abort either when <code>cancelPending</code> is called, when the request times out, or when the original <code>AbortSignal</code> is aborted. Updates <code>pendingCount$</code> when the request is started/finalized. |
| [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | |

View file

@ -7,11 +7,7 @@
<b>Signature:</b>
```typescript
export declare function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient): Promise<{
maxConcurrentShardRequests: number | undefined;
ignoreUnavailable: boolean;
trackTotalHits: boolean;
}>;
export declare function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient): Promise<Pick<Search, 'max_concurrent_shard_requests' | 'ignore_unavailable' | 'track_total_hits'>>;
```
## Parameters
@ -22,9 +18,5 @@ export declare function getDefaultSearchParams(uiSettingsClient: IUiSettingsClie
<b>Returns:</b>
`Promise<{
maxConcurrentShardRequests: number | undefined;
ignoreUnavailable: boolean;
trackTotalHits: boolean;
}>`
`Promise<Pick<Search, 'max_concurrent_shard_requests' | 'ignore_unavailable' | 'track_total_hits'>>`

View file

@ -7,11 +7,7 @@
<b>Signature:</b>
```typescript
export declare function getShardTimeout(config: SharedGlobalConfig): {
timeout: string;
} | {
timeout?: undefined;
};
export declare function getShardTimeout(config: SharedGlobalConfig): Pick<Search, 'timeout'>;
```
## Parameters
@ -22,9 +18,5 @@ export declare function getShardTimeout(config: SharedGlobalConfig): {
<b>Returns:</b>
`{
timeout: string;
} | {
timeout?: undefined;
}`
`Pick<Search, 'timeout'>`

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [IEsRawSearchResponse](./kibana-plugin-plugins-data-server.iesrawsearchresponse.md) &gt; [id](./kibana-plugin-plugins-data-server.iesrawsearchresponse.id.md)
## IEsRawSearchResponse.id property
<b>Signature:</b>
```typescript
id?: string;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [IEsRawSearchResponse](./kibana-plugin-plugins-data-server.iesrawsearchresponse.md) &gt; [is\_partial](./kibana-plugin-plugins-data-server.iesrawsearchresponse.is_partial.md)
## IEsRawSearchResponse.is\_partial property
<b>Signature:</b>
```typescript
is_partial?: boolean;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [IEsRawSearchResponse](./kibana-plugin-plugins-data-server.iesrawsearchresponse.md) &gt; [is\_running](./kibana-plugin-plugins-data-server.iesrawsearchresponse.is_running.md)
## IEsRawSearchResponse.is\_running property
<b>Signature:</b>
```typescript
is_running?: boolean;
```

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [IEsRawSearchResponse](./kibana-plugin-plugins-data-server.iesrawsearchresponse.md)
## IEsRawSearchResponse interface
<b>Signature:</b>
```typescript
export interface IEsRawSearchResponse<Source = any> extends SearchResponse<Source>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-plugins-data-server.iesrawsearchresponse.id.md) | <code>string</code> | |
| [is\_partial](./kibana-plugin-plugins-data-server.iesrawsearchresponse.is_partial.md) | <code>boolean</code> | |
| [is\_running](./kibana-plugin-plugins-data-server.iesrawsearchresponse.is_running.md) | <code>boolean</code> | |

View file

@ -34,6 +34,7 @@
| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | |
| [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally |
| [searchUsageObserver(logger, usage)](./kibana-plugin-plugins-data-server.searchusageobserver.md) | Rxjs observer for easily doing <code>tap(searchUsageObserver(logger, usage))</code> in an rxjs chain. |
| [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | |
| [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | |
@ -45,7 +46,6 @@
| [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | |
| [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) | |
| [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | |
| [IEsRawSearchResponse](./kibana-plugin-plugins-data-server.iesrawsearchresponse.md) | |
| [IEsSearchRequest](./kibana-plugin-plugins-data-server.iessearchrequest.md) | |
| [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | |
| [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | |

View file

@ -8,24 +8,6 @@
```typescript
search: {
esSearch: {
utils: {
doSearch: <SearchResponse = any>(searchMethod: () => Promise<SearchResponse>, abortSignal?: AbortSignal | undefined) => import("rxjs").Observable<SearchResponse>;
shimAbortSignal: <T extends import("../common").TransportRequestPromise<unknown>>(promise: T, signal: AbortSignal | undefined) => T;
trackSearchStatus: <KibanaResponse extends import("../common").IKibanaSearchResponse<any> = import("./search").IEsSearchResponse<import("src/core/server").SearchResponse<unknown>>>(logger: import("src/core/server").Logger, usage?: import("./search").SearchUsage | undefined) => import("rxjs").UnaryFunction<import("rxjs").Observable<KibanaResponse>, import("rxjs").Observable<KibanaResponse>>;
includeTotalLoaded: () => import("rxjs").OperatorFunction<import("../common").IKibanaSearchResponse<import("elasticsearch").SearchResponse<unknown>>, {
total: number;
loaded: number;
id?: string | undefined;
isRunning?: boolean | undefined;
isPartial?: boolean | undefined;
rawResponse: import("elasticsearch").SearchResponse<unknown>;
}>;
toKibanaSearchResponse: <SearchResponse_1 extends import("../common").IEsRawSearchResponse<any> = import("../common").IEsRawSearchResponse<any>, KibanaResponse_1 extends import("../common").IKibanaSearchResponse<any> = import("../common").IKibanaSearchResponse<SearchResponse_1>>() => import("rxjs").OperatorFunction<import("@elastic/elasticsearch").ApiResponse<SearchResponse_1, import("@elastic/elasticsearch/lib/Transport").Context>, KibanaResponse_1>;
getTotalLoaded: typeof getTotalLoaded;
toSnakeCase: typeof toSnakeCase;
};
};
aggs: {
CidrMask: typeof CidrMask;
dateHistogramInterval: typeof dateHistogramInterval;

View file

@ -0,0 +1,31 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [searchUsageObserver](./kibana-plugin-plugins-data-server.searchusageobserver.md)
## searchUsageObserver() function
Rxjs observer for easily doing `tap(searchUsageObserver(logger, usage))` in an rxjs chain.
<b>Signature:</b>
```typescript
export declare function searchUsageObserver(logger: Logger, usage?: SearchUsage): {
next(response: IEsSearchResponse): void;
error(): void;
};
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| logger | <code>Logger</code> | |
| usage | <code>SearchUsage</code> | |
<b>Returns:</b>
`{
next(response: IEsSearchResponse): void;
error(): void;
}`

View file

@ -1,55 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { from } from 'rxjs';
import { map } from 'rxjs/operators';
import type { SearchResponse } from 'elasticsearch';
import type { ApiResponse } from '@elastic/elasticsearch';
import { shimAbortSignal } from './shim_abort_signal';
import { getTotalLoaded } from './get_total_loaded';
import type { IEsRawSearchResponse } from './types';
import type { IKibanaSearchResponse } from '../types';
export const doSearch = <SearchResponse = any>(
searchMethod: () => Promise<SearchResponse>,
abortSignal?: AbortSignal
) => from(shimAbortSignal(searchMethod(), abortSignal));
export const toKibanaSearchResponse = <
SearchResponse extends IEsRawSearchResponse = IEsRawSearchResponse,
KibanaResponse extends IKibanaSearchResponse = IKibanaSearchResponse<SearchResponse>
>() =>
map<ApiResponse<SearchResponse>, KibanaResponse>(
(response) =>
({
id: response.body.id,
isPartial: response.body.is_partial || false,
isRunning: response.body.is_running || false,
rawResponse: response.body,
} as KibanaResponse)
);
export const includeTotalLoaded = () =>
map((response: IKibanaSearchResponse<SearchResponse<unknown>>) => ({
...response,
...getTotalLoaded(response.rawResponse._shards),
}));

View file

@ -1,36 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getTotalLoaded } from './get_total_loaded';
describe('getTotalLoaded', () => {
it('returns the total/loaded, not including skipped', () => {
const result = getTotalLoaded({
successful: 10,
failed: 5,
skipped: 5,
total: 100,
});
expect(result).toEqual({
total: 100,
loaded: 15,
});
});
});

View file

@ -18,8 +18,3 @@
*/
export * from './types';
export * from './utils';
export * from './es_search_rxjs_utils';
export * from './shim_abort_signal';
export * from './to_snake_case';
export * from './get_total_loaded';

View file

@ -1,64 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { shimAbortSignal } from './shim_abort_signal';
const createSuccessTransportRequestPromise = (
body: any,
{ statusCode = 200 }: { statusCode?: number } = {}
) => {
const promise = Promise.resolve({ body, statusCode }) as any;
promise.abort = jest.fn();
return promise;
};
describe('shimAbortSignal', () => {
test('aborts the promise if the signal is aborted', () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
shimAbortSignal(promise, controller.signal);
controller.abort();
expect(promise.abort).toHaveBeenCalled();
});
test('returns the original promise', async () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
const response = await shimAbortSignal(promise, controller.signal);
expect(response).toEqual(expect.objectContaining({ body: { success: true } }));
});
test('allows the promise to be aborted manually', () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
const enhancedPromise = shimAbortSignal(promise, controller.signal);
enhancedPromise.abort();
expect(promise.abort).toHaveBeenCalled();
});
});

View file

@ -1,48 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @internal
* TransportRequestPromise extends base Promise with an "abort" method
*/
export interface TransportRequestPromise<T> extends Promise<T> {
abort?: () => void;
}
/**
*
* @internal
* NOTE: Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297
* is resolved
*
* @param promise a TransportRequestPromise
* @param signal optional AbortSignal
*
* @returns a TransportRequestPromise that will be aborted if the signal is aborted
*/
export const shimAbortSignal = <T extends TransportRequestPromise<unknown>>(
promise: T,
signal: AbortSignal | undefined
): T => {
if (signal) {
signal.addEventListener('abort', () => promise.abort && promise.abort());
}
return promise;
};

View file

@ -1,24 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { mapKeys, snakeCase } from 'lodash';
export function toSnakeCase(obj: Record<string, any>): Record<string, any> {
return mapKeys(obj, (value, key) => snakeCase(key));
}

View file

@ -30,10 +30,4 @@ export interface IEsSearchRequest extends IKibanaSearchRequest<ISearchRequestPar
indexType?: string;
}
export interface IEsRawSearchResponse<Source = any> extends SearchResponse<Source> {
id?: string;
is_partial?: boolean;
is_running?: boolean;
}
export type IEsSearchResponse<Source = any> = IKibanaSearchResponse<SearchResponse<Source>>;

View file

@ -24,3 +24,4 @@ export * from './search_source';
export * from './tabify';
export * from './types';
export * from './session';
export * from './utils';

View file

@ -0,0 +1,106 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { isErrorResponse, isCompleteResponse, isPartialResponse } from './utils';
describe('utils', () => {
describe('isErrorResponse', () => {
it('returns `true` if the response is undefined', () => {
const isError = isErrorResponse();
expect(isError).toBe(true);
});
it('returns `true` if the response is not running and partial', () => {
const isError = isErrorResponse({
isPartial: true,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(true);
});
it('returns `false` if the response is running and partial', () => {
const isError = isErrorResponse({
isPartial: true,
isRunning: true,
rawResponse: {},
});
expect(isError).toBe(false);
});
it('returns `false` if the response is complete', () => {
const isError = isErrorResponse({
isPartial: false,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(false);
});
});
describe('isCompleteResponse', () => {
it('returns `false` if the response is undefined', () => {
const isError = isCompleteResponse();
expect(isError).toBe(false);
});
it('returns `false` if the response is running and partial', () => {
const isError = isCompleteResponse({
isPartial: true,
isRunning: true,
rawResponse: {},
});
expect(isError).toBe(false);
});
it('returns `true` if the response is complete', () => {
const isError = isCompleteResponse({
isPartial: false,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(true);
});
});
describe('isPartialResponse', () => {
it('returns `false` if the response is undefined', () => {
const isError = isPartialResponse();
expect(isError).toBe(false);
});
it('returns `true` if the response is running and partial', () => {
const isError = isPartialResponse({
isPartial: true,
isRunning: true,
rawResponse: {},
});
expect(isError).toBe(true);
});
it('returns `false` if the response is complete', () => {
const isError = isPartialResponse({
isPartial: false,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(false);
});
});
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
import type { IKibanaSearchResponse } from '../types';
import type { IKibanaSearchResponse } from './types';
/**
* @returns true if response had an error while executing in ES

View file

@ -1686,7 +1686,7 @@ export interface OptionedValueProp {
// @public (undocumented)
export class PainlessError extends EsError {
// Warning: (ae-forgotten-export) The symbol "IEsError" needs to be exported by the entry point index.d.ts
constructor(err: IEsError, request: IKibanaSearchRequest);
constructor(err: IEsError);
// (undocumented)
getErrorMessage(application: ApplicationStart): JSX.Element;
// (undocumented)
@ -2090,7 +2090,7 @@ export class SearchInterceptor {
// (undocumented)
protected getTimeoutMode(): TimeoutErrorMode;
// (undocumented)
protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error;
// @internal
protected pendingCount$: BehaviorSubject<number>;
// @internal (undocumented)

View file

@ -25,11 +25,10 @@ import { ApplicationStart } from 'kibana/public';
import { IEsError, isEsError } from './types';
import { EsError } from './es_error';
import { getRootCause } from './utils';
import { IKibanaSearchRequest } from '..';
export class PainlessError extends EsError {
painlessStack?: string;
constructor(err: IEsError, request: IKibanaSearchRequest) {
constructor(err: IEsError) {
super(err);
}

View file

@ -65,20 +65,17 @@ describe('SearchInterceptor', () => {
test('Renders a PainlessError', async () => {
searchInterceptor.showError(
new PainlessError(
{
body: {
attributes: {
error: {
failed_shards: {
reason: 'bananas',
},
new PainlessError({
body: {
attributes: {
error: {
failed_shards: {
reason: 'bananas',
},
},
} as any,
},
{} as any
)
},
} as any,
})
);
expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1);
expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled();

View file

@ -93,12 +93,7 @@ export class SearchInterceptor {
* @returns `Error` a search service specific error or the original error, if a specific error can't be recognized.
* @internal
*/
protected handleSearchError(
e: any,
request: IKibanaSearchRequest,
timeoutSignal: AbortSignal,
options?: ISearchOptions
): Error {
protected handleSearchError(e: any, timeoutSignal: AbortSignal, options?: ISearchOptions): Error {
if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') {
// Handle a client or a server side timeout
const err = new SearchTimeoutError(e, this.getTimeoutMode());
@ -112,7 +107,7 @@ export class SearchInterceptor {
return e;
} else if (isEsError(e)) {
if (isPainlessError(e)) {
return new PainlessError(e, request);
return new PainlessError(e);
} else {
return new EsError(e);
}
@ -244,7 +239,7 @@ export class SearchInterceptor {
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe(
catchError((e: Error) => {
return throwError(this.handleSearchError(e, request, timeoutSignal, options));
return throwError(this.handleSearchError(e, timeoutSignal, options));
}),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);

View file

@ -156,7 +156,6 @@ export {
IndexPatternAttributes,
UI_SETTINGS,
IndexPattern,
IEsRawSearchResponse,
} from '../common';
/**
@ -189,13 +188,6 @@ import {
// tabify
tabifyAggResponse,
tabifyGetColumns,
// search
toSnakeCase,
shimAbortSignal,
doSearch,
includeTotalLoaded,
toKibanaSearchResponse,
getTotalLoaded,
calcAutoIntervalLessThan,
} from '../common';
@ -243,27 +235,17 @@ export {
SearchStrategyDependencies,
getDefaultSearchParams,
getShardTimeout,
getTotalLoaded,
toKibanaSearchResponse,
shimHitsTotal,
usageProvider,
searchUsageObserver,
shimAbortSignal,
SearchUsage,
} from './search';
import { trackSearchStatus } from './search';
// Search namespace
export const search = {
esSearch: {
utils: {
doSearch,
shimAbortSignal,
trackSearchStatus,
includeTotalLoaded,
toKibanaSearchResponse,
// utils:
getTotalLoaded,
toSnakeCase,
},
},
aggs: {
CidrMask,
dateHistogramInterval,

View file

@ -17,4 +17,5 @@
* under the License.
*/
export { usageProvider, SearchUsage } from './usage';
export type { SearchUsage } from './usage';
export { usageProvider, searchUsageObserver } from './usage';

View file

@ -17,8 +17,9 @@
* under the License.
*/
import { CoreSetup } from 'kibana/server';
import { Usage } from './register';
import type { CoreSetup, Logger } from 'kibana/server';
import type { IEsSearchResponse } from '../../../common';
import type { Usage } from './register';
const SAVED_OBJECT_ID = 'search-telemetry';
@ -74,3 +75,19 @@ export function usageProvider(core: CoreSetup): SearchUsage {
trackSuccess: getTracker('successCount'),
};
}
/**
* Rxjs observer for easily doing `tap(searchUsageObserver(logger, usage))` in an rxjs chain.
*/
export function searchUsageObserver(logger: Logger, usage?: SearchUsage) {
return {
next(response: IEsSearchResponse) {
logger.debug(`trackSearchStatus:next ${response.rawResponse.took}`);
usage?.trackSuccess(response.rawResponse.took);
},
error() {
logger.debug(`trackSearchStatus:error`);
usage?.trackError();
},
};
}

View file

@ -1,53 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { pipe } from 'rxjs';
import { tap } from 'rxjs/operators';
import type { Logger, SearchResponse } from 'kibana/server';
import type { SearchUsage } from '../collectors';
import type { IEsSearchResponse, IKibanaSearchResponse } from '../../../common/search';
/**
* trackSearchStatus is a custom rxjs operator that can be used to track the progress of a search.
* @param Logger
* @param SearchUsage
*/
export const trackSearchStatus = <
KibanaResponse extends IKibanaSearchResponse = IEsSearchResponse<SearchResponse<unknown>>
>(
logger: Logger,
usage?: SearchUsage
) => {
return pipe(
tap(
(response: KibanaResponse) => {
const trackSuccessData = response.rawResponse.took;
if (trackSuccessData !== undefined) {
logger.debug(`trackSearchStatus:next ${trackSuccessData}`);
usage?.trackSuccess(trackSuccessData);
}
},
(err: any) => {
logger.debug(`trackSearchStatus:error ${err}`);
usage?.trackError();
}
)
);
};

View file

@ -16,20 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import type { Logger } from 'kibana/server';
import type { ApiResponse } from '@elastic/elasticsearch';
import type { SharedGlobalConfig } from 'kibana/server';
import { doSearch, includeTotalLoaded, toKibanaSearchResponse, toSnakeCase } from '../../../common';
import { trackSearchStatus } from './es_search_rxjs_utils';
import { getDefaultSearchParams, getShardTimeout } from '../es_search';
import { from, Observable } from 'rxjs';
import { first, tap } from 'rxjs/operators';
import type { SearchResponse } from 'elasticsearch';
import type { Logger, SharedGlobalConfig } from 'kibana/server';
import type { ISearchStrategy } from '../types';
import type { SearchUsage } from '../collectors/usage';
import type { IEsRawSearchResponse } from '../../../common';
import type { SearchUsage } from '../collectors';
import { getDefaultSearchParams, getShardTimeout, shimAbortSignal } from './request_utils';
import { toKibanaSearchResponse } from './response_utils';
import { searchUsageObserver } from '../collectors/usage';
export const esSearchStrategyProvider = (
config$: Observable<SharedGlobalConfig>,
@ -43,19 +38,18 @@ export const esSearchStrategyProvider = (
throw new Error(`Unsupported index pattern type ${request.indexType}`);
}
return doSearch<ApiResponse<IEsRawSearchResponse>>(async () => {
const search = async () => {
const config = await config$.pipe(first()).toPromise();
const params = toSnakeCase({
const params = {
...(await getDefaultSearchParams(uiSettingsClient)),
...getShardTimeout(config),
...request.params,
});
};
const promise = esClient.asCurrentUser.search<SearchResponse<unknown>>(params);
const { body } = await shimAbortSignal(promise, abortSignal);
return toKibanaSearchResponse(body);
};
return esClient.asCurrentUser.search(params);
}, abortSignal).pipe(
toKibanaSearchResponse(),
trackSearchStatus(logger, usage),
includeTotalLoaded()
);
return from(search()).pipe(tap(searchUsageObserver(logger, usage)));
},
});

View file

@ -1,41 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { UI_SETTINGS } from '../../../common/constants';
import type { SharedGlobalConfig, IUiSettingsClient } from '../../../../../core/server';
export function getShardTimeout(config: SharedGlobalConfig) {
const timeout = config.elasticsearch.shardTimeout.asMilliseconds();
return timeout
? {
timeout: `${timeout}ms`,
}
: {};
}
export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient) {
const maxConcurrentShardRequests = await uiSettingsClient.get<number>(
UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS
);
return {
maxConcurrentShardRequests:
maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined,
ignoreUnavailable: true, // Don't fail if the index/indices don't exist
trackTotalHits: true,
};
}

View file

@ -18,7 +18,6 @@
*/
export { esSearchStrategyProvider } from './es_search_strategy';
export * from './get_default_search_params';
export * from './es_search_rxjs_utils';
export * from './request_utils';
export * from './response_utils';
export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common';

View file

@ -0,0 +1,148 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getShardTimeout, getDefaultSearchParams, shimAbortSignal } from './request_utils';
import { IUiSettingsClient, SharedGlobalConfig } from 'kibana/server';
const createSuccessTransportRequestPromise = (
body: any,
{ statusCode = 200 }: { statusCode?: number } = {}
) => {
const promise = Promise.resolve({ body, statusCode }) as any;
promise.abort = jest.fn();
return promise;
};
describe('request utils', () => {
describe('getShardTimeout', () => {
test('returns an empty object if the config does not contain a value', () => {
const result = getShardTimeout(({
elasticsearch: {
shardTimeout: {
asMilliseconds: jest.fn(),
},
},
} as unknown) as SharedGlobalConfig);
expect(result).toEqual({});
});
test('returns an empty object if the config contains 0', () => {
const result = getShardTimeout(({
elasticsearch: {
shardTimeout: {
asMilliseconds: jest.fn().mockReturnValue(0),
},
},
} as unknown) as SharedGlobalConfig);
expect(result).toEqual({});
});
test('returns a duration if the config >= 0', () => {
const result = getShardTimeout(({
elasticsearch: {
shardTimeout: {
asMilliseconds: jest.fn().mockReturnValue(10),
},
},
} as unknown) as SharedGlobalConfig);
expect(result).toEqual({ timeout: '10ms' });
});
});
describe('getDefaultSearchParams', () => {
describe('max_concurrent_shard_requests', () => {
test('returns value if > 0', async () => {
const result = await getDefaultSearchParams(({
get: jest.fn().mockResolvedValue(1),
} as unknown) as IUiSettingsClient);
expect(result).toHaveProperty('max_concurrent_shard_requests', 1);
});
test('returns undefined if === 0', async () => {
const result = await getDefaultSearchParams(({
get: jest.fn().mockResolvedValue(0),
} as unknown) as IUiSettingsClient);
expect(result.max_concurrent_shard_requests).toBe(undefined);
});
test('returns undefined if undefined', async () => {
const result = await getDefaultSearchParams(({
get: jest.fn(),
} as unknown) as IUiSettingsClient);
expect(result.max_concurrent_shard_requests).toBe(undefined);
});
});
describe('other defaults', () => {
test('returns ignore_unavailable and track_total_hits', async () => {
const result = await getDefaultSearchParams(({
get: jest.fn(),
} as unknown) as IUiSettingsClient);
expect(result).toHaveProperty('ignore_unavailable', true);
expect(result).toHaveProperty('track_total_hits', true);
});
});
});
describe('shimAbortSignal', () => {
test('aborts the promise if the signal is already aborted', async () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
controller.abort();
shimAbortSignal(promise, controller.signal);
expect(promise.abort).toHaveBeenCalled();
});
test('aborts the promise if the signal is aborted', () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
shimAbortSignal(promise, controller.signal);
controller.abort();
expect(promise.abort).toHaveBeenCalled();
});
test('returns the original promise', async () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
const response = await shimAbortSignal(promise, controller.signal);
expect(response).toEqual(expect.objectContaining({ body: { success: true } }));
});
test('allows the promise to be aborted manually', () => {
const promise = createSuccessTransportRequestPromise({
success: true,
});
const controller = new AbortController();
const enhancedPromise = shimAbortSignal(promise, controller.signal);
enhancedPromise.abort();
expect(promise.abort).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import type { Search } from '@elastic/elasticsearch/api/requestParams';
import type { IUiSettingsClient, SharedGlobalConfig } from 'kibana/server';
import { UI_SETTINGS } from '../../../common';
export function getShardTimeout(config: SharedGlobalConfig): Pick<Search, 'timeout'> {
const timeout = config.elasticsearch.shardTimeout.asMilliseconds();
return timeout ? { timeout: `${timeout}ms` } : {};
}
export async function getDefaultSearchParams(
uiSettingsClient: IUiSettingsClient
): Promise<
Pick<Search, 'max_concurrent_shard_requests' | 'ignore_unavailable' | 'track_total_hits'>
> {
const maxConcurrentShardRequests = await uiSettingsClient.get<number>(
UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS
);
return {
max_concurrent_shard_requests:
maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined,
ignore_unavailable: true, // Don't fail if the index/indices don't exist
track_total_hits: true,
};
}
/**
* Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 is resolved.
* Shims the `AbortSignal` behavior so that, if the given `signal` aborts, the `abort` method on the
* `TransportRequestPromise` is called, actually performing the cancellation.
* @internal
*/
export const shimAbortSignal = <T>(promise: TransportRequestPromise<T>, signal?: AbortSignal) => {
if (!signal) return promise;
const abortHandler = () => {
promise.abort();
cleanup();
};
const cleanup = () => signal.removeEventListener('abort', abortHandler);
if (signal.aborted) {
promise.abort();
} else {
signal.addEventListener('abort', abortHandler);
promise.then(cleanup, cleanup);
}
return promise;
};

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getTotalLoaded, toKibanaSearchResponse } from './response_utils';
import { SearchResponse } from 'elasticsearch';
describe('response utils', () => {
describe('getTotalLoaded', () => {
it('returns the total/loaded, not including skipped', () => {
const result = getTotalLoaded(({
_shards: {
successful: 10,
failed: 5,
skipped: 5,
total: 100,
},
} as unknown) as SearchResponse<unknown>);
expect(result).toEqual({
total: 100,
loaded: 15,
});
});
});
describe('toKibanaSearchResponse', () => {
it('returns rawResponse, isPartial, isRunning, total, and loaded', () => {
const result = toKibanaSearchResponse(({
_shards: {
successful: 10,
failed: 5,
skipped: 5,
total: 100,
},
} as unknown) as SearchResponse<unknown>);
expect(result).toEqual({
rawResponse: {
_shards: {
successful: 10,
failed: 5,
skipped: 5,
total: 100,
},
},
isRunning: false,
isPartial: false,
total: 100,
loaded: 15,
});
});
});
});

View file

@ -17,14 +17,28 @@
* under the License.
*/
import type { ShardsResponse } from 'elasticsearch';
import { SearchResponse } from 'elasticsearch';
/**
* Get the `total`/`loaded` for this response (see `IKibanaSearchResponse`). Note that `skipped` is
* not included as it is already included in `successful`.
* @internal
*/
export function getTotalLoaded({ total, failed, successful }: ShardsResponse) {
export function getTotalLoaded(response: SearchResponse<unknown>) {
const { total, failed, successful } = response._shards;
const loaded = failed + successful;
return { total, loaded };
}
/**
* Get the Kibana representation of this response (see `IKibanaSearchResponse`).
* @internal
*/
export function toKibanaSearchResponse(rawResponse: SearchResponse<unknown>) {
return {
rawResponse,
isPartial: false,
isRunning: false,
...getTotalLoaded(rawResponse),
};
}

View file

@ -19,6 +19,6 @@
export * from './types';
export * from './es_search';
export { usageProvider, SearchUsage } from './collectors';
export { usageProvider, SearchUsage, searchUsageObserver } from './collectors';
export * from './aggs';
export { shimHitsTotal } from './routes';

View file

@ -24,9 +24,8 @@ import { SearchResponse } from 'elasticsearch';
import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server';
import type { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source';
import { toSnakeCase, shimAbortSignal } from '../../../common/search/es_search';
import { shimHitsTotal } from './shim_hits_total';
import { getShardTimeout, getDefaultSearchParams } from '..';
import { getShardTimeout, getDefaultSearchParams, shimAbortSignal } from '..';
/** @internal */
export function convertRequestBody(
@ -71,7 +70,7 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) {
const timeout = getShardTimeout(config);
// trackTotalHits is not supported by msearch
const { trackTotalHits, ...defaultParams } = await getDefaultSearchParams(uiSettings);
const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(uiSettings);
const body = convertRequestBody(params.body, timeout);
@ -81,7 +80,7 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) {
body,
},
{
querystring: toSnakeCase(defaultParams),
querystring: defaultParams,
}
),
params.signal

View file

@ -34,6 +34,7 @@ import { IScopedClusterClient } from 'src/core/server';
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';
import { ISearchSource } from 'src/plugins/data/public';
import { IUiSettingsClient } from 'src/core/server';
import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server';
import { KibanaRequest } from 'src/core/server';
import { LegacyAPICaller } from 'src/core/server';
import { Logger } from 'src/core/server';
@ -58,8 +59,9 @@ import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'kiba
import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchResponse } from 'elasticsearch';
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
import { ShardsResponse } from 'elasticsearch';
import { SharedGlobalConfig as SharedGlobalConfig_2 } from 'kibana/server';
import { ToastInputFields } from 'src/core/public/notifications';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiStatsMetricType } from '@kbn/analytics';
@ -410,25 +412,15 @@ export function getCapabilitiesForRollupIndices(indices: {
[key: string]: any;
};
// Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient_2): Promise<{
maxConcurrentShardRequests: number | undefined;
ignoreUnavailable: boolean;
trackTotalHits: boolean;
}>;
export function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient_3): Promise<Pick<Search, 'max_concurrent_shard_requests' | 'ignore_unavailable' | 'track_total_hits'>>;
// Warning: (ae-forgotten-export) The symbol "SharedGlobalConfig" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getShardTimeout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function getShardTimeout(config: SharedGlobalConfig): {
timeout: string;
} | {
timeout?: undefined;
};
export function getShardTimeout(config: SharedGlobalConfig_2): Pick<Search, 'timeout'>;
// Warning: (ae-forgotten-export) The symbol "IIndexPattern" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -439,6 +431,12 @@ export function getTime(indexPattern: IIndexPattern | undefined, timeRange: Time
fieldName?: string;
}): import("../..").RangeFilter | undefined;
// @internal
export function getTotalLoaded(response: SearchResponse<unknown>): {
total: number;
loaded: number;
};
// Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@ -455,18 +453,6 @@ export type IAggConfigs = AggConfigs;
// @public (undocumented)
export type IAggType = AggType;
// Warning: (ae-missing-release-tag) "IEsRawSearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IEsRawSearchResponse<Source = any> extends SearchResponse<Source> {
// (undocumented)
id?: string;
// (undocumented)
is_partial?: boolean;
// (undocumented)
is_running?: boolean;
}
// Warning: (ae-forgotten-export) The symbol "IKibanaSearchRequest" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ISearchRequestParams" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -1040,24 +1026,6 @@ export interface RefreshInterval {
//
// @public (undocumented)
export const search: {
esSearch: {
utils: {
doSearch: <SearchResponse = any>(searchMethod: () => Promise<SearchResponse>, abortSignal?: AbortSignal | undefined) => import("rxjs").Observable<SearchResponse>;
shimAbortSignal: <T extends import("../common").TransportRequestPromise<unknown>>(promise: T, signal: AbortSignal | undefined) => T;
trackSearchStatus: <KibanaResponse extends import("../common").IKibanaSearchResponse<any> = import("./search").IEsSearchResponse<import("src/core/server").SearchResponse<unknown>>>(logger: import("src/core/server").Logger, usage?: import("./search").SearchUsage | undefined) => import("rxjs").UnaryFunction<import("rxjs").Observable<KibanaResponse>, import("rxjs").Observable<KibanaResponse>>;
includeTotalLoaded: () => import("rxjs").OperatorFunction<import("../common").IKibanaSearchResponse<import("elasticsearch").SearchResponse<unknown>>, {
total: number;
loaded: number;
id?: string | undefined;
isRunning?: boolean | undefined;
isPartial?: boolean | undefined;
rawResponse: import("elasticsearch").SearchResponse<unknown>;
}>;
toKibanaSearchResponse: <SearchResponse_1 extends import("../common").IEsRawSearchResponse<any> = import("../common").IEsRawSearchResponse<any>, KibanaResponse_1 extends import("../common").IKibanaSearchResponse<any> = import("../common").IKibanaSearchResponse<SearchResponse_1>>() => import("rxjs").OperatorFunction<import("@elastic/elasticsearch").ApiResponse<SearchResponse_1, import("@elastic/elasticsearch/lib/Transport").Context>, KibanaResponse_1>;
getTotalLoaded: typeof getTotalLoaded;
toSnakeCase: typeof toSnakeCase;
};
};
aggs: {
CidrMask: typeof CidrMask;
dateHistogramInterval: typeof dateHistogramInterval;
@ -1114,6 +1082,17 @@ export interface SearchUsage {
trackSuccess(duration: number): Promise<void>;
}
// Warning: (ae-missing-release-tag) "searchUsageObserver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage): {
next(response: IEsSearchResponse): void;
error(): void;
};
// @internal
export const shimAbortSignal: <T>(promise: TransportRequestPromise<T>, signal?: AbortSignal | undefined) => TransportRequestPromise<T>;
// @internal
export function shimHitsTotal(response: SearchResponse<any>): {
hits: {
@ -1176,6 +1155,15 @@ export type TimeRange = {
mode?: 'absolute' | 'relative';
};
// @internal
export function toKibanaSearchResponse(rawResponse: SearchResponse<unknown>): {
total: number;
loaded: number;
rawResponse: SearchResponse<unknown>;
isPartial: boolean;
isRunning: boolean;
};
// Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1247,22 +1235,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:254:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:254:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:254:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:254:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:269:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:270:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:284:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:285:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:286:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:290:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:291:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:295:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:298:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:299:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:250:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:261:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts

View file

@ -10,9 +10,6 @@ export {
EqlRequestParams,
EqlSearchStrategyRequest,
EqlSearchStrategyResponse,
IAsyncSearchRequest,
IEnhancedEsSearchRequest,
IAsyncSearchOptions,
doPartialSearch,
throwOnEsError,
pollSearch,
} from './search';

View file

@ -1,51 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { of, merge, timer, throwError } from 'rxjs';
import { map, takeWhile, switchMap, expand, mergeMap, tap } from 'rxjs/operators';
import { ApiResponse } from '@elastic/elasticsearch';
import {
doSearch,
IKibanaSearchResponse,
isErrorResponse,
} from '../../../../../../src/plugins/data/common';
import { AbortError } from '../../../../../../src/plugins/kibana_utils/common';
import type { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common';
import type { IAsyncSearchOptions } from '../../../common/search/types';
const DEFAULT_POLLING_INTERVAL = 1000;
export const doPartialSearch = <SearchResponse = any>(
searchMethod: () => Promise<SearchResponse>,
partialSearchMethod: (id: IKibanaSearchRequest['id']) => Promise<SearchResponse>,
isCompleteResponse: (response: SearchResponse) => boolean,
getId: (response: SearchResponse) => IKibanaSearchRequest['id'],
requestId: IKibanaSearchRequest['id'],
{ abortSignal, pollInterval = DEFAULT_POLLING_INTERVAL }: IAsyncSearchOptions
) =>
doSearch<SearchResponse>(
requestId ? () => partialSearchMethod(requestId) : searchMethod,
abortSignal
).pipe(
tap((response) => (requestId = getId(response))),
expand(() => timer(pollInterval).pipe(switchMap(() => partialSearchMethod(requestId)))),
takeWhile((response) => !isCompleteResponse(response), true)
);
export const normalizeEqlResponse = <SearchResponse extends ApiResponse = ApiResponse>() =>
map<SearchResponse, SearchResponse>((eqlResponse) => ({
...eqlResponse,
body: {
...eqlResponse.body,
...eqlResponse,
},
}));
export const throwOnEsError = () =>
mergeMap((r: IKibanaSearchResponse) =>
isErrorResponse(r) ? merge(of(r), throwError(new AbortError())) : of(r)
);

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './es_search_rxjs_utils';

View file

@ -5,4 +5,4 @@
*/
export * from './types';
export * from './es_search';
export * from './poll_search';

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { from, NEVER, Observable, timer } from 'rxjs';
import { expand, finalize, switchMap, takeUntil, takeWhile, tap } from 'rxjs/operators';
import type { IKibanaSearchResponse } from '../../../../../src/plugins/data/common';
import { isErrorResponse, isPartialResponse } from '../../../../../src/plugins/data/common';
import { AbortError, abortSignalToPromise } from '../../../../../src/plugins/kibana_utils/common';
import type { IAsyncSearchOptions } from './types';
export const pollSearch = <Response extends IKibanaSearchResponse>(
search: () => Promise<Response>,
{ pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
): Observable<Response> => {
const aborted = options?.abortSignal
? abortSignalToPromise(options?.abortSignal)
: { promise: NEVER, cleanup: () => {} };
return from(search()).pipe(
expand(() => timer(pollInterval).pipe(switchMap(search))),
tap((response) => {
if (isErrorResponse(response)) throw new AbortError();
}),
takeWhile<Response>(isPartialResponse, true),
takeUntil<Response>(from(aborted.promise)),
finalize(aborted.cleanup)
);
};

View file

@ -9,27 +9,12 @@ import { ApiResponse, TransportRequestOptions } from '@elastic/elasticsearch/lib
import {
ISearchOptions,
IEsSearchRequest,
IKibanaSearchRequest,
IKibanaSearchResponse,
} from '../../../../../src/plugins/data/common';
export const ENHANCED_ES_SEARCH_STRATEGY = 'ese';
export interface IAsyncSearchRequest extends IEsSearchRequest {
/**
* The ID received from the response from the initial request
*/
id?: string;
}
export interface IEnhancedEsSearchRequest extends IEsSearchRequest {
/**
* Used to determine whether to use the _rollups_search or a regular search endpoint.
*/
isRollup?: boolean;
}
export const EQL_SEARCH_STRATEGY = 'eql';
export type EqlRequestParams = EqlSearch<Record<string, unknown>>;

View file

@ -117,7 +117,7 @@ describe('EnhancedSearchInterceptor', () => {
{
time: 10,
value: {
isPartial: false,
isPartial: true,
isRunning: true,
id: 1,
rawResponse: {
@ -175,8 +175,6 @@ describe('EnhancedSearchInterceptor', () => {
await timeTravel(10);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).toStrictEqual(responses[0].value);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
});
@ -212,7 +210,7 @@ describe('EnhancedSearchInterceptor', () => {
{
time: 10,
value: {
isPartial: false,
isPartial: true,
isRunning: true,
id: 1,
},
@ -280,7 +278,7 @@ describe('EnhancedSearchInterceptor', () => {
{
time: 10,
value: {
isPartial: false,
isPartial: true,
isRunning: true,
id: 1,
},
@ -320,7 +318,7 @@ describe('EnhancedSearchInterceptor', () => {
{
time: 10,
value: {
isPartial: false,
isPartial: true,
isRunning: true,
id: 1,
},

View file

@ -4,24 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { throwError, from, Subscription } from 'rxjs';
import { tap, takeUntil, finalize, catchError } from 'rxjs/operators';
import { throwError, Subscription } from 'rxjs';
import { tap, finalize, catchError } from 'rxjs/operators';
import {
TimeoutErrorMode,
IEsSearchResponse,
SearchInterceptor,
SearchInterceptorDeps,
UI_SETTINGS,
IKibanaSearchRequest,
} from '../../../../../src/plugins/data/public';
import { AbortError, abortSignalToPromise } from '../../../../../src/plugins/kibana_utils/public';
import {
IAsyncSearchRequest,
ENHANCED_ES_SEARCH_STRATEGY,
IAsyncSearchOptions,
doPartialSearch,
throwOnEsError,
} from '../../common';
import { AbortError } from '../../../../../src/plugins/kibana_utils/common';
import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common';
export class EnhancedSearchInterceptor extends SearchInterceptor {
private uiSettingsSub: Subscription;
@ -60,49 +53,26 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
if (this.deps.usageCollector) this.deps.usageCollector.trackQueriesCancelled();
};
public search(
request: IAsyncSearchRequest,
{ pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
) {
let { id } = request;
public search({ id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions = {}) {
const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({
abortSignal: options.abortSignal,
timeout: this.searchTimeout,
});
const abortedPromise = abortSignalToPromise(combinedSignal);
const strategy = options?.strategy ?? ENHANCED_ES_SEARCH_STRATEGY;
const searchOptions = { ...options, strategy, abortSignal: combinedSignal };
const search = () => this.runSearch({ id, ...request }, searchOptions);
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return doPartialSearch<IEsSearchResponse>(
() => this.runSearch(request, { ...options, strategy, abortSignal: combinedSignal }),
(requestId) =>
this.runSearch(
{ ...request, id: requestId },
{ ...options, strategy, abortSignal: combinedSignal }
),
(r) => !r.isRunning,
(response) => response.id,
id,
{ pollInterval }
).pipe(
tap((r) => {
id = r.id ?? id;
}),
throwOnEsError(),
takeUntil(from(abortedPromise.promise)),
return pollSearch(search, { ...options, abortSignal: combinedSignal }).pipe(
tap((response) => (id = response.id)),
catchError((e: AbortError) => {
if (id) {
this.deps.http.delete(`/internal/search/${strategy}/${id}`);
}
return throwError(this.handleSearchError(e, request, timeoutSignal, options));
if (id) this.deps.http.delete(`/internal/search/${strategy}/${id}`);
return throwError(this.handleSearchError(e, timeoutSignal, options));
}),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
cleanup();
abortedPromise.cleanup();
})
);
}

View file

@ -178,7 +178,7 @@ describe('EQL search strategy', () => {
expect(requestOptions).toEqual(
expect.objectContaining({
max_retries: 2,
maxRetries: 2,
ignore: [300],
})
);

View file

@ -4,21 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { tap } from 'rxjs/operators';
import type { Logger } from 'kibana/server';
import type { ApiResponse } from '@elastic/elasticsearch';
import { search } from '../../../../../src/plugins/data/server';
import {
doPartialSearch,
normalizeEqlResponse,
} from '../../common/search/es_search/es_search_rxjs_utils';
import { getAsyncOptions, getDefaultSearchParams } from './get_default_search_params';
import type { ISearchStrategy, IEsRawSearchResponse } from '../../../../../src/plugins/data/server';
import type { ISearchStrategy } from '../../../../../src/plugins/data/server';
import type {
EqlSearchStrategyRequest,
EqlSearchStrategyResponse,
} from '../../common/search/types';
IAsyncSearchOptions,
} from '../../common';
import { getDefaultSearchParams, shimAbortSignal } from '../../../../../src/plugins/data/server';
import { pollSearch } from '../../common';
import { getDefaultAsyncGetParams, getIgnoreThrottled } from './request_utils';
import { toEqlKibanaSearchResponse } from './response_utils';
import { EqlSearchResponse } from './types';
export const eqlSearchStrategyProvider = (
logger: Logger
@ -26,48 +24,37 @@ export const eqlSearchStrategyProvider = (
return {
cancel: async (id, options, { esClient }) => {
logger.debug(`_eql/delete ${id}`);
await esClient.asCurrentUser.eql.delete({
id,
});
await esClient.asCurrentUser.eql.delete({ id });
},
search: (request, options, { esClient, uiSettingsClient }) => {
logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`);
search: ({ id, ...request }, options: IAsyncSearchOptions, { esClient, uiSettingsClient }) => {
logger.debug(`_eql/search ${JSON.stringify(request.params) || id}`);
const { utils } = search.esSearch;
const asyncOptions = getAsyncOptions();
const requestOptions = utils.toSnakeCase({ ...request.options });
const client = esClient.asCurrentUser.eql;
return doPartialSearch<ApiResponse<IEsRawSearchResponse>>(
async () => {
const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams(
uiSettingsClient
);
return client.search(
utils.toSnakeCase({
ignoreThrottled,
ignoreUnavailable,
...asyncOptions,
const search = async () => {
const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(
uiSettingsClient
);
const params = id
? getDefaultAsyncGetParams()
: {
...(await getIgnoreThrottled(uiSettingsClient)),
...defaultParams,
...getDefaultAsyncGetParams(),
...request.params,
}) as EqlSearchStrategyRequest['params'],
requestOptions
);
},
(id) =>
client.get(
{
id: id!,
...utils.toSnakeCase(asyncOptions),
},
requestOptions
),
(response) => !response.body.is_running,
(response) => response.body.id,
request.id,
options
).pipe(normalizeEqlResponse(), utils.toKibanaSearchResponse());
};
const promise = id
? client.get<EqlSearchResponse>({ ...params, id }, request.options)
: client.search<EqlSearchResponse>(
params as EqlSearchStrategyRequest['params'],
request.options
);
const response = await shimAbortSignal(promise, options.abortSignal);
return toEqlKibanaSearchResponse(response);
};
return pollSearch(search, options).pipe(tap((response) => (id = response.id)));
},
};
};

View file

@ -4,86 +4,67 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { Observable } from 'rxjs';
import type { Logger, SharedGlobalConfig } from 'kibana/server';
import { first, tap } from 'rxjs/operators';
import { SearchResponse } from 'elasticsearch';
import { from } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import type { SearchResponse } from 'elasticsearch';
import type { ApiResponse } from '@elastic/elasticsearch';
import {
getShardTimeout,
shimHitsTotal,
search,
SearchStrategyDependencies,
} from '../../../../../src/plugins/data/server';
import { doPartialSearch } from '../../common/search/es_search/es_search_rxjs_utils';
import { getDefaultSearchParams, getAsyncOptions } from './get_default_search_params';
import type { SharedGlobalConfig, Logger } from '../../../../../src/core/server';
import type {
ISearchStrategy,
SearchUsage,
IEsRawSearchResponse,
ISearchOptions,
IEsSearchRequest,
IEsSearchResponse,
ISearchOptions,
ISearchStrategy,
SearchStrategyDependencies,
SearchUsage,
} from '../../../../../src/plugins/data/server';
import type { IEnhancedEsSearchRequest } from '../../common';
const { utils } = search.esSearch;
interface IEsRawAsyncSearchResponse<Source = any> extends IEsRawSearchResponse<Source> {
response: SearchResponse<Source>;
}
import {
getDefaultSearchParams,
getShardTimeout,
getTotalLoaded,
searchUsageObserver,
shimAbortSignal,
} from '../../../../../src/plugins/data/server';
import type { IAsyncSearchOptions } from '../../common';
import { pollSearch } from '../../common';
import {
getDefaultAsyncGetParams,
getDefaultAsyncSubmitParams,
getIgnoreThrottled,
} from './request_utils';
import { toAsyncKibanaSearchResponse } from './response_utils';
import { AsyncSearchResponse } from './types';
export const enhancedEsSearchStrategyProvider = (
config$: Observable<SharedGlobalConfig>,
logger: Logger,
usage?: SearchUsage
): ISearchStrategy<IEnhancedEsSearchRequest> => {
): ISearchStrategy<IEsSearchRequest> => {
function asyncSearch(
request: IEnhancedEsSearchRequest,
options: ISearchOptions,
{ id, ...request }: IEsSearchRequest,
options: IAsyncSearchOptions,
{ esClient, uiSettingsClient }: SearchStrategyDependencies
) {
const asyncOptions = getAsyncOptions();
const client = esClient.asCurrentUser.asyncSearch;
return doPartialSearch<ApiResponse<IEsRawAsyncSearchResponse>>(
async () =>
client.submit(
utils.toSnakeCase({
...(await getDefaultSearchParams(uiSettingsClient)),
batchedReduceSize: 64,
keepOnCompletion: !!options.sessionId, // Always return an ID, even if the request completes quickly
...asyncOptions,
...request.params,
})
),
(id) =>
client.get({
id: id!,
...utils.toSnakeCase({ ...asyncOptions }),
}),
(response) => !response.body.is_running,
(response) => response.body.id,
request.id,
options
).pipe(
utils.toKibanaSearchResponse(),
map((response) => ({
...response,
rawResponse: shimHitsTotal(response.rawResponse.response!),
})),
utils.trackSearchStatus(logger, usage),
utils.includeTotalLoaded()
const search = async () => {
const params = id
? getDefaultAsyncGetParams()
: { ...(await getDefaultAsyncSubmitParams(uiSettingsClient, options)), ...request.params };
const promise = id
? client.get<AsyncSearchResponse>({ ...params, id })
: client.submit<AsyncSearchResponse>(params);
const { body } = await shimAbortSignal(promise, options.abortSignal);
return toAsyncKibanaSearchResponse(body);
};
return pollSearch(search, options).pipe(
tap((response) => (id = response.id)),
tap(searchUsageObserver(logger, usage))
);
}
async function rollupSearch(
request: IEnhancedEsSearchRequest,
request: IEsSearchRequest,
options: ISearchOptions,
{ esClient, uiSettingsClient }: SearchStrategyDependencies
): Promise<IEsSearchResponse> {
@ -91,11 +72,12 @@ export const enhancedEsSearchStrategyProvider = (
const { body, index, ...params } = request.params!;
const method = 'POST';
const path = encodeURI(`/${index}/_rollup_search`);
const querystring = utils.toSnakeCase({
const querystring = {
...getShardTimeout(config),
...(await getIgnoreThrottled(uiSettingsClient)),
...(await getDefaultSearchParams(uiSettingsClient)),
...params,
});
};
const promise = esClient.asCurrentUser.transport.request({
method,
@ -104,17 +86,16 @@ export const enhancedEsSearchStrategyProvider = (
querystring,
});
const esResponse = await utils.shimAbortSignal(promise, options?.abortSignal);
const esResponse = await shimAbortSignal(promise, options?.abortSignal);
const response = esResponse.body as SearchResponse<any>;
return {
rawResponse: response,
...utils.getTotalLoaded(response._shards),
...getTotalLoaded(response),
};
}
return {
search: (request, options, deps) => {
search: (request, options: IAsyncSearchOptions, deps) => {
logger.debug(`search ${JSON.stringify(request.params) || request.id}`);
return request.indexType !== 'rollup'

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IUiSettingsClient } from 'src/core/server';
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
import { getDefaultSearchParams as getBaseSearchParams } from '../../../../../src/plugins/data/server';
/**
@internal
*/
export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient) {
const ignoreThrottled = !(await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN));
return {
ignoreThrottled,
...(await getBaseSearchParams(uiSettingsClient)),
};
}
/**
@internal
*/
export const getAsyncOptions = (): {
waitForCompletionTimeout: string;
keepAlive: string;
} => ({
waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return
keepAlive: '1m', // Extend the TTL for this search request by one minute,
});

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IUiSettingsClient } from 'kibana/server';
import {
AsyncSearchGet,
AsyncSearchSubmit,
Search,
} from '@elastic/elasticsearch/api/requestParams';
import { ISearchOptions, UI_SETTINGS } from '../../../../../src/plugins/data/common';
import { getDefaultSearchParams } from '../../../../../src/plugins/data/server';
/**
* @internal
*/
export async function getIgnoreThrottled(
uiSettingsClient: IUiSettingsClient
): Promise<Pick<Search, 'ignore_throttled'>> {
const includeFrozen = await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
return { ignore_throttled: !includeFrozen };
}
/**
@internal
*/
export async function getDefaultAsyncSubmitParams(
uiSettingsClient: IUiSettingsClient,
options: ISearchOptions
): Promise<
Pick<
AsyncSearchSubmit,
| 'batched_reduce_size'
| 'keep_alive'
| 'wait_for_completion_timeout'
| 'ignore_throttled'
| 'max_concurrent_shard_requests'
| 'ignore_unavailable'
| 'track_total_hits'
| 'keep_on_completion'
>
> {
return {
batched_reduce_size: 64,
keep_on_completion: !!options.sessionId, // Always return an ID, even if the request completes quickly
...getDefaultAsyncGetParams(),
...(await getIgnoreThrottled(uiSettingsClient)),
...(await getDefaultSearchParams(uiSettingsClient)),
};
}
/**
@internal
*/
export function getDefaultAsyncGetParams(): Pick<
AsyncSearchGet,
'keep_alive' | 'wait_for_completion_timeout'
> {
return {
keep_alive: '1m', // Extend the TTL for this search request by one minute
wait_for_completion_timeout: '100ms', // Wait up to 100ms for the response to return
};
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { ApiResponse } from '@elastic/elasticsearch';
import { getTotalLoaded } from '../../../../../src/plugins/data/server';
import { AsyncSearchResponse, EqlSearchResponse } from './types';
import { EqlSearchStrategyResponse } from '../../common/search';
/**
* Get the Kibana representation of an async search response (see `IKibanaSearchResponse`).
*/
export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse) {
return {
id: response.id,
rawResponse: response.response,
isPartial: response.is_partial,
isRunning: response.is_running,
...getTotalLoaded(response.response),
};
}
/**
* Get the Kibana representation of an EQL search response (see `IKibanaSearchResponse`).
* (EQL does not provide _shard info, so total/loaded cannot be calculated.)
*/
export function toEqlKibanaSearchResponse(
response: ApiResponse<EqlSearchResponse>
): EqlSearchStrategyResponse {
return {
id: response.body.id,
rawResponse: response,
isPartial: response.body.is_partial,
isRunning: response.body.is_running,
};
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchResponse } from 'elasticsearch';
export interface AsyncSearchResponse<T = unknown> {
id?: string;
response: SearchResponse<T>;
is_partial: boolean;
is_running: boolean;
}
export interface EqlSearchResponse<T = unknown> extends SearchResponse<T> {
id?: string;
is_partial: boolean;
is_running: boolean;
}

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mergeMap } from 'rxjs/operators';
import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server';
import { map, mergeMap } from 'rxjs/operators';
import {
ISearchStrategy,
PluginStart,
shimHitsTotal,
} from '../../../../../../src/plugins/data/server';
import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/common';
import {
FactoryQueryTypes,
@ -28,9 +32,17 @@ export const securitySolutionSearchStrategyProvider = <T extends FactoryQueryTyp
const queryFactory: SecuritySolutionFactory<T> =
securitySolutionFactory[request.factoryQueryType];
const dsl = queryFactory.buildDsl(request);
return es
.search({ ...request, params: dsl }, options, deps)
.pipe(mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)));
return es.search({ ...request, params: dsl }, options, deps).pipe(
map((response) => {
return {
...response,
...{
rawResponse: shimHitsTotal(response.rawResponse),
},
};
}),
mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))
);
},
cancel: async (id, options, deps) => {
if (es.cancel) {

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mergeMap } from 'rxjs/operators';
import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server';
import { map, mergeMap } from 'rxjs/operators';
import {
ISearchStrategy,
PluginStart,
shimHitsTotal,
} from '../../../../../../src/plugins/data/server';
import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/common';
import {
TimelineFactoryQueryTypes,
@ -29,9 +33,17 @@ export const securitySolutionTimelineSearchStrategyProvider = <T extends Timelin
securitySolutionTimelineFactory[request.factoryQueryType];
const dsl = queryFactory.buildDsl(request);
return es
.search({ ...request, params: dsl }, options, deps)
.pipe(mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)));
return es.search({ ...request, params: dsl }, options, deps).pipe(
map((response) => {
return {
...response,
...{
rawResponse: shimHitsTotal(response.rawResponse),
},
};
}),
mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))
);
},
cancel: async (id, options, deps) => {
if (es.cancel) {