diff --git a/src/platform/packages/private/kbn-index-editor/src/services/index_update_service.ts b/src/platform/packages/private/kbn-index-editor/src/services/index_update_service.ts index ec33f30c620c4..2b7ca5064bf13 100644 --- a/src/platform/packages/private/kbn-index-editor/src/services/index_update_service.ts +++ b/src/platform/packages/private/kbn-index-editor/src/services/index_update_service.ts @@ -754,7 +754,7 @@ export class IndexUpdateService { }, { strategy: 'esql_async', - retrieveResults: true, + returnIntermediateResults: true, } ) .pipe( diff --git a/src/platform/packages/shared/kbn-search-types/src/types.ts b/src/platform/packages/shared/kbn-search-types/src/types.ts index d2074cab8c011..9d939d1ba7be8 100644 --- a/src/platform/packages/shared/kbn-search-types/src/types.ts +++ b/src/platform/packages/shared/kbn-search-types/src/types.ts @@ -94,11 +94,10 @@ export interface ISearchOptions { isRestore?: boolean; /** - * By default, when polling, we don't retrieve the results of the search request (until it is complete). (For async - * search, this is the difference between calling _async_search/{id} and _async_search/status/{id}.) setting this to - * `true` will request the search results, regardless of whether or not the search is complete. + * By default, when polling, we don't retrieve the results of the search request (until it is complete). + * setting this to `true` will request the search results, regardless of whether or not the search is complete. */ - retrieveResults?: boolean; + returnIntermediateResults?: boolean; /** * Represents a meta-information about a Kibana entity intitating a saerch request. @@ -144,7 +143,7 @@ export type ISearchOptionsSerializable = Pick< | 'isStored' | 'isSearchStored' | 'isRestore' - | 'retrieveResults' + | 'returnIntermediateResults' | 'executionContext' | 'stream' | 'projectRouting' diff --git a/src/platform/plugins/shared/data/common/search/poll_search.ts b/src/platform/plugins/shared/data/common/search/poll_search.ts index c0c13e9d5b385..0b2d498ba2c94 100644 --- a/src/platform/plugins/shared/data/common/search/poll_search.ts +++ b/src/platform/plugins/shared/data/common/search/poll_search.ts @@ -16,7 +16,6 @@ import { fromEvent, switchMap, takeUntil, - takeWhile, tap, throwError, timer, @@ -49,12 +48,12 @@ export const pollSearch = ( }; return defer(() => { - const startTime = Date.now(); - if (abortSignal?.aborted) { throw new AbortError(); } + const startTime = Date.now(); + const safeCancel = () => cancel?.().catch((e) => { console.error(e); // eslint-disable-line no-console @@ -69,16 +68,17 @@ export const pollSearch = ( ); return from(search()).pipe( - expand(() => { + expand((response) => { const elapsedTime = Date.now() - startTime; - return timer(getPollInterval(elapsedTime)).pipe(switchMap(() => search())); + return isRunningResponse(response) + ? timer(getPollInterval(elapsedTime)).pipe(switchMap(() => search())) + : EMPTY; }), tap((response) => { if (isAbortResponse(response)) { throw new AbortError(); } }), - takeWhile(isRunningResponse, true), takeUntil(aborted$) ); }); diff --git a/src/platform/plugins/shared/data/common/search/strategies/ese_search/types.ts b/src/platform/plugins/shared/data/common/search/strategies/ese_search/types.ts index 7e61a78713893..3569e54d83bca 100644 --- a/src/platform/plugins/shared/data/common/search/strategies/ese_search/types.ts +++ b/src/platform/plugins/shared/data/common/search/strategies/ese_search/types.ts @@ -14,7 +14,11 @@ export const ENHANCED_ES_SEARCH_STRATEGY = 'ese'; export interface IAsyncSearchOptions extends SearchSourceSearchOptions { /** * The number of milliseconds to wait between receiving a response and sending another request - * If not provided, then a default 1 second interval with back-off up to 5 seconds interval is used + * If not provided, then it defaults to 0 (no wait time) */ pollInterval?: number; + /** + * The length of time to wait for results before initiating a new poll request. + */ + pollLength?: string; } diff --git a/src/platform/plugins/shared/data/config.mock.ts b/src/platform/plugins/shared/data/config.mock.ts index f7352bac8a552..3c24889ea2a71 100644 --- a/src/platform/plugins/shared/data/config.mock.ts +++ b/src/platform/plugins/shared/data/config.mock.ts @@ -17,10 +17,12 @@ export const getMockSearchConfig = ({ }, asyncSearch: { waitForCompletion = moment.duration(100, 'ms'), + pollLength = moment.duration(2000, 'ms'), keepAlive = moment.duration(1, 'm'), batchedReduceSize = 64, } = { waitForCompletion: moment.duration(100, 'ms'), + pollLength: moment.duration(2000, 'ms'), keepAlive: moment.duration(1, 'm'), batchedReduceSize: 64, }, @@ -33,6 +35,7 @@ export const getMockSearchConfig = ({ waitForCompletion, keepAlive, batchedReduceSize, + pollLength, } as SearchConfigSchema['asyncSearch'], sessions: { enabled, diff --git a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.test.ts b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.test.ts index 11a9adc2768fd..677e6a223842b 100644 --- a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.test.ts @@ -33,6 +33,7 @@ import { SearchTimeoutError, TimeoutErrorMode } from './timeout_error'; import { SearchSessionIncompleteWarning } from './search_session_incomplete_warning'; import { getMockSearchConfig } from '../../../config.mock'; import type { ICPSManager } from '@kbn/cps-utils'; +import moment from 'moment'; jest.mock('./create_request_hash', () => { const originalModule = jest.requireActual('./create_request_hash'); @@ -2170,7 +2171,7 @@ describe('SearchInterceptor', () => { "/internal/search/ese/1", Object { "asResponse": true, - "body": "{\\"id\\":\\"1\\",\\"params\\":{},\\"retrieveResults\\":true,\\"stream\\":true}", + "body": "{\\"id\\":\\"1\\",\\"params\\":{},\\"returnIntermediateResults\\":true,\\"stream\\":true}", "context": undefined, "signal": AbortSignal {}, "version": "1", @@ -2205,7 +2206,7 @@ describe('SearchInterceptor', () => { "/internal/search/ese/1", Object { "asResponse": true, - "body": "{\\"id\\":\\"1\\",\\"params\\":{},\\"retrieveResults\\":true,\\"stream\\":true}", + "body": "{\\"id\\":\\"1\\",\\"params\\":{},\\"returnIntermediateResults\\":true,\\"stream\\":true}", "context": undefined, "signal": AbortSignal {}, "version": "1", @@ -2490,4 +2491,221 @@ describe('SearchInterceptor', () => { }); }); }); + + describe('pollLength configuration', () => { + const inspectorServiceMock = { + open: () => {}, + } as unknown as InspectorStart; + + beforeEach(() => { + mockCoreSetup.http.post.mockReset(); + }); + + test('should use DEFAULT_MULTIPLEXING_POLL_LENGTH when pollLength is not set and protocol supports multiplexing', async () => { + const interceptor = new SearchInterceptor({ + toasts: mockCoreSetup.notifications.toasts, + startServices: new Promise((resolve) => { + resolve([ + mockCoreStart, + { inspector: inspectorServiceMock } as unknown as SearchServiceStartDependencies, + {}, + ]); + }), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, + executionContext: mockCoreSetup.executionContext, + session: sessionService, + searchConfig: { + asyncSearch: { + waitForCompletion: moment.duration(100, 'ms'), + keepAlive: moment.duration(1, 'm'), + batchedReduceSize: 64, + pollLength: undefined, // Explicitly undefined + }, + sessions: { + enabled: true, + defaultExpiration: moment.duration(7, 'd'), + }, + } as any, + }); + (interceptor as any).protocolSupportsMultiplexing = true; + + const responses = [ + { + time: 10, + value: getMockSearchResponse({ + isPartial: true, + isRunning: true, + id: '1', + rawResponse: {}, + }), + }, + { + time: 20, + value: getMockSearchResponse({ + isPartial: false, + isRunning: false, + id: '1', + rawResponse: {}, + }), + }, + ]; + + mockCoreSetup.http.post.mockImplementation(getHttpMock(responses)); + + const response = interceptor.search({ params: {} }, { pollInterval: 0 }); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + await timeTravel(20); + + expect(mockCoreSetup.http.post).toHaveBeenCalledTimes(2); + + const pollRequest = ( + mockCoreSetup.http.post.mock.calls[1] as unknown as [string, HttpFetchOptions] + )[1]; + const pollBody = JSON.parse(pollRequest?.body as string); + + // Should use DEFAULT_MULTIPLEXING_POLL_LENGTH (30s) + expect(pollBody.params.wait_for_completion_timeout).toBe('30s'); + }); + + test('should not set wait_for_completion_timeout when pollLength is not set and protocol does not support multiplexing', async () => { + const interceptor = new SearchInterceptor({ + toasts: mockCoreSetup.notifications.toasts, + startServices: new Promise((resolve) => { + resolve([ + mockCoreStart, + { inspector: inspectorServiceMock } as unknown as SearchServiceStartDependencies, + {}, + ]); + }), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, + executionContext: mockCoreSetup.executionContext, + session: sessionService, + searchConfig: { + asyncSearch: { + waitForCompletion: moment.duration(100, 'ms'), + keepAlive: moment.duration(1, 'm'), + batchedReduceSize: 64, + pollLength: undefined, // Explicitly undefined + }, + sessions: { + enabled: true, + defaultExpiration: moment.duration(7, 'd'), + }, + } as any, + }); + (interceptor as any).protocolSupportsMultiplexing = false; + + const responses = [ + { + time: 10, + value: getMockSearchResponse({ + isPartial: true, + isRunning: true, + id: '1', + rawResponse: {}, + }), + }, + { + time: 20, + value: getMockSearchResponse({ + isPartial: false, + isRunning: false, + id: '1', + rawResponse: {}, + }), + }, + ]; + + mockCoreSetup.http.post.mockImplementation(getHttpMock(responses)); + + const response = interceptor.search({ params: {} }, { pollInterval: 0 }); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + await timeTravel(20); + + expect(mockCoreSetup.http.post).toHaveBeenCalledTimes(2); + + const pollRequest = ( + mockCoreSetup.http.post.mock.calls[1] as unknown as [string, HttpFetchOptions] + )[1]; + const pollBody = JSON.parse(pollRequest?.body as string); + + // Should not have wait_for_completion_timeout + expect(pollBody.params.wait_for_completion_timeout).toBeUndefined(); + }); + + test('should not set wait_for_completion_timeout when pollLength is set even if protocol supports multiplexing', async () => { + const interceptor = new SearchInterceptor({ + toasts: mockCoreSetup.notifications.toasts, + startServices: new Promise((resolve) => { + resolve([ + mockCoreStart, + { inspector: inspectorServiceMock } as unknown as SearchServiceStartDependencies, + {}, + ]); + }), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, + executionContext: mockCoreSetup.executionContext, + session: sessionService, + searchConfig: { + asyncSearch: { + waitForCompletion: moment.duration(100, 'ms'), + keepAlive: moment.duration(1, 'm'), + batchedReduceSize: 64, + pollLength: moment.duration(1, 'm'), // Explicitly set + }, + sessions: { + enabled: true, + defaultExpiration: moment.duration(7, 'd'), + }, + } as any, + }); + (interceptor as any).protocolSupportsMultiplexing = true; + + const responses = [ + { + time: 10, + value: getMockSearchResponse({ + isPartial: true, + isRunning: true, + id: '1', + rawResponse: {}, + }), + }, + { + time: 20, + value: getMockSearchResponse({ + isPartial: false, + isRunning: false, + id: '1', + rawResponse: {}, + }), + }, + ]; + + mockCoreSetup.http.post.mockImplementation(getHttpMock(responses)); + + const response = interceptor.search({ params: {} }, { pollInterval: 0 }); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + await timeTravel(20); + + expect(mockCoreSetup.http.post).toHaveBeenCalledTimes(2); + + const pollRequest = ( + mockCoreSetup.http.post.mock.calls[1] as unknown as [string, HttpFetchOptions] + )[1]; + const pollBody = JSON.parse(pollRequest?.body as string); + + // Should not have wait_for_completion_timeout + expect(pollBody.params.wait_for_completion_timeout).toBeUndefined(); + }); + }); }); diff --git a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts index f09486a807e36..91f43550faf37 100644 --- a/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/platform/plugins/shared/data/public/search/search_interceptor/search_interceptor.ts @@ -66,6 +66,7 @@ import type { import { createEsError, isEsError, renderSearchError } from '@kbn/search-errors'; import { AbortReason, defaultFreeze } from '@kbn/kibana-utils-plugin/common'; import type { ICPSManager } from '@kbn/cps-utils'; +import moment from 'moment'; import { EVENT_TYPE_DATA_SEARCH_TIMEOUT, EVENT_PROPERTY_SEARCH_TIMEOUT_MS, @@ -112,6 +113,8 @@ export interface SearchInterceptorDeps { const MAX_CACHE_ITEMS = 50; const MAX_CACHE_SIZE_MB = 10; +const DEFAULT_MULTIPLEXING_POLL_LENGTH = '30s'; + export class SearchInterceptor { private uiSettingsSubs: Subscription[] = []; private searchTimeout: number; @@ -119,6 +122,8 @@ export class SearchInterceptor { MAX_CACHE_ITEMS, MAX_CACHE_SIZE_MB ); + private protocolSupportsMultiplexing: boolean = false; + private performanceObserver?: PerformanceObserver; /** * Observable that emits when the number of pending requests changes. @@ -164,11 +169,29 @@ export class SearchInterceptor { this.searchTimeout = timeout; }) ); + + // Set up PerformanceObserver to capture search requests as they happen + try { + this.performanceObserver = new PerformanceObserver((list) => { + const entries = list.getEntries() as PerformanceResourceTiming[]; + const entry = entries.find(({ name }) => name.includes('/internal/search/')); + if (entry) { + this.protocolSupportsMultiplexing = ['h2', 'h3'].includes(entry.nextHopProtocol); + this.performanceObserver?.disconnect(); // We only need to detect this once, so we can disconnect the observer after the first match + } + }); + this.performanceObserver.observe({ entryTypes: ['resource'] }); + } catch (e) { + // Silently fail - protocol detection is not critical + } } public stop() { this.responseCache.clear(); this.uiSettingsSubs.forEach((s) => s.unsubscribe()); + if (this.performanceObserver) { + this.performanceObserver.disconnect(); + } } /* @@ -275,8 +298,8 @@ export class SearchInterceptor { if (combined.sessionId !== undefined) serializableOptions.sessionId = combined.sessionId; if (combined.isRestore !== undefined) serializableOptions.isRestore = combined.isRestore; - if (combined.retrieveResults !== undefined) - serializableOptions.retrieveResults = combined.retrieveResults; + if (combined.returnIntermediateResults !== undefined) + serializableOptions.returnIntermediateResults = combined.returnIntermediateResults; if (combined.legacyHitsTotal !== undefined) serializableOptions.legacyHitsTotal = combined.legacyHitsTotal; if (combined.strategy !== undefined) serializableOptions.strategy = combined.strategy; @@ -306,7 +329,8 @@ export class SearchInterceptor { const search = ({ abortSignal = searchAbortController.getSignal(), - }: Pick = {}) => { + pollLength, + }: Pick & { pollLength?: string } = {}) => { const [{ isSearchStored }, afterPoll] = searchTracker?.beforePoll() ?? [ { isSearchStored: false }, () => {}, @@ -320,6 +344,7 @@ export class SearchInterceptor { ...this.deps.session.getSearchOptions(sessionId), abortSignal, isSearchStored, + pollLength, } ) .then((result) => { @@ -337,7 +362,12 @@ export class SearchInterceptor { abort: (reason?: AbortReason) => searchAbortController.abort(reason), poll: async (abortSignal) => { if (id) { - await search({ abortSignal }); + await search({ + abortSignal, + // pollLength should be 0 because this poll is not for results, but just to signal to the server to + // record the search in the background search saved object. we want it to finish quickly + pollLength: '0', + }); } }, }) @@ -404,8 +434,17 @@ export class SearchInterceptor { // Preserve and project first request params into responses. let firstRequestParams: SanitizedConnectionRequestParams; + const pollInterval = this.deps.searchConfig.asyncSearch.pollInterval + ? // the types incorrectly report asyncSearch.pollInterval as a duration already, but it + // is actually a duration string that needs to be initialized with moment.duration + // TODO — can we fix this? + moment.duration(this.deps.searchConfig.asyncSearch.pollInterval).asMilliseconds() + : this.protocolSupportsMultiplexing + ? 0 + : undefined; + return pollSearch(search, cancel, { - pollInterval: this.deps.searchConfig.asyncSearch.pollInterval, + pollInterval, ...options, abortSignal: searchAbortController.getSignal(), }).pipe( @@ -440,7 +479,11 @@ export class SearchInterceptor { return from( this.runSearch( { id, ...request }, - { ...options, abortSignal: new AbortController().signal, retrieveResults: true } + { + ...options, + abortSignal: new AbortController().signal, + returnIntermediateResults: true, + } ) ).pipe( map((response) => @@ -484,18 +527,28 @@ export class SearchInterceptor { */ private runSearch( { params, ...request }: IKibanaSearchRequest, - options?: ISearchOptions + options?: ISearchOptions & { pollLength?: string } ): Promise { const { abortSignal } = options || {}; const requestHash = params ? createRequestHashForBackgroundSearches(params) : undefined; const { executionContext, strategy, ...searchOptions } = this.getSerializableOptions(options); - - // FIXME: the dropNullColumns param shouldn't be needed during polling - // once https://github.com/elastic/elasticsearch/issues/138439 is resolved - // at that point, exclude all params when request.id is defined (polling phase) - const paramsToUse = request.id ? { dropNullColumns: params?.dropNullColumns } : params || {}; + const paramsToUse = request.id + ? { + wait_for_completion_timeout: options?.pollLength + ? options.pollLength + : // don't set or override user-configured pollLength, it will be applied server-side + !this.deps.searchConfig.asyncSearch.pollLength && this.protocolSupportsMultiplexing + ? DEFAULT_MULTIPLEXING_POLL_LENGTH + : undefined, + + // FIXME: the dropNullColumns param shouldn't be needed during polling + // once https://github.com/elastic/elasticsearch/issues/138439 is resolved + // at that point, exclude all params when request.id is defined (polling phase) + dropNullColumns: params?.dropNullColumns, + } + : params || {}; return this.deps.http .post( buildPath('/internal/search/{strategy}/{id?}', { diff --git a/src/platform/plugins/shared/data/server/config.ts b/src/platform/plugins/shared/data/server/config.ts index fc9a25b317261..4ac00f3a86d5c 100644 --- a/src/platform/plugins/shared/data/server/config.ts +++ b/src/platform/plugins/shared/data/server/config.ts @@ -55,7 +55,6 @@ export const searchConfigSchema = schema.object({ asyncSearch: schema.object({ /** * Block and wait until the search is completed up to the timeout (see es async_search's `wait_for_completion_timeout`) - * TODO: we should optimize this as 100ms is likely not optimal (https://github.com/elastic/kibana/issues/143277) */ waitForCompletion: schema.duration({ defaultValue: '200ms' }), /** @@ -71,9 +70,15 @@ export const searchConfigSchema = schema.object({ batchedReduceSize: schema.number({ defaultValue: 64 }), /** * How long to wait before polling the async_search after the previous poll response. - * If not provided, then default dynamic interval with backoff is used. + * If not provided, defaults to zero. */ pollInterval: schema.maybe(schema.number({ min: 200 })), + /** + * How long to wait for results before initiating a new poll request. + * Accepts duration format (e.g., "30s", "100ms"). If not provided, + * defaults to protocol-specific behavior (30s for HTTP/2 or HTTP/3). + */ + pollLength: schema.maybe(schema.duration()), }), aggs: schema.object({ shardDelay: schema.object({ diff --git a/src/platform/plugins/shared/data/server/index.ts b/src/platform/plugins/shared/data/server/index.ts index d7c0da1541ada..b9b2e53ce01be 100644 --- a/src/platform/plugins/shared/data/server/index.ts +++ b/src/platform/plugins/shared/data/server/index.ts @@ -59,7 +59,6 @@ export type { ISearchSessionService, SearchRequestHandlerContext, DataRequestHandlerContext, - AsyncSearchStatusResponse, } from './search'; export { SearchSessionService, diff --git a/src/platform/plugins/shared/data/server/search/routes/search.ts b/src/platform/plugins/shared/data/server/search/routes/search.ts index 3e268c6bca722..98ecdfb277125 100644 --- a/src/platform/plugins/shared/data/server/search/routes/search.ts +++ b/src/platform/plugins/shared/data/server/search/routes/search.ts @@ -52,7 +52,7 @@ export function registerSearchRoute( sessionId: schema.maybe(schema.string()), isStored: schema.maybe(schema.boolean()), isRestore: schema.maybe(schema.boolean()), - retrieveResults: schema.maybe(schema.boolean()), + returnIntermediateResults: schema.maybe(schema.boolean()), stream: schema.maybe(schema.boolean()), requestHash: schema.maybe(schema.string()), projectRouting: schema.maybe(schema.string()), @@ -68,7 +68,7 @@ export function registerSearchRoute( sessionId, isStored, isRestore, - retrieveResults, + returnIntermediateResults, stream, requestHash, projectRouting, @@ -103,7 +103,7 @@ export function registerSearchRoute( sessionId, isStored, isRestore, - retrieveResults, + returnIntermediateResults, stream, requestHash, projectRouting, diff --git a/src/platform/plugins/shared/data/server/search/strategies/common/async_utils.ts b/src/platform/plugins/shared/data/server/search/strategies/common/async_utils.ts index 5d1d8be8cc8fa..043ce87135ae8 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/common/async_utils.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/common/async_utils.ts @@ -67,7 +67,9 @@ export function getCommonDefaultAsyncGetParams( return { // Wait up to the timeout for the response to return - wait_for_completion_timeout: `${config.asyncSearch.waitForCompletion.asMilliseconds()}ms`, + ...(config.asyncSearch.pollLength + ? { wait_for_completion_timeout: `${config.asyncSearch.pollLength.asMilliseconds()}ms` } + : {}), ...(useSearchSessions && options.isStored ? // Use session's keep_alive if search belongs to a stored session options.isSearchStored || options.isRestore // if search was already stored and extended, then no need to extend keepAlive diff --git a/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.test.ts b/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.test.ts index 9507d0d812605..d2155cb0a89b5 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.test.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.test.ts @@ -98,6 +98,7 @@ describe('EQL search strategy', () => { ignore_unavailable: true, index: 'logstash-*', keep_alive: '60000ms', + keep_on_completion: false, max_concurrent_shard_requests: undefined, wait_for_completion_timeout: '100ms', }); @@ -113,7 +114,7 @@ describe('EQL search strategy', () => { expect(requestParams).toEqual({ id: 'my-search-id', keep_alive: '60000ms', - wait_for_completion_timeout: '100ms', + wait_for_completion_timeout: '2000ms', }); }); diff --git a/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.ts index b86a57881dc60..788a7d31d3709 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -23,7 +23,10 @@ import type { EqlSearchResponse } from './types'; import type { ISearchStrategy } from '../../types'; import { getDefaultSearchParams } from '../es_search'; import { getIgnoreThrottled } from '../ese_search/request_utils'; -import { getCommonDefaultAsyncGetParams } from '../common/async_utils'; +import { + getCommonDefaultAsyncGetParams, + getCommonDefaultAsyncSubmitParams, +} from '../common/async_utils'; export const eqlSearchStrategyProvider = ( searchConfig: SearchConfigSchema, @@ -60,7 +63,7 @@ export const eqlSearchStrategyProvider = ( : { ...(await getIgnoreThrottled(uiSettingsClient)), ...defaultParams, - ...getCommonDefaultAsyncGetParams(searchConfig, options, { + ...getCommonDefaultAsyncSubmitParams(searchConfig, options, { /* disable until full eql support */ disableSearchSessions: true, }), ...request.params, diff --git a/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.test.ts b/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.test.ts index b384f40029b0d..c9ff7ce47f3ea 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.test.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.test.ts @@ -19,27 +19,6 @@ import { createSearchSessionsClientMock } from '../../mocks'; import { getMockSearchConfig } from '../../../../config.mock'; import { DataViewType } from '@kbn/data-views-plugin/common'; -const mockAsyncStatusResponse = (isComplete = false) => ({ - body: { - id: 'FlVYVkw0clJIUS1TMHpHdXA3a29pZUEedldKX1c1bnBRVXFmalZ4emV1cjFCUToxNjYzMDgx', - is_running: !isComplete, - is_partial: !isComplete, - start_time_in_millis: 1710451842532, - expiration_time_in_millis: 1710451907469, - _shards: { - total: 10, - successful: 0, - skipped: 0, - failed: 0, - }, - }, - headers: { - 'x-elasticsearch-async-id': - 'FlVYVkw0clJIUS1TMHpHdXA3a29pZUEedldKX1c1bnBRVXFmalZ4emV1cjFCUToxNjYzMDgx', - 'x-elasticsearch-async-is-running': isComplete ? '?0' : '?1', - }, -}); - const mockAsyncResponse = { body: { id: 'foo', @@ -72,7 +51,6 @@ const mockRollupResponse = { describe('ES search strategy', () => { const mockApiCaller = jest.fn(); const mockRollupSearchCaller = jest.fn(); - const mockStatusCaller = jest.fn(); const mockGetCaller = jest.fn(); const mockSubmitCaller = jest.fn(); const mockDeleteCaller = jest.fn(); @@ -87,7 +65,6 @@ describe('ES search strategy', () => { esClient: { asCurrentUser: { asyncSearch: { - status: mockStatusCaller, get: mockGetCaller, submit: mockSubmitCaller, delete: mockDeleteCaller, @@ -115,7 +92,6 @@ describe('ES search strategy', () => { beforeEach(() => { mockApiCaller.mockClear(); - mockStatusCaller.mockClear(); mockGetCaller.mockClear(); mockSubmitCaller.mockClear(); mockDeleteCaller.mockClear(); @@ -152,26 +128,7 @@ describe('ES search strategy', () => { expect(request).toHaveProperty('keep_alive', '60000ms'); }); - it('returns status if incomplete', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(false)); - - const params = { index: 'logstash-*', query: {} }; - const esSearch = await enhancedEsSearchStrategyProvider( - mockLegacyConfig$, - mockSearchConfig, - mockLogger - ); - - const response = await firstValueFrom(esSearch.search({ id: 'foo', params }, {}, mockDeps)); - - expect(mockGetCaller).not.toBeCalled(); - expect(response).toHaveProperty('id'); - expect(response).toHaveProperty('isPartial', true); - expect(response).toHaveProperty('isRunning', true); - }); - it('makes a GET request to async search with ID', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', query: {} }; @@ -188,10 +145,10 @@ describe('ES search strategy', () => { expect(request.id).toEqual('foo'); expect(request).toHaveProperty('wait_for_completion_timeout'); expect(request).toHaveProperty('keep_alive', '60000ms'); + expect(request).toHaveProperty('return_intermediate_results', false); }); it('allows overriding keep_alive and wait_for_completion_timeout', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { @@ -213,6 +170,27 @@ describe('ES search strategy', () => { expect(request.id).toEqual('foo'); expect(request).toHaveProperty('wait_for_completion_timeout', '10s'); expect(request).toHaveProperty('keep_alive', '5m'); + expect(request).toHaveProperty('return_intermediate_results', false); + }); + + it('sets return_intermediate_results to true when returnIntermediateResults is true', async () => { + mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'logstash-*', query: {} }; + const esSearch = await enhancedEsSearchStrategyProvider( + mockLegacyConfig$, + mockSearchConfig, + mockLogger + ); + + await esSearch + .search({ id: 'foo', params }, { returnIntermediateResults: true }, mockDeps) + .toPromise(); + + expect(mockGetCaller).toBeCalled(); + const request = mockGetCaller.mock.calls[0][0]; + expect(request.id).toEqual('foo'); + expect(request).toHaveProperty('return_intermediate_results', true); }); it('sets transport options on POST requests', async () => { @@ -247,7 +225,6 @@ describe('ES search strategy', () => { }); it('sets transport options on GET requests', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', query: {} }; const esSearch = enhancedEsSearchStrategyProvider( @@ -260,14 +237,25 @@ describe('ES search strategy', () => { esSearch.search({ id: 'foo', params }, { transport: { maxRetries: 1 } }, mockDeps) ); - expect(mockGetCaller).toHaveBeenNthCalledWith( - 1, + expect(mockGetCaller).toBeCalled(); + const request = mockGetCaller.mock.calls[0][0]; + const transportOptions = mockGetCaller.mock.calls[0][1]; + + expect(request).toEqual( expect.objectContaining({ id: 'foo', keep_alive: '60000ms', - wait_for_completion_timeout: '100ms', - }), - expect.objectContaining({ maxRetries: 1, meta: true, signal: undefined }) + return_intermediate_results: false, + }) + ); + expect(transportOptions).toEqual( + expect.objectContaining({ + maxRetries: 1, + meta: true, + signal: undefined, + requestTimeout: 600000, + asStream: undefined, + }) ); }); @@ -474,7 +462,6 @@ describe('ES search strategy', () => { }); it('makes a GET request to async search with short keepalive, if session is not saved', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', query: {} }; @@ -491,10 +478,10 @@ describe('ES search strategy', () => { expect(request.id).toEqual('foo'); expect(request).toHaveProperty('wait_for_completion_timeout'); expect(request).toHaveProperty('keep_alive', '60000ms'); + expect(request).toHaveProperty('return_intermediate_results', false); }); it('makes a GET request to async search with long keepalive, if session is saved', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', query: {} }; @@ -513,10 +500,10 @@ describe('ES search strategy', () => { expect(request.id).toEqual('foo'); expect(request).toHaveProperty('wait_for_completion_timeout'); expect(request).toHaveProperty('keep_alive', '604800000ms'); + expect(request).toHaveProperty('return_intermediate_results', false); }); it('makes a GET request to async search with no keepalive, if session is session saved and search is stored', async () => { - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', query: {} }; @@ -539,6 +526,7 @@ describe('ES search strategy', () => { expect(request.id).toEqual('foo'); expect(request).toHaveProperty('wait_for_completion_timeout'); expect(request).not.toHaveProperty('keep_alive'); + expect(request).toHaveProperty('return_intermediate_results', false); }); it('should not delete a saved session when aborted', async () => { @@ -712,7 +700,6 @@ describe('ES search strategy', () => { it('throws normalized error on ElasticsearchClientError', async () => { const errResponse = new errors.ElasticsearchClientError('something is wrong with EsClient'); - mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true)); mockGetCaller.mockRejectedValue(errResponse); const id = 'some_other_id'; diff --git a/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.ts index 11bd84bdd13f2..b9edb6c8a2b54 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -17,13 +17,13 @@ import type { IAsyncSearchRequestParams } from '../..'; import { getKbnSearchError, KbnSearchError } from '../../report_search_error'; import type { ISearchStrategy, SearchStrategyDependencies } from '../../types'; import type { IAsyncSearchOptions } from '../../../../common'; -import { DataViewType, isRunningResponse, pollSearch } from '../../../../common'; +import { DataViewType, pollSearch } from '../../../../common'; import { getDefaultAsyncGetParams, getDefaultAsyncSubmitParams, getIgnoreThrottled, } from './request_utils'; -import { toAsyncKibanaSearchResponse, toAsyncKibanaSearchStatusResponse } from './response_utils'; +import { toAsyncKibanaSearchResponse } from './response_utils'; import type { SearchUsage } from '../../collectors/search'; import { searchUsageObserver } from '../../collectors/search'; import { getDefaultSearchParams, getShardTimeout } from '../es_search'; @@ -44,35 +44,12 @@ export const enhancedEsSearchStrategyProvider = ( return client.asyncSearch.delete({ id }); } - async function asyncSearchStatus( - { id, ...request }: IEsSearchRequest, - options: IAsyncSearchOptions, - { esClient }: Pick - ) { - const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser; - const keepAlive = - request.params?.keep_alive ?? getDefaultAsyncGetParams(searchConfig, options).keep_alive; - - const { body, headers } = await client.asyncSearch.status( - { id: id!, keep_alive: keepAlive }, - { ...options.transport, signal: options.abortSignal, meta: true } - ); - return toAsyncKibanaSearchStatusResponse(body, headers?.warning); - } - - // Gets the current status of the async search request. If the request is complete, then queries for the results. + // Gets the current status of the async search request. async function getAsyncSearch( { id, ...request }: IEsSearchRequest, options: IAsyncSearchOptions, { esClient }: SearchStrategyDependencies ) { - if (!options.retrieveResults) { - // First, request the status of the async search, and return the status if incomplete - const status = await asyncSearchStatus({ id, ...request }, options, { esClient }); - if (isRunningResponse(status)) return status; - } - - // Then, if the search is complete, request & return the final results const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser; const params = { ...getDefaultAsyncGetParams(searchConfig, options), @@ -80,6 +57,7 @@ export const enhancedEsSearchStrategyProvider = ( ...(request.params?.wait_for_completion_timeout ? { wait_for_completion_timeout: request.params.wait_for_completion_timeout } : {}), + return_intermediate_results: options.returnIntermediateResults ?? false, }; const { body, headers, meta } = await client.asyncSearch.get( { ...params, id: id! }, @@ -88,6 +66,7 @@ export const enhancedEsSearchStrategyProvider = ( signal: options.abortSignal, meta: true, asStream: options.stream, + requestTimeout: 600_000, // 10 minutes, making this huge enough that it should never interfere with the `wait_for_completion_timeout` param, which is what should be controlling the timeout of the search request. } ); @@ -218,7 +197,11 @@ export const enhancedEsSearchStrategyProvider = ( if (request.indexType === DataViewType.ROLLUP && deps.rollupsEnabled) { return from(rollupSearch(request, options, deps)); } else { - return asyncSearch(request, options, deps); + try { + return asyncSearch(request, options, deps); + } catch (e) { + throw getKbnSearchError(e); + } } }, /** diff --git a/src/platform/plugins/shared/data/server/search/strategies/ese_search/response_utils.ts b/src/platform/plugins/shared/data/server/search/strategies/ese_search/response_utils.ts index 0db970e2d9704..8b2033c6ef15f 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/ese_search/response_utils.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/ese_search/response_utils.ts @@ -12,26 +12,9 @@ import type { IKibanaSearchResponse } from '@kbn/search-types'; import type { IncomingHttpHeaders } from 'http'; import type { AsyncSearchResponse } from './types'; import { sanitizeRequestParams } from '../../sanitize_request_params'; -import type { AsyncSearchStatusResponse } from './types'; import type { IAsyncSearchOptions } from '../../../../common'; import { shimHitsTotal, getTotalLoaded } from '../../../../common'; -/** - * Get the Kibana representation of an async search status response. - */ -export function toAsyncKibanaSearchStatusResponse( - response: AsyncSearchStatusResponse, - warning?: string -): IKibanaSearchResponse { - return { - id: response.id, - rawResponse: {}, - isPartial: response.is_partial, - isRunning: response.is_running, - ...(warning ? { warning } : {}), - }; -} - /** * Get the Kibana representation of an async search response. */ diff --git a/src/platform/plugins/shared/data/server/search/strategies/ese_search/types.ts b/src/platform/plugins/shared/data/server/search/strategies/ese_search/types.ts index 58a56cb1f33e4..f1fed51c5675e 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/ese_search/types.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/ese_search/types.ts @@ -7,11 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { - AsyncSearchGetRequest, - SearchResponse, - ShardStatistics, -} from '@elastic/elasticsearch/lib/api/types'; +import type { AsyncSearchGetRequest, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ISearchRequestParams } from '@kbn/search-types'; export interface IAsyncSearchRequestParams extends ISearchRequestParams { @@ -27,7 +23,3 @@ export interface AsyncSearchResponse { is_partial: boolean; is_running: boolean; } -export interface AsyncSearchStatusResponse extends Omit { - completion_status?: number; - _shards: ShardStatistics; -} diff --git a/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.test.ts b/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.test.ts index 3838d1bf638f1..606aa93d8a722 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.test.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.test.ts @@ -175,7 +175,7 @@ describe('ES|QL async search strategy', () => { expect.objectContaining({ id: 'foo', keep_alive: '60000ms', - wait_for_completion_timeout: '100ms', + wait_for_completion_timeout: '2000ms', }), expect.objectContaining({ maxRetries: 1, meta: true, signal: undefined }) ); @@ -197,13 +197,15 @@ describe('ES|QL async search strategy', () => { expect(request).toHaveProperty('keep_alive'); }); - it('calls asyncQueryStop with the given ID when using options.retrieveResults: true', async () => { + it('calls asyncQueryStop with the given ID when using options.returnIntermediateResults: true', async () => { mockAsyncQueryStop.mockResolvedValueOnce(mockAsyncResponse); const id = 'FlBvQU5CS3BKVEdPcWM1V2lkYXNUbXccVmNhQl9wcWFRdG1WYzE4N2tsOFNNdzozNjMzOQ=='; const params = { query: 'from logs* | limit 10' }; const esSearch = await esqlAsyncSearchStrategyProvider(mockSearchConfig, mockLogger); - await esSearch.search({ id, params }, { retrieveResults: true }, mockDeps).toPromise(); + await esSearch + .search({ id, params }, { returnIntermediateResults: true }, mockDeps) + .toPromise(); expect(mockAsyncQueryStop).toBeCalled(); const request = mockAsyncQueryStop.mock.calls[0][0]; diff --git a/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.ts b/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.ts index 69242b24359cf..d0b505da3b116 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/esql_async_search_strategy.ts @@ -94,6 +94,7 @@ export const esqlAsyncSearchStrategyProvider = ( signal: options.abortSignal, meta: true, asStream: options.stream, + requestTimeout: 600_000, // 10 minutes, making this huge enough that it should never interfere with the `wait_for_completion_timeout` param, which is what should be controlling the timeout of the search request. } ); } @@ -136,8 +137,8 @@ export const esqlAsyncSearchStrategyProvider = ( const { abortSignal, ...options } = searchOptions; const search = async () => { const response = await (!id - ? submitEsqlSearch(request, options, deps) - : options.retrieveResults + ? submitEsqlSearch({ id, ...request }, options, deps) + : options.returnIntermediateResults ? stopEsqlAsyncSearch(id, options, deps) : getEsqlAsyncSearch({ id, ...request }, options, deps)); diff --git a/src/platform/plugins/shared/data/server/search/strategies/sql_search/request_utils.test.ts b/src/platform/plugins/shared/data/server/search/strategies/sql_search/request_utils.test.ts index 2d422f14dcb5d..75340e9058772 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/sql_search/request_utils.test.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/sql_search/request_utils.test.ts @@ -97,7 +97,7 @@ describe('request utils', () => { }); describe('getDefaultAsyncGetParams', () => { - test('Uses `wait_for_completion_timeout`', async () => { + test('Uses `wait_for_completion_timeout` from pollLength', async () => { const mockConfig = getMockSearchConfig({ sessions: { defaultExpiration: moment.duration(3, 'd'), @@ -105,7 +105,7 @@ describe('request utils', () => { }, }); const params = getDefaultAsyncGetParams(mockConfig, {}); - expect(params).toHaveProperty('wait_for_completion_timeout'); + expect(params).toHaveProperty('wait_for_completion_timeout', '2000ms'); }); test('Uses `keep_alive` if `sessionId` is not provided', async () => { diff --git a/src/platform/plugins/shared/data/server/search/strategies/sql_search/sql_search_strategy.test.ts b/src/platform/plugins/shared/data/server/search/strategies/sql_search/sql_search_strategy.test.ts index 824775644a8fb..75df5dd50049d 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/sql_search/sql_search_strategy.test.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/sql_search/sql_search_strategy.test.ts @@ -117,7 +117,7 @@ describe('SQL search strategy', () => { format: 'json', id: 'foo', keep_alive: '60000ms', - wait_for_completion_timeout: '100ms', + wait_for_completion_timeout: '2000ms', }); expect(searchOptions).toEqual({ meta: true, diff --git a/x-pack/platform/plugins/shared/alerting/server/lib/wrap_async_search_client.test.ts b/x-pack/platform/plugins/shared/alerting/server/lib/wrap_async_search_client.test.ts index 8c577e162bf28..3c4c185d2bc70 100644 --- a/x-pack/platform/plugins/shared/alerting/server/lib/wrap_async_search_client.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/lib/wrap_async_search_client.test.ts @@ -79,7 +79,7 @@ describe('wrapScopedClusterClient', () => { request: { params: { query: '', filter: '', keep_alive: '10m' }, }, - options: { retrieveResults: true }, + options: { returnIntermediateResults: true }, }); expect(client.search).toHaveBeenCalledWith( @@ -87,7 +87,7 @@ describe('wrapScopedClusterClient', () => { { abortSignal: abortController.signal, strategy: ESQL_ASYNC_SEARCH_STRATEGY, - retrieveResults: true, + returnIntermediateResults: true, } ); }); @@ -119,7 +119,7 @@ describe('wrapScopedClusterClient', () => { request: { params: { query: '', filter: '', keep_alive: '10m', wait_for_completion_timeout: '10m' }, }, - options: { retrieveResults: true }, + options: { returnIntermediateResults: true }, }); expect(response).toEqual({ took: 1, columns: [], values: [] }); diff --git a/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entries_search_strategy.ts b/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entries_search_strategy.ts index a054900d6b9ce..4354a2df75df5 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entries_search_strategy.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entries_search_strategy.ts @@ -122,7 +122,7 @@ export const logEntriesSearchStrategyProvider = ({ esRequest, { ...options, - retrieveResults: true, // the subsequent processing requires the actual search results + returnIntermediateResults: true, // the subsequent processing requires the actual search results }, dependencies ) diff --git a/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entry_search_strategy.ts index d9f96f5f5340f..8160c6ca2bd58 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entry_search_strategy.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/services/log_entries/log_entry_search_strategy.ts @@ -90,7 +90,7 @@ export const logEntrySearchStrategyProvider = ({ esRequest, { ...options, - retrieveResults: true, // without it response will not contain progress information + returnIntermediateResults: true, // without it response will not contain progress information }, dependencies ) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_routing/state_management/stream_routing_state_machine/routing_samples_state_machine.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_routing/state_management/stream_routing_state_machine/routing_samples_state_machine.ts index a90f4abb108d8..dbfa08cd7b7e7 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_routing/state_management/stream_routing_state_machine/routing_samples_state_machine.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_routing/state_management/stream_routing_state_machine/routing_samples_state_machine.ts @@ -358,7 +358,7 @@ function collectDocuments({ let registerFetchLatency: () => void = () => {}; const subscription = data.search - .search({ params }, { abortSignal: abortController.signal, retrieveResults: true }) + .search({ params }, { abortSignal: abortController.signal, returnIntermediateResults: true }) .pipe( tap({ subscribe: () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/solutions/security/plugins/security_solution/server/search_strategy/security_solution/index.ts index d9bec3913d71d..ae23a5a36e7d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -30,7 +30,7 @@ export const securitySolutionSearchStrategyProvider = ( const parsedRequest = searchStrategyRequestSchema.parse(request); const queryFactory = securitySolutionFactory[parsedRequest.factoryQueryType]; // NOTE: without this parameter, .hits.hits can be empty - options.retrieveResults = true; + options.returnIntermediateResults = true; const dsl = queryFactory.buildDsl(parsedRequest); return es.search({ ...request, params: dsl }, options, deps).pipe( diff --git a/x-pack/solutions/security/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/solutions/security/plugins/timelines/server/search_strategy/timeline/index.ts index 03c0b633fe86f..02e8dd45aa6db 100644 --- a/x-pack/solutions/security/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/solutions/security/plugins/timelines/server/search_strategy/timeline/index.ts @@ -83,7 +83,7 @@ const timelineSearchStrategy = ({ logger: Logger; }) => { // NOTE: without this parameter, .hits.hits can be empty - options.retrieveResults = true; + options.returnIntermediateResults = true; const dsl = queryFactory.buildDsl(request); return es.search({ ...request, params: dsl }, options, deps).pipe( @@ -111,7 +111,7 @@ const timelineSessionsSearchStrategy = ({ queryFactory: TimelineFactory; }) => { // NOTE: without this parameter, .hits.hits can be empty - options.retrieveResults = true; + options.returnIntermediateResults = true; const indices = request.defaultIndex ?? request.indexType; const requestSessionLeaders = {