From a2b4517b9e762072ca246fa3619c9acb83eb4330 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 5 Jan 2021 08:26:06 -0700 Subject: [PATCH] [data.search.session] Add extend functionality to server-side search service (#86195) * [data.search.session] Store search strategy in saved object * [data.search.session] Add extend functionality * Update docs * Update unit test to check strategy * Throw kbnServerError instead of error * Fix test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...gins-data-server.isearchstrategy.extend.md | 11 ++ ...gin-plugins-data-server.isearchstrategy.md | 1 + src/plugins/data/common/search/types.ts | 12 ++ .../data/server/search/search_service.ts | 14 ++ src/plugins/data/server/search/types.ts | 6 + src/plugins/data/server/server.api.md | 4 +- .../server/search/eql_search_strategy.ts | 5 + .../server/search/es_search_strategy.test.ts | 136 +++++++++++------- .../server/search/es_search_strategy.ts | 4 + 9 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md new file mode 100644 index 0000000000000..65e3c2868f29f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.extend.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) > [extend](./kibana-plugin-plugins-data-server.isearchstrategy.extend.md) + +## ISearchStrategy.extend property + +Signature: + +```typescript +extend?: (id: string, keepAlive: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index c9f4c886735a7..c46a580d5ceb8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -17,5 +17,6 @@ export interface ISearchStrategy(id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise<void> | | +| [extend](./kibana-plugin-plugins-data-server.isearchstrategy.extend.md) | (id: string, keepAlive: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise<void> | | | [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable<SearchStrategyResponse> | | diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 695ee34d3b468..34e411aa85c80 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -29,10 +29,22 @@ export type ISearchGeneric = < ) => Observable; export type ISearchCancelGeneric = (id: string, options?: ISearchOptions) => Promise; +export type ISearchExtendGeneric = ( + id: string, + keepAlive: string, + options?: ISearchOptions +) => Promise; export interface ISearchClient { search: ISearchGeneric; + /** + * Used to cancel an in-progress search request. + */ cancel: ISearchCancelGeneric; + /** + * Used to extend the TTL of an in-progress search request. + */ + extend: ISearchExtendGeneric; } export interface IKibanaSearchResponse { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 5de019cd1b83e..d26c099b23f39 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -315,6 +315,19 @@ export class SearchService implements Plugin { return strategy.cancel(id, options, deps); }; + private extend = ( + id: string, + keepAlive: string, + options: ISearchOptions, + deps: SearchStrategyDependencies + ) => { + const strategy = this.getSearchStrategy(options.strategy); + if (!strategy.extend) { + throw new KbnServerError(`Search strategy ${options.strategy} does not support extend`, 400); + } + return strategy.extend(id, keepAlive, options, deps); + }; + private getSearchStrategy = < SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse @@ -344,6 +357,7 @@ export class SearchService implements Plugin { search: (searchRequest, options = {}) => this.search(scopedSession, searchRequest, options, deps), cancel: (id, options = {}) => this.cancel(id, options, deps), + extend: (id, keepAlive, options = {}) => this.extend(id, keepAlive, options, deps), }; }; }; diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index db8b8ac72d0e5..fb00f86464e4e 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -86,6 +86,12 @@ export interface ISearchStrategy< deps: SearchStrategyDependencies ) => Observable; cancel?: (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; + extend?: ( + id: string, + keepAlive: string, + options: ISearchOptions, + deps: SearchStrategyDependencies + ) => Promise; } export interface ISearchStart< diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index cd3527d5ad7ab..bb8ed92e9bfce 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -952,6 +952,8 @@ export interface ISearchStrategy Promise; // (undocumented) + extend?: (id: string, keepAlive: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; + // (undocumented) search: (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable; } @@ -1430,7 +1432,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:279: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:70:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:90:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:106:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:112:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index 26325afc378f7..e615d9d2a660a 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -56,5 +56,10 @@ export const eqlSearchStrategyProvider = ( return pollSearch(search, options).pipe(tap((response) => (id = response.id))); }, + + extend: async (id, keepAlive, options, { esClient }) => { + logger.debug(`_eql/extend ${id} by ${keepAlive}`); + await esClient.asCurrentUser.eql.get({ id, keep_alive: keepAlive }); + }, }; }; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index b9b6e25067f2f..3230895da7705 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -37,6 +37,7 @@ describe('ES search strategy', () => { const mockApiCaller = jest.fn(); const mockGetCaller = jest.fn(); const mockSubmitCaller = jest.fn(); + const mockDeleteCaller = jest.fn(); const mockLogger: any = { debug: () => {}, }; @@ -49,6 +50,7 @@ describe('ES search strategy', () => { asyncSearch: { get: mockGetCaller, submit: mockSubmitCaller, + delete: mockDeleteCaller, }, transport: { request: mockApiCaller }, }, @@ -66,77 +68,113 @@ describe('ES search strategy', () => { beforeEach(() => { mockApiCaller.mockClear(); + mockGetCaller.mockClear(); + mockSubmitCaller.mockClear(); + mockDeleteCaller.mockClear(); }); - it('returns a strategy with `search`', async () => { + it('returns a strategy with `search and `cancel`', async () => { const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); expect(typeof esSearch.search).toBe('function'); }); - it('makes a POST request to async search with params when no ID is provided', async () => { - mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + describe('search', () => { + it('makes a POST request to async search with params when no ID is provided', async () => { + mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); - const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search({ params }, {}, mockDeps).toPromise(); + await esSearch.search({ params }, {}, mockDeps).toPromise(); - expect(mockSubmitCaller).toBeCalled(); - const request = mockSubmitCaller.mock.calls[0][0]; - expect(request.index).toEqual(params.index); - expect(request.body).toEqual(params.body); - }); + expect(mockSubmitCaller).toBeCalled(); + const request = mockSubmitCaller.mock.calls[0][0]; + expect(request.index).toEqual(params.index); + expect(request.body).toEqual(params.body); + }); - it('makes a GET request to async search with ID when ID is provided', async () => { - mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); + it('makes a GET request to async search with ID when ID is provided', async () => { + mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); - const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const params = { index: 'logstash-*', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); + + expect(mockGetCaller).toBeCalled(); + const request = mockGetCaller.mock.calls[0][0]; + expect(request.id).toEqual('foo'); + expect(request).toHaveProperty('wait_for_completion_timeout'); + expect(request).toHaveProperty('keep_alive'); + }); + + it('calls the rollup API if the index is a rollup type', async () => { + mockApiCaller.mockResolvedValueOnce(mockRollupResponse); + + const params = { index: 'foo-程', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch + .search( + { + indexType: 'rollup', + params, + }, + {}, + mockDeps + ) + .toPromise(); - await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); + expect(mockApiCaller).toBeCalled(); + const { method, path } = mockApiCaller.mock.calls[0][0]; + expect(method).toBe('POST'); + expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); + }); - expect(mockGetCaller).toBeCalled(); - const request = mockGetCaller.mock.calls[0][0]; - expect(request.id).toEqual('foo'); - expect(request).toHaveProperty('wait_for_completion_timeout'); - expect(request).toHaveProperty('keep_alive'); + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { + mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-*', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search({ params }, {}, mockDeps).toPromise(); + + expect(mockSubmitCaller).toBeCalled(); + const request = mockSubmitCaller.mock.calls[0][0]; + expect(request).toHaveProperty('wait_for_completion_timeout'); + expect(request).toHaveProperty('keep_alive'); + }); }); - it('calls the rollup API if the index is a rollup type', async () => { - mockApiCaller.mockResolvedValueOnce(mockRollupResponse); + describe('cancel', () => { + it('makes a DELETE request to async search with the provided ID', async () => { + mockDeleteCaller.mockResolvedValueOnce(200); - const params = { index: 'foo-程', body: {} }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const id = 'some_id'; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch - .search( - { - indexType: 'rollup', - params, - }, - {}, - mockDeps - ) - .toPromise(); - - expect(mockApiCaller).toBeCalled(); - const { method, path } = mockApiCaller.mock.calls[0][0]; - expect(method).toBe('POST'); - expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); + await esSearch.cancel!(id, {}, mockDeps); + + expect(mockDeleteCaller).toBeCalled(); + const request = mockDeleteCaller.mock.calls[0][0]; + expect(request).toEqual({ id }); + }); }); - it('sets wait_for_completion_timeout and keep_alive in the request', async () => { - mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + describe('extend', () => { + it('makes a GET request to async search with the provided ID and keepAlive', async () => { + mockGetCaller.mockResolvedValueOnce(mockAsyncResponse); - const params = { index: 'foo-*', body: {} }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + const id = 'some_other_id'; + const keepAlive = '1d'; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search({ params }, {}, mockDeps).toPromise(); + await esSearch.extend!(id, keepAlive, {}, mockDeps); - expect(mockSubmitCaller).toBeCalled(); - const request = mockSubmitCaller.mock.calls[0][0]; - expect(request).toHaveProperty('wait_for_completion_timeout'); - expect(request).toHaveProperty('keep_alive'); + expect(mockGetCaller).toBeCalled(); + const request = mockGetCaller.mock.calls[0][0]; + expect(request).toEqual({ id, keep_alive: keepAlive }); + }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index fc5787f9e5cab..c1520d931c272 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -111,5 +111,9 @@ export const enhancedEsSearchStrategyProvider = ( logger.debug(`cancel ${id}`); await esClient.asCurrentUser.asyncSearch.delete({ id }); }, + extend: async (id, keepAlive, options, { esClient }) => { + logger.debug(`extend ${id} by ${keepAlive}`); + await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive }); + }, }; };