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 7b667fa7cf91e..d356581f27694 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 @@ -32,7 +32,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { AsyncSearchGetResponse, ErrorResponseBase, - SqlGetAsyncResponse, + EsqlAsyncQueryResponse, } from '@elastic/elasticsearch/lib/api/types'; import { i18n } from '@kbn/i18n'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -549,7 +549,7 @@ export class SearchInterceptor { ...getTotalLoaded(shimmedResponse), }; case ESQL_ASYNC_SEARCH_STRATEGY: - const esqlResponse = rawResponse.body as unknown as SqlGetAsyncResponse; + const esqlResponse = rawResponse.body as unknown as EsqlAsyncQueryResponse; return { id: esqlResponse.id, rawResponse: esqlResponse, 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 2411132db9bdf..3838d1bf638f1 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 @@ -36,9 +36,13 @@ const mockAsyncResponse = { }; describe('ES|QL async search strategy', () => { - const mockApiCaller = jest.fn(); + const mockAsyncQuery = jest.fn(); + const mockAsyncQueryGet = jest.fn(); + const mockAsyncQueryDelete = jest.fn(); + const mockAsyncQueryStop = jest.fn(); const mockLogger: any = { debug: () => {}, + error: () => {}, }; const mockDeps = { uiSettingsClient: { @@ -46,7 +50,12 @@ describe('ES|QL async search strategy', () => { }, esClient: { asCurrentUser: { - transport: { request: mockApiCaller }, + esql: { + asyncQuery: mockAsyncQuery, + asyncQueryGet: mockAsyncQueryGet, + asyncQueryDelete: mockAsyncQueryDelete, + asyncQueryStop: mockAsyncQueryStop, + }, }, }, } as unknown as SearchStrategyDependencies; @@ -54,7 +63,10 @@ describe('ES|QL async search strategy', () => { const mockSearchConfig = getMockSearchConfig({}); beforeEach(() => { - mockApiCaller.mockClear(); + mockAsyncQuery.mockClear(); + mockAsyncQueryGet.mockClear(); + mockAsyncQueryDelete.mockClear(); + mockAsyncQueryStop.mockClear(); }); it('returns a strategy with `search and `cancel`', async () => { @@ -66,7 +78,7 @@ describe('ES|QL async search strategy', () => { describe('search', () => { describe('no sessionId', () => { it('makes a POST request with params when no ID provided', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQuery.mockResolvedValueOnce(mockAsyncResponse); const esSearch = await esqlAsyncSearchStrategyProvider(mockSearchConfig, mockLogger); const params = { @@ -83,14 +95,14 @@ describe('ES|QL async search strategy', () => { ) .toPromise(); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0].body; + expect(mockAsyncQuery).toBeCalled(); + const request = mockAsyncQuery.mock.calls[0][0]; expect(request.query).toEqual(params.query); expect(request).toHaveProperty('keep_alive', '60000ms'); }); it('makes a GET request to async search with ID', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQueryGet.mockResolvedValueOnce(mockAsyncResponse); const params = { query: 'from logs* | limit 10', @@ -99,15 +111,15 @@ describe('ES|QL async search strategy', () => { await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0]; - expect(request.path).toContain('foo'); - expect(request.querystring).toHaveProperty('wait_for_completion_timeout'); - expect(request.querystring).toHaveProperty('keep_alive', '60000ms'); + expect(mockAsyncQueryGet).toBeCalled(); + const request = mockAsyncQueryGet.mock.calls[0][0]; + expect(request.id).toBe('foo'); + expect(request).toHaveProperty('wait_for_completion_timeout'); + expect(request).toHaveProperty('keep_alive', '60000ms'); }); it('allows overriding keep_alive and wait_for_completion_timeout', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQueryGet.mockResolvedValueOnce(mockAsyncResponse); const params = { query: 'from logs* | limit 10', @@ -118,16 +130,16 @@ describe('ES|QL async search strategy', () => { await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0]; - expect(request.path).toContain('foo'); - expect(request.querystring).toHaveProperty('wait_for_completion_timeout', '10s'); - expect(request.querystring).toHaveProperty('keep_alive', '5m'); + expect(mockAsyncQueryGet).toBeCalled(); + const request = mockAsyncQueryGet.mock.calls[0][0]; + expect(request.id).toBe('foo'); + expect(request).toHaveProperty('wait_for_completion_timeout', '10s'); + expect(request).toHaveProperty('keep_alive', '5m'); }); it('sets transport options on POST requests', async () => { const transportOptions = { maxRetries: 1 }; - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQuery.mockResolvedValueOnce(mockAsyncResponse); const params = { query: 'from logs' }; const esSearch = esqlAsyncSearchStrategyProvider(mockSearchConfig, mockLogger); @@ -135,24 +147,20 @@ describe('ES|QL async search strategy', () => { esSearch.search({ params }, { transport: transportOptions }, mockDeps) ); - expect(mockApiCaller).toHaveBeenNthCalledWith( + expect(mockAsyncQuery).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - method: 'POST', - path: '/_query/async', - body: { - keep_alive: '60000ms', - wait_for_completion_timeout: '100ms', - keep_on_completion: false, - query: 'from logs', - }, + keep_alive: '60000ms', + wait_for_completion_timeout: '100ms', + keep_on_completion: false, + query: 'from logs', }), expect.objectContaining({ maxRetries: 1, meta: true, signal: undefined }) ); }); it('sets transport options on GET requests', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQueryGet.mockResolvedValueOnce(mockAsyncResponse); const params = { query: 'from logs* | limit 10', }; @@ -162,21 +170,19 @@ describe('ES|QL async search strategy', () => { esSearch.search({ id: 'foo', params }, { transport: { maxRetries: 1 } }, mockDeps) ); - expect(mockApiCaller).toHaveBeenNthCalledWith( + expect(mockAsyncQueryGet).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - path: '/_query/async/foo', - querystring: { - keep_alive: '60000ms', - wait_for_completion_timeout: '100ms', - }, + id: 'foo', + keep_alive: '60000ms', + wait_for_completion_timeout: '100ms', }), expect.objectContaining({ maxRetries: 1, meta: true, signal: undefined }) ); }); it('sets wait_for_completion_timeout and keep_alive in the request', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQuery.mockResolvedValueOnce(mockAsyncResponse); const params = { query: 'from logs* | limit 10', @@ -185,29 +191,27 @@ describe('ES|QL async search strategy', () => { await esSearch.search({ params }, {}, mockDeps).toPromise(); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0].body; + expect(mockAsyncQuery).toBeCalled(); + const request = mockAsyncQuery.mock.calls[0][0]; expect(request).toHaveProperty('wait_for_completion_timeout'); expect(request).toHaveProperty('keep_alive'); }); - it('calls /stop with the given ID when using options.retrieveResults: true', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + it('calls asyncQueryStop with the given ID when using options.retrieveResults: 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(); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0]; - expect(request.path).toEqual( - '/_query/async/FlBvQU5CS3BKVEdPcWM1V2lkYXNUbXccVmNhQl9wcWFRdG1WYzE4N2tsOFNNdzozNjMzOQ==/stop' - ); + expect(mockAsyncQueryStop).toBeCalled(); + const request = mockAsyncQueryStop.mock.calls[0][0]; + expect(request.id).toEqual(id); }); it('should delete when aborted', async () => { - mockApiCaller.mockResolvedValueOnce({ + mockAsyncQuery.mockResolvedValueOnce({ ...mockAsyncResponse, body: { ...mockAsyncResponse.body, @@ -231,9 +235,9 @@ describe('ES|QL async search strategy', () => { } catch (e) { err = e; } - expect(mockApiCaller).toBeCalled(); + expect(mockAsyncQuery).toBeCalled(); expect(err).not.toBeUndefined(); - expect(mockApiCaller).toBeCalled(); + expect(mockAsyncQuery).toBeCalled(); }); }); @@ -246,7 +250,7 @@ describe('ES|QL async search strategy', () => { meta: {} as any, }); - mockApiCaller.mockRejectedValue(errResponse); + mockAsyncQuery.mockRejectedValue(errResponse); const params = { query: 'from logs* | limit 10', @@ -259,7 +263,7 @@ describe('ES|QL async search strategy', () => { } catch (e) { err = e; } - expect(mockApiCaller).toBeCalled(); + expect(mockAsyncQuery).toBeCalled(); expect(err).toBeInstanceOf(KbnSearchError); expect(err?.statusCode).toBe(404); expect(err?.message).toBe(errResponse.message); @@ -269,7 +273,7 @@ describe('ES|QL async search strategy', () => { it('throws normalized error if Error is thrown', async () => { const errResponse = new Error('not good'); - mockApiCaller.mockRejectedValue(errResponse); + mockAsyncQuery.mockRejectedValue(errResponse); const params = { query: 'from logs* | limit 10', @@ -282,7 +286,7 @@ describe('ES|QL async search strategy', () => { } catch (e) { err = e; } - expect(mockApiCaller).toBeCalled(); + expect(mockAsyncQuery).toBeCalled(); expect(err).toBeInstanceOf(KbnSearchError); expect(err?.statusCode).toBe(500); expect(err?.message).toBe(errResponse.message); @@ -292,16 +296,16 @@ describe('ES|QL async search strategy', () => { describe('cancel', () => { it('makes a DELETE request to async search with the provided ID', async () => { - mockApiCaller.mockResolvedValueOnce(200); + mockAsyncQueryDelete.mockResolvedValueOnce(200); const id = 'some_id'; const esSearch = await esqlAsyncSearchStrategyProvider(mockSearchConfig, mockLogger); await esSearch.cancel!(id, {}, mockDeps); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0]; - expect(request.path).toContain(id); + expect(mockAsyncQueryDelete).toBeCalled(); + const request = mockAsyncQueryDelete.mock.calls[0][0]; + expect(request.id).toBe(id); }); it('throws normalized error on ResponseError', async () => { @@ -312,7 +316,7 @@ describe('ES|QL async search strategy', () => { warnings: [], meta: {} as any, }); - mockApiCaller.mockRejectedValue(errResponse); + mockAsyncQueryDelete.mockRejectedValue(errResponse); const id = 'some_id'; const esSearch = await esqlAsyncSearchStrategyProvider(mockSearchConfig, mockLogger); @@ -324,7 +328,7 @@ describe('ES|QL async search strategy', () => { err = e; } - expect(mockApiCaller).toBeCalled(); + expect(mockAsyncQueryDelete).toBeCalled(); expect(err).toBeInstanceOf(KbnServerError); expect(err?.statusCode).toBe(400); expect(err?.message).toBe(errResponse.message); @@ -334,7 +338,7 @@ describe('ES|QL async search strategy', () => { describe('extend', () => { it('makes a GET request to async search with the provided ID and keepAlive', async () => { - mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + mockAsyncQueryGet.mockResolvedValueOnce(mockAsyncResponse); const id = 'some_other_id'; const keepAlive = '1d'; @@ -342,14 +346,14 @@ describe('ES|QL async search strategy', () => { await esSearch.extend!(id, keepAlive, {}, mockDeps); - expect(mockApiCaller).toBeCalled(); - const request = mockApiCaller.mock.calls[0][0]; - expect(request.querystring).toEqual({ id, keep_alive: keepAlive }); + expect(mockAsyncQueryGet).toBeCalled(); + const request = mockAsyncQueryGet.mock.calls[0][0]; + expect(request).toEqual({ id, keep_alive: keepAlive }); }); it('throws normalized error on ElasticsearchClientError', async () => { const errResponse = new errors.ElasticsearchClientError('something is wrong with EsClient'); - mockApiCaller.mockRejectedValue(errResponse); + mockAsyncQueryGet.mockRejectedValue(errResponse); const id = 'some_other_id'; const keepAlive = '1d'; @@ -362,7 +366,7 @@ describe('ES|QL async search strategy', () => { err = e; } - expect(mockApiCaller).toBeCalled(); + expect(mockAsyncQueryGet).toBeCalled(); expect(err).toBeInstanceOf(KbnServerError); expect(err?.statusCode).toBe(500); expect(err?.message).toBe(errResponse.message); 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 1c8221521d377..69242b24359cf 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 @@ -12,8 +12,9 @@ import { catchError, tap } from 'rxjs'; import { getKbnServerError } from '@kbn/kibana-utils-plugin/server'; import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; import type { SqlQueryRequest } from '@elastic/elasticsearch/lib/api/types'; -import type { SqlGetAsyncResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { EsqlAsyncQueryResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ESQLSearchParams } from '@kbn/es-types'; +import type { WithRequiredProperty } from '@kbn/utility-types'; import { toAsyncKibanaSearchResponse } from './response_utils'; import { getCommonDefaultAsyncSubmitParams, @@ -36,17 +37,14 @@ export const esqlAsyncSearchStrategyProvider = ( logger: Logger ): ISearchStrategy< IKibanaSearchRequest, - IKibanaSearchResponse + IKibanaSearchResponse > => { function cancelEsqlAsyncSearch( id: string, { esClient }: Pick ) { - return esClient.asCurrentUser.transport.request( - { - method: 'DELETE', - path: `/_query/async/${id}`, - }, + return esClient.asCurrentUser.esql.asyncQueryDelete( + { id }, { meta: true, // we don't want the ES client to retry (default value is 3) @@ -60,11 +58,8 @@ export const esqlAsyncSearchStrategyProvider = ( options: IAsyncSearchOptions, { esClient }: Pick ) { - return esClient.asCurrentUser.transport.request( - { - method: 'POST', - path: `/_query/async/${id}/stop`, - }, + return esClient.asCurrentUser.esql.asyncQueryStop( + { id }, { ...options.transport, signal: options.abortSignal, @@ -75,7 +70,7 @@ export const esqlAsyncSearchStrategyProvider = ( } function getEsqlAsyncSearch( - { id, ...request }: IKibanaSearchRequest, + { id, ...request }: WithRequiredProperty, 'id'>, options: IAsyncSearchOptions, { esClient }: SearchStrategyDependencies ) { @@ -87,12 +82,12 @@ export const esqlAsyncSearchStrategyProvider = ( : {}), }; - return esClient.asCurrentUser.transport.request( + return esClient.asCurrentUser.esql.asyncQueryGet( { - method: 'GET', - path: `/_query/async/${id}`, + id, + ...params, // FIXME: the drop_null_columns param shouldn't be needed here once https://github.com/elastic/elasticsearch/issues/138439 is resolved - querystring: { ...params, drop_null_columns: request.params?.dropNullColumns }, + drop_null_columns: request.params?.dropNullColumns, }, { ...options.transport, @@ -104,23 +99,22 @@ export const esqlAsyncSearchStrategyProvider = ( } async function submitEsqlSearch( - { id, ...request }: IKibanaSearchRequest, + request: IKibanaSearchRequest, options: IAsyncSearchOptions, { esClient }: SearchStrategyDependencies ) { - const { dropNullColumns, ...requestParams } = request.params ?? {}; + if (!request.params) throw new Error('Missing request params'); + const { dropNullColumns, ...requestParams } = request.params; const params = { ...(await getCommonDefaultAsyncSubmitParams(searchConfig, options)), ...requestParams, }; - return esClient.asCurrentUser.transport.request( + return esClient.asCurrentUser.esql.asyncQuery( { - method: 'POST', - path: `/_query/async`, - body: params, - querystring: dropNullColumns ? 'drop_null_columns' : '', + ...params, + ...(dropNullColumns ? { drop_null_columns: true } : {}), }, { ...options.transport, @@ -142,14 +136,18 @@ export const esqlAsyncSearchStrategyProvider = ( const { abortSignal, ...options } = searchOptions; const search = async () => { const response = await (!id - ? submitEsqlSearch({ id, ...request }, options, deps) + ? submitEsqlSearch(request, options, deps) : options.retrieveResults ? stopEsqlAsyncSearch(id, options, deps) : getEsqlAsyncSearch({ id, ...request }, options, deps)); const { body, headers, meta } = response; - return toAsyncKibanaSearchResponse(body, headers, meta?.request?.params); + return toAsyncKibanaSearchResponse( + body as EsqlAsyncQueryResponse, // We can remove this cast after https://github.com/elastic/elasticsearch-js/issues/3215 + headers, + meta?.request?.params + ); }; const cancel = async () => { @@ -181,7 +179,7 @@ export const esqlAsyncSearchStrategyProvider = ( * @param request * @param options * @param deps `SearchStrategyDependencies` - * @returns `Observable>` + * @returns `Observable>` * @throws `KbnSearchError` */ search: (request, options: IAsyncSearchOptions, deps) => { @@ -218,12 +216,8 @@ export const esqlAsyncSearchStrategyProvider = ( extend: async (id, keepAlive, options, { esClient }) => { logger.debug(`extend ${id} by ${keepAlive}`); try { - await esClient.asCurrentUser.transport.request( - { - method: 'GET', - path: `/_query/async/${id}`, - querystring: { id, keep_alive: keepAlive }, - }, + await esClient.asCurrentUser.esql.asyncQueryGet( + { id, keep_alive: keepAlive }, { ...options.transport, signal: options.abortSignal, meta: true } ); } catch (e) { diff --git a/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/response_utils.ts b/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/response_utils.ts index 5ff4a51046558..3654dbdd097f3 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/response_utils.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/esql_async_search/response_utils.ts @@ -8,7 +8,7 @@ */ import type { ConnectionRequestParams } from '@elastic/transport'; -import type { SqlGetAsyncResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { EsqlAsyncQueryResponse } from '@elastic/elasticsearch/lib/api/types'; import type { IKibanaSearchResponse } from '@kbn/search-types'; import type { IncomingHttpHeaders } from 'http'; import { sanitizeRequestParams } from '../../sanitize_request_params'; @@ -17,10 +17,10 @@ import { sanitizeRequestParams } from '../../sanitize_request_params'; * Get the Kibana representation of an async search response (see `IKibanaSearchResponse`). */ export function toAsyncKibanaSearchResponse( - response: SqlGetAsyncResponse, + response: EsqlAsyncQueryResponse, headers: IncomingHttpHeaders, requestParams?: ConnectionRequestParams -): IKibanaSearchResponse { +): IKibanaSearchResponse { const responseIsStream = response.id === undefined; return { id: responseIsStream ? (headers['x-elasticsearch-async-id'] as string) : response.id, diff --git a/src/platform/plugins/shared/data/server/search/strategies/esql_search/esql_search_strategy.ts b/src/platform/plugins/shared/data/server/search/strategies/esql_search/esql_search_strategy.ts index 53c44152e9cdf..6c52fcc174aaf 100644 --- a/src/platform/plugins/shared/data/server/search/strategies/esql_search/esql_search_strategy.ts +++ b/src/platform/plugins/shared/data/server/search/strategies/esql_search/esql_search_strategy.ts @@ -38,14 +38,10 @@ export const esqlSearchStrategyProvider = ( // `columns` contain only columns with data // `all_columns` contain everything const { terminateAfter, dropNullColumns, ...requestParams } = request.params ?? {}; - const { headers, body, meta } = await esClient.asCurrentUser.transport.request( + const { headers, body, meta } = await esClient.asCurrentUser.esql.query( { - method: 'POST', - path: `/_query`, - querystring: dropNullColumns ? 'drop_null_columns' : '', - body: { - ...requestParams, - }, + ...requestParams, + ...(dropNullColumns ? { drop_null_columns: true } : {}), }, { signal: abortSignal, diff --git a/src/platform/test/functional/apps/discover/esql/_esql_view.ts b/src/platform/test/functional/apps/discover/esql/_esql_view.ts index d5d4156ff6053..5174cc5799964 100644 --- a/src/platform/test/functional/apps/discover/esql/_esql_view.ts +++ b/src/platform/test/functional/apps/discover/esql/_esql_view.ts @@ -407,7 +407,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(requestNames).to.contain('Table'); expect(requestNames).to.contain('Visualization'); const request = await inspector.getRequest(1); - expect(request.command).to.be('POST /_query/async?drop_null_columns'); + expect(request.command).to.be('POST /_query/async?drop_null_columns=true'); }); }); diff --git a/x-pack/platform/plugins/shared/maps/server/mvt/mvt_routes.ts b/x-pack/platform/plugins/shared/maps/server/mvt/mvt_routes.ts index 83bcf38ebc774..be6f20d415f0d 100644 --- a/x-pack/platform/plugins/shared/maps/server/mvt/mvt_routes.ts +++ b/x-pack/platform/plugins/shared/maps/server/mvt/mvt_routes.ts @@ -230,6 +230,7 @@ async function getTile({ method: 'POST', path, body, + querystring: { project_routing: '_alias:_origin' }, }, { signal: abortController.signal, diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/server/functions/elasticsearch.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/server/functions/elasticsearch.ts index bbb4cfc3e1b53..fcbe3d1c0fd1e 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/server/functions/elasticsearch.ts +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/server/functions/elasticsearch.ts @@ -55,7 +55,11 @@ export function registerElasticsearchFunction({ const response = await esClient.asCurrentUser.transport.request({ method, path, - body, + // POST _search: inject project_routing for CPS (stripped automatically when CPS is disabled) + body: + isSearchEndpoint && method === 'POST' + ? { project_routing: '_alias:_origin', ...body } + : body, }); return { content: { response } }; diff --git a/x-pack/platform/test/serverless/functional/test_suites/discover/esql/_esql_view.ts b/x-pack/platform/test/serverless/functional/test_suites/discover/esql/_esql_view.ts index c1bcd57753359..7e4daccee855d 100644 --- a/x-pack/platform/test/serverless/functional/test_suites/discover/esql/_esql_view.ts +++ b/x-pack/platform/test/serverless/functional/test_suites/discover/esql/_esql_view.ts @@ -381,7 +381,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(requestNames).to.contain('Table'); expect(requestNames).to.contain('Visualization'); const request = await inspector.getRequest(1); - expect(request.command).to.be('POST /_query/async?drop_null_columns'); + expect(request.command).to.be('POST /_query/async?drop_null_columns=true'); }); }); }); diff --git a/x-pack/solutions/search/plugins/search_playground/server/lib/conversational_chain.test.ts b/x-pack/solutions/search/plugins/search_playground/server/lib/conversational_chain.test.ts index 89e925b29f7c7..1fea969f0c4a1 100644 --- a/x-pack/solutions/search/plugins/search_playground/server/lib/conversational_chain.test.ts +++ b/x-pack/solutions/search/plugins/search_playground/server/lib/conversational_chain.test.ts @@ -108,6 +108,7 @@ describe('conversational chain', () => { }); const mockElasticsearchClient = { + search: searchMock, transport: { request: searchMock, }, @@ -197,9 +198,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'what is the work from home policy?' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'what is the work from home policy?' } }, + size: 3, }, ], }); @@ -231,9 +232,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'what is the work from home policy?' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'what is the work from home policy?' } }, + size: 3, }, ], contentField: { index: 'field', website: 'metadata.source' }, @@ -270,9 +271,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'what is the work from home policy?' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'what is the work from home policy?' } }, + size: 3, }, ], contentField: { index: 'field' }, @@ -309,9 +310,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'what is the work from home policy?' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'what is the work from home policy?' } }, + size: 3, }, ], }); @@ -353,9 +354,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'rewrite the question' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'rewrite the question' } }, + size: 3, }, ], }); @@ -392,9 +393,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'what is the work from home policy?' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'what is the work from home policy?' } }, + size: 3, }, ], }); @@ -436,9 +437,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'rewrite "the" question' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'rewrite "the" question' } }, + size: 3, }, ], }); @@ -480,9 +481,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'rewrite "the" question' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'rewrite "the" question' } }, + size: 3, }, ], isChatModel: false, @@ -540,9 +541,9 @@ describe('conversational chain', () => { ], expectedSearchRequest: [ { - method: 'POST', - path: '/index,website/_search', - body: { query: { match: { field: 'rewrite "the" question' } }, size: 3 }, + index: 'index,website', + query: { match: { field: 'rewrite "the" question' } }, + size: 3, }, ], expectedTokens: [], diff --git a/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts b/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts index 662d611140fab..5030e2d31471c 100644 --- a/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts +++ b/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts @@ -8,11 +8,7 @@ import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers'; import type { Document } from '@langchain/core/documents'; import type { ElasticsearchClient } from '@kbn/core/server'; -import type { - AggregationsAggregate, - SearchHit, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/types'; +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { contextDocumentHitMapper } from '../utils/context_document_mapper'; import type { ElasticsearchRetrieverContentField } from '../types'; @@ -71,14 +67,11 @@ export class ElasticsearchRetriever extends BaseRetriever { try { const queryBody = this.query_body_fn(query); - const results = (await this.client.transport.request({ - method: 'POST', - path: `/${this.index}/_search`, - body: { - ...queryBody, - size: this.k, - }, - })) as SearchResponse>; + const results = await this.client.search({ + index: this.index, + ...queryBody, + size: this.k, + }); const hits = results.hits.hits; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.test.ts index 1894582827573..b83a6dfec7dc7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.test.ts @@ -61,7 +61,9 @@ describe('esqlExecutor', () => { describe('errors', () => { it('should return result with user error equal true when request fails with data verification exception', async () => { - ruleServices.scopedClusterClient.asCurrentUser.transport.request.mockRejectedValue( + ( + ruleServices.scopedClusterClient.asCurrentUser.esql.asyncQuery as unknown as jest.Mock + ).mockRejectedValue( new KbnServerError( 'verification_exception: Found 1 problem\nline 1:45: invalid [test_not_lookup] resolution in lookup mode to an index in [standard] mode', 400, @@ -91,7 +93,9 @@ describe('esqlExecutor', () => { }); it('should return result without user error when request fails with non-categorized error', async () => { - ruleServices.scopedClusterClient.asCurrentUser.transport.request.mockRejectedValue( + ( + ruleServices.scopedClusterClient.asCurrentUser.esql.asyncQuery as unknown as jest.Mock + ).mockRejectedValue( new KbnServerError('Unknown Error', 500, { error: { root_cause: [ @@ -138,9 +142,7 @@ describe('esqlExecutor', () => { expect(result.warningMessages).toContain( 'Excluded documents exceeded the limit of 100000, some alerts might not have been created. Consider reducing the lookback time for the rule.' ); - expect( - ruleServices.scopedClusterClient.asCurrentUser.transport.request - ).not.toHaveBeenCalled(); + expect(ruleServices.scopedClusterClient.asCurrentUser.esql.asyncQuery).not.toHaveBeenCalled(); }); it('should include documents ids from state in ES|QL request', async () => { @@ -159,15 +161,16 @@ describe('esqlExecutor', () => { }; await esqlExecutor(mockedArguments); - const transportRequestArgs = - ruleServices.scopedClusterClient.asCurrentUser.transport.request.mock.calls[0][0]; + const asyncQueryMock = ruleServices.scopedClusterClient.asCurrentUser.esql + .asyncQuery as unknown as jest.Mock; + const asyncQueryArgs = asyncQueryMock.mock.calls[0][0]; - expect(transportRequestArgs).toHaveProperty( - 'body.filter.bool.must_not.0.bool.filter.0.ids.values', - ['doc1', 'doc2'] - ); - expect(transportRequestArgs).toHaveProperty( - 'body.filter.bool.must_not.0.bool.filter.1.term._index', + expect(asyncQueryArgs).toHaveProperty('filter.bool.must_not.0.bool.filter.0.ids.values', [ + 'doc1', + 'doc2', + ]); + expect(asyncQueryArgs).toHaveProperty( + 'filter.bool.must_not.0.bool.filter.1.term._index', 'test_index_1' ); }); @@ -190,13 +193,14 @@ describe('esqlExecutor', () => { }; await esqlExecutor(mockedArguments); - const transportRequestArgs = - ruleServices.scopedClusterClient.asCurrentUser.transport.request.mock.calls[0][0]; + const asyncQueryMock = ruleServices.scopedClusterClient.asCurrentUser.esql + .asyncQuery as unknown as jest.Mock; + const asyncQueryArgs = asyncQueryMock.mock.calls[0][0]; - expect(transportRequestArgs).toHaveProperty( - 'body.filter.bool.must_not.0.bool.filter.0.ids.values', - ['doc1', 'doc2'] - ); + expect(asyncQueryArgs).toHaveProperty('filter.bool.must_not.0.bool.filter.0.ids.values', [ + 'doc1', + 'doc2', + ]); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.test.ts index 745e20a84026c..4148558d8e226 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.test.ts @@ -47,6 +47,9 @@ const requestQueryParams = { drop_null_columns: true }; describe('performEsqlRequest', () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); + const asyncQueryMock = esClient.esql.asyncQuery as unknown as jest.Mock; + const asyncQueryGetMock = esClient.esql.asyncQueryGet as unknown as jest.Mock; + const asyncQueryDeleteMock = esClient.esql.asyncQueryDelete as unknown as jest.Mock; const shouldStopExecution: jest.Mock = jest.fn(); shouldStopExecution.mockReturnValue(false); @@ -63,7 +66,7 @@ describe('performEsqlRequest', () => { values, }; - esClient.transport.request.mockResolvedValueOnce(mockResponse); + asyncQueryMock.mockResolvedValueOnce(mockResponse); const result = await performEsqlRequest({ esClient, @@ -73,17 +76,13 @@ describe('performEsqlRequest', () => { }); expect(result).toEqual(mockResponse); - expect(esClient.transport.request).toHaveBeenCalledTimes(2); - expect(esClient.transport.request).toHaveBeenCalledWith({ - method: 'POST', - path: '/_query/async', - body: requestBody, - querystring: requestQueryParams, - }); - expect(esClient.transport.request).toHaveBeenCalledWith({ - method: 'DELETE', - path: '/_query/async/QUERY-ID', + expect(asyncQueryMock).toHaveBeenCalledTimes(1); + expect(asyncQueryMock).toHaveBeenCalledWith({ + ...requestBody, + ...requestQueryParams, }); + expect(asyncQueryDeleteMock).toHaveBeenCalledTimes(1); + expect(asyncQueryDeleteMock).toHaveBeenCalledWith({ id: 'QUERY-ID' }); }); it('polls until the query is completed', async () => { @@ -101,9 +100,8 @@ describe('performEsqlRequest', () => { values, }; - esClient.transport.request - .mockResolvedValueOnce(mockSubmitResponse) - .mockResolvedValueOnce(mockPollResponse); + asyncQueryMock.mockResolvedValueOnce(mockSubmitResponse); + asyncQueryGetMock.mockResolvedValueOnce(mockPollResponse); const waitForPerformEsql = performEsqlRequest({ esClient, @@ -117,21 +115,15 @@ describe('performEsqlRequest', () => { const result = await waitForPerformEsql; expect(result).toEqual(mockPollResponse); - expect(esClient.transport.request).toHaveBeenCalledTimes(3); - expect(esClient.transport.request).toHaveBeenNthCalledWith(1, { - method: 'POST', - path: '/_query/async', - body: requestBody, - querystring: requestQueryParams, - }); - expect(esClient.transport.request).toHaveBeenNthCalledWith(2, { - method: 'GET', - path: '/_query/async/QUERY-ID', - }); - expect(esClient.transport.request).toHaveBeenCalledWith({ - method: 'DELETE', - path: '/_query/async/QUERY-ID', + expect(asyncQueryMock).toHaveBeenCalledTimes(1); + expect(asyncQueryMock).toHaveBeenCalledWith({ + ...requestBody, + ...requestQueryParams, }); + expect(asyncQueryGetMock).toHaveBeenCalledTimes(1); + expect(asyncQueryGetMock).toHaveBeenCalledWith({ id: 'QUERY-ID' }); + expect(asyncQueryDeleteMock).toHaveBeenCalledTimes(1); + expect(asyncQueryDeleteMock).toHaveBeenCalledWith({ id: 'QUERY-ID' }); }); it('throws an error if execution is cancelled', async () => { @@ -142,7 +134,8 @@ describe('performEsqlRequest', () => { values: [], }; - esClient.transport.request.mockResolvedValue(mockSubmitResponse); + asyncQueryMock.mockResolvedValue(mockSubmitResponse); + asyncQueryGetMock.mockResolvedValue(mockSubmitResponse); shouldStopExecution.mockReturnValue(true); const waitForPerformEsql = performEsqlRequest({ @@ -167,9 +160,8 @@ describe('performEsqlRequest', () => { values: [], }; - esClient.transport.request - .mockResolvedValueOnce(mockSubmitResponse) - .mockRejectedValueOnce(new Error('Test error')); + asyncQueryMock.mockResolvedValueOnce(mockSubmitResponse); + asyncQueryGetMock.mockRejectedValueOnce(new Error('Test error')); const waitForPerformEsql = performEsqlRequest({ esClient, @@ -183,10 +175,7 @@ describe('performEsqlRequest', () => { await jest.advanceTimersByTimeAsync(15000); await waitForPerformEsql; - expect(esClient.transport.request).toHaveBeenCalledWith({ - method: 'DELETE', - path: '/_query/async/QUERY-ID', - }); + expect(asyncQueryDeleteMock).toHaveBeenCalledWith({ id: 'QUERY-ID' }); expect.assertions(2); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.ts index e844ffcc88fee..c73d94f72070b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql_request.ts @@ -32,11 +32,6 @@ export interface EsqlResultColumn { type: 'date' | 'keyword'; } -type AsyncEsqlResponse = { - id: string; - is_running: boolean; -} & EsqlTable; - export type EsqlResultRow = Array; export interface EsqlTable { @@ -73,7 +68,7 @@ export const performEsqlRequest = async ({ }): Promise => { let pollInterval = 10 * 1000; // Poll every 10 seconds let pollCount = 0; - let queryId: string = ''; + let queryId: string | undefined; try { loggedRequests?.push({ @@ -82,19 +77,19 @@ export const performEsqlRequest = async ({ request_type: 'findMatches', }); const asyncSearchStarted = performance.now(); - const asyncEsqlResponse = await esClient.transport.request({ - method: 'POST', - path: '/_query/async', - body: requestBody, - querystring: requestQueryParams, + const asyncEsqlResponse = await esClient.esql.asyncQuery({ + ...requestBody, + ...requestQueryParams, }); setLatestRequestDuration(asyncSearchStarted, loggedRequests); - queryId = asyncEsqlResponse.id; - const isRunning = asyncEsqlResponse.is_running; - if (!isRunning) { - return asyncEsqlResponse; + if (!asyncEsqlResponse.is_running) { + return asyncEsqlResponse as EsqlTable; + } + + if (!queryId) { + throw new Error('Async ES|QL query is running but no query ID was returned'); } // Poll for long-executing query @@ -106,14 +101,13 @@ export const performEsqlRequest = async ({ description: i18n.ESQL_POLL_REQUEST_DESCRIPTION, }); const pollStarted = performance.now(); - const pollResponse = await esClient.transport.request({ - method: 'GET', - path: `/_query/async/${queryId}`, + const pollResponse = await esClient.esql.asyncQueryGet({ + id: queryId, }); setLatestRequestDuration(pollStarted, loggedRequests); if (!pollResponse.is_running) { - return pollResponse; + return pollResponse as EsqlTable; } pollCount++; @@ -140,10 +134,7 @@ export const performEsqlRequest = async ({ description: i18n.ESQL_DELETE_REQUEST_DESCRIPTION, }); const deleteStarted = performance.now(); - await esClient.transport.request({ - method: 'DELETE', - path: `/_query/async/${queryId}`, - }); + await esClient.esql.asyncQueryDelete({ id: queryId }); setLatestRequestDuration(deleteStarted, loggedRequests); } }