[data.search.SearchSource] Remove legacy ES client APIs. (#75943) (#76676)

This commit is contained in:
Luke Elmers 2020-09-03 14:47:38 -06:00 committed by GitHub
parent 1f179bbbc1
commit e934fb8b36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 539 additions and 405 deletions

View file

@ -362,7 +362,6 @@
"dedent": "^0.7.0",
"deepmerge": "^4.2.2",
"delete-empty": "^2.0.0",
"elasticsearch-browser": "^16.7.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-adapter-utils": "^1.13.0",

View file

@ -77,7 +77,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
// or which have require() statements that should be ignored because the file is
// already bundled with all its necessary depedencies
noParse: [
/[\/\\]node_modules[\/\\]elasticsearch-browser[\/\\]/,
/[\/\\]node_modules[\/\\]lodash[\/\\]index\.js$/,
/[\/\\]node_modules[\/\\]vega[\/\\]build[\/\\]vega\.js$/,
],

View file

@ -54,6 +54,3 @@ export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme
import * as Theme from './theme.ts';
export { Theme };
// massive deps that we should really get rid of or reduce in size substantially
export const ElasticsearchBrowser = require('elasticsearch-browser/elasticsearch.js');

View file

@ -62,12 +62,5 @@ exports.externals = {
'@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme',
'@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.Theme.euiLightVars',
'@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars',
/**
* massive deps that we should really get rid of or reduce in size substantially
*/
elasticsearch: '__kbnSharedDeps__.ElasticsearchBrowser',
'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser',
'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser',
};
exports.publicPathLoader = require.resolve('./public_path_loader');

View file

@ -19,7 +19,6 @@
"compression-webpack-plugin": "^4.0.0",
"core-js": "^3.2.1",
"custom-event-polyfill": "^0.3.0",
"elasticsearch-browser": "^16.7.0",
"jquery": "^3.5.0",
"mini-css-extract-plugin": "0.8.0",
"moment": "^2.24.0",

View file

@ -19,13 +19,7 @@
import './index.scss';
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
PackageInfo,
} from 'src/core/public';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { ConfigSchema } from '../config';
import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public';
import {
@ -101,7 +95,6 @@ export class DataPublicPlugin
private readonly fieldFormatsService: FieldFormatsService;
private readonly queryService: QueryService;
private readonly storage: IStorageWrapper;
private readonly packageInfo: PackageInfo;
constructor(initializerContext: PluginInitializerContext<ConfigSchema>) {
this.searchService = new SearchService();
@ -109,7 +102,6 @@ export class DataPublicPlugin
this.fieldFormatsService = new FieldFormatsService();
this.autocomplete = new AutocompleteService(initializerContext);
this.storage = new Storage(window.localStorage);
this.packageInfo = initializerContext.env.packageInfo;
}
public setup(
@ -146,7 +138,6 @@ export class DataPublicPlugin
const searchService = this.searchService.setup(core, {
usageCollection,
packageInfo: this.packageInfo,
expressions,
});

View file

@ -28,6 +28,7 @@ import { ExpressionAstFunction } from 'src/plugins/expressions/common';
import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { History } from 'history';
import { Href } from 'history';
import { HttpStart } from 'src/core/public';
import { IconType } from '@elastic/eui';
import { InjectedIntl } from '@kbn/i18n/react';
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';

View file

@ -17,8 +17,9 @@
* under the License.
*/
import { HttpStart } from 'src/core/public';
import { BehaviorSubject } from 'rxjs';
import { GetConfigFn } from '../../../common';
import { ISearchStartLegacy } from '../types';
/**
* @internal
@ -30,9 +31,9 @@ import { ISearchStartLegacy } from '../types';
export type SearchRequest = Record<string, any>;
export interface FetchHandlers {
legacySearchService: ISearchStartLegacy;
config: { get: GetConfigFn };
esShardTimeout: number;
http: HttpStart;
loadingCount$: BehaviorSubject<number>;
}
export interface SearchError {

View file

@ -17,11 +17,13 @@
* under the License.
*/
import { coreMock } from '../../../../../core/public/mocks';
import { callClient } from './call_client';
import { SearchStrategySearchParams } from './types';
import { defaultSearchStrategy } from './default_search_strategy';
import { FetchHandlers } from '../fetch';
import { handleResponse } from '../fetch/handle_response';
import { BehaviorSubject } from 'rxjs';
const mockAbortFn = jest.fn();
jest.mock('../fetch/handle_response', () => ({
@ -54,7 +56,13 @@ describe('callClient', () => {
test('Passes the additional arguments it is given to the search strategy', () => {
const searchRequests = [{ _searchStrategyId: 0 }];
const args = { legacySearchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers;
const args = {
http: coreMock.createStart().http,
legacySearchService: {},
config: { get: jest.fn() },
esShardTimeout: 0,
loadingCount$: new BehaviorSubject(0),
} as FetchHandlers;
callClient(searchRequests, [], args);

View file

@ -17,44 +17,26 @@
* under the License.
*/
import { IUiSettingsClient } from 'kibana/public';
import { HttpStart } from 'src/core/public';
import { coreMock } from '../../../../../core/public/mocks';
import { defaultSearchStrategy } from './default_search_strategy';
import { searchServiceMock } from '../mocks';
import { SearchStrategySearchParams } from './types';
import { UI_SETTINGS } from '../../../common';
import { BehaviorSubject } from 'rxjs';
const { search } = defaultSearchStrategy;
function getConfigStub(config: any = {}) {
return {
get: (key) => config[key],
} as IUiSettingsClient;
}
const msearchMockResponse: any = Promise.resolve([]);
msearchMockResponse.abort = jest.fn();
const msearchMock = jest.fn().mockReturnValue(msearchMockResponse);
const searchMockResponse: any = Promise.resolve([]);
searchMockResponse.abort = jest.fn();
const searchMock = jest.fn().mockReturnValue(searchMockResponse);
const msearchMock = jest.fn().mockResolvedValue({ body: { responses: [] } });
describe('defaultSearchStrategy', function () {
describe('search', function () {
let searchArgs: MockedKeys<Omit<SearchStrategySearchParams, 'config'>>;
let es: any;
let searchArgs: MockedKeys<SearchStrategySearchParams>;
let http: jest.Mocked<HttpStart>;
beforeEach(() => {
msearchMockResponse.abort.mockClear();
msearchMock.mockClear();
searchMockResponse.abort.mockClear();
searchMock.mockClear();
const searchService = searchServiceMock.createStartContract();
searchService.aggs.calculateAutoTimeExpression = jest.fn().mockReturnValue('1d');
searchService.__LEGACY.esClient.search = searchMock;
searchService.__LEGACY.esClient.msearch = msearchMock;
http = coreMock.createStart().http;
http.post.mockResolvedValue(msearchMock);
searchArgs = {
searchRequests: [
@ -62,49 +44,27 @@ describe('defaultSearchStrategy', function () {
index: { title: 'foo' },
},
],
esShardTimeout: 0,
legacySearchService: searchService.__LEGACY,
http,
config: {
get: jest.fn(),
},
loadingCount$: new BehaviorSubject(0) as any,
};
es = searchArgs.legacySearchService.esClient;
});
test('does not send max_concurrent_shard_requests by default', async () => {
const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true });
await search({ ...searchArgs, config });
expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined);
});
test('allows configuration of max_concurrent_shard_requests', async () => {
const config = getConfigStub({
[UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
[UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 42,
});
await search({ ...searchArgs, config });
expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42);
});
test('should set rest_total_hits_as_int to true on a request', async () => {
const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true });
await search({ ...searchArgs, config });
expect(es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true);
});
test('should set ignore_throttled=false when including frozen indices', async () => {
const config = getConfigStub({
[UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
[UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true,
});
await search({ ...searchArgs, config });
expect(es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false);
});
test('should properly call abort with msearch', () => {
const config = getConfigStub({
[UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
});
search({ ...searchArgs, config }).abort();
expect(msearchMockResponse.abort).toHaveBeenCalled();
test('calls http.post with the correct arguments', async () => {
await search({ ...searchArgs });
expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"/internal/_msearch",
Object {
"body": "{\\"searches\\":[{\\"header\\":{\\"index\\":\\"foo\\"}}]}",
"signal": AbortSignal {},
},
],
]
`);
});
});
});

View file

@ -17,8 +17,7 @@
* under the License.
*/
import { getPreference, getTimeout } from '../fetch';
import { getMSearchParams } from './get_msearch_params';
import { getPreference } from '../fetch';
import { SearchStrategyProvider, SearchStrategySearchParams } from './types';
// @deprecated
@ -30,34 +29,45 @@ export const defaultSearchStrategy: SearchStrategyProvider = {
},
};
function msearch({
searchRequests,
legacySearchService,
config,
esShardTimeout,
}: SearchStrategySearchParams) {
const es = legacySearchService.esClient;
const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => {
const inlineHeader = {
index: index.title || index,
search_type: searchType,
ignore_unavailable: true,
preference: getPreference(config.get),
function msearch({ searchRequests, config, http, loadingCount$ }: SearchStrategySearchParams) {
const requests = searchRequests.map(({ index, body }) => {
return {
header: {
index: index.title || index,
preference: getPreference(config.get),
},
body,
};
const inlineBody = {
...body,
timeout: getTimeout(esShardTimeout),
};
return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`;
});
const searching = es.msearch({
...getMSearchParams(config.get),
body: `${inlineRequests.join('\n')}\n`,
});
const abortController = new AbortController();
let resolved = false;
// Start LoadingIndicator
loadingCount$.next(loadingCount$.getValue() + 1);
const cleanup = () => {
if (!resolved) {
resolved = true;
// Decrement loading counter & cleanup BehaviorSubject
loadingCount$.next(loadingCount$.getValue() - 1);
loadingCount$.complete();
}
};
const searching = http
.post('/internal/_msearch', {
body: JSON.stringify({ searches: requests }),
signal: abortController.signal,
})
.then(({ body }) => body?.responses)
.finally(() => cleanup());
return {
searching: searching.then(({ responses }: any) => responses),
abort: searching.abort,
abort: () => {
abortController.abort();
cleanup();
},
searching,
};
}

View file

@ -1,98 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// @ts-ignore
import { default as es } from 'elasticsearch-browser/elasticsearch';
import { CoreStart, PackageInfo } from 'kibana/public';
import { BehaviorSubject } from 'rxjs';
export function getEsClient({
esRequestTimeout,
esApiVersion,
http,
packageVersion,
}: {
esRequestTimeout: number;
esApiVersion: string;
http: CoreStart['http'];
packageVersion: PackageInfo['version'];
}) {
// Use legacy es client for msearch.
const client = es.Client({
host: getEsUrl(http, packageVersion),
log: 'info',
requestTimeout: esRequestTimeout,
apiVersion: esApiVersion,
});
const loadingCount$ = new BehaviorSubject(0);
http.addLoadingCountSource(loadingCount$);
return {
search: wrapEsClientMethod(client, 'search', loadingCount$),
msearch: wrapEsClientMethod(client, 'msearch', loadingCount$),
create: wrapEsClientMethod(client, 'create', loadingCount$),
};
}
function wrapEsClientMethod(esClient: any, method: string, loadingCount$: BehaviorSubject<number>) {
return (args: any) => {
// esClient returns a promise, with an additional abort handler
// To tap into the abort handling, we have to override that abort handler.
const customPromiseThingy = esClient[method](args);
const { abort } = customPromiseThingy;
let resolved = false;
// Start LoadingIndicator
loadingCount$.next(loadingCount$.getValue() + 1);
// Stop LoadingIndicator when user aborts
customPromiseThingy.abort = () => {
abort();
if (!resolved) {
resolved = true;
loadingCount$.next(loadingCount$.getValue() - 1);
}
};
// Stop LoadingIndicator when promise finishes
customPromiseThingy.finally(() => {
resolved = true;
loadingCount$.next(loadingCount$.getValue() - 1);
});
return customPromiseThingy;
};
}
function getEsUrl(http: CoreStart['http'], packageVersion: PackageInfo['version']) {
const a = document.createElement('a');
a.href = http.basePath.prepend('/elasticsearch');
const protocolPort = /https/.test(a.protocol) ? 443 : 80;
const port = a.port || protocolPort;
return {
host: a.hostname,
port,
protocol: a.protocol,
pathname: a.pathname,
headers: {
'kbn-version': packageVersion,
},
};
}

View file

@ -1,64 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getMSearchParams } from './get_msearch_params';
import { GetConfigFn, UI_SETTINGS } from '../../../common';
function getConfigStub(config: any = {}): GetConfigFn {
return (key) => config[key];
}
describe('getMSearchParams', () => {
test('includes rest_total_hits_as_int', () => {
const config = getConfigStub();
const msearchParams = getMSearchParams(config);
expect(msearchParams.rest_total_hits_as_int).toBe(true);
});
test('includes ignore_throttled according to search:includeFrozen', () => {
let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true });
let msearchParams = getMSearchParams(config);
expect(msearchParams.ignore_throttled).toBe(false);
config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false });
msearchParams = getMSearchParams(config);
expect(msearchParams.ignore_throttled).toBe(true);
});
test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests if greater than 0', () => {
let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 });
let msearchParams = getMSearchParams(config);
expect(msearchParams.max_concurrent_shard_requests).toBe(undefined);
config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 });
msearchParams = getMSearchParams(config);
expect(msearchParams.max_concurrent_shard_requests).toBe(5);
});
test('does not include other search params that are included in the msearch header or body', () => {
const config = getConfigStub({
[UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false,
[UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5,
});
const msearchParams = getMSearchParams(config);
expect(msearchParams.hasOwnProperty('ignore_unavailable')).toBe(false);
expect(msearchParams.hasOwnProperty('preference')).toBe(false);
expect(msearchParams.hasOwnProperty('timeout')).toBe(false);
});
});

View file

@ -1,29 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { GetConfigFn } from '../../../common';
import { getIgnoreThrottled, getMaxConcurrentShardRequests } from '../fetch';
export function getMSearchParams(getConfig: GetConfigFn) {
return {
rest_total_hits_as_int: true,
ignore_throttled: getIgnoreThrottled(getConfig),
max_concurrent_shard_requests: getMaxConcurrentShardRequests(getConfig),
};
}

View file

@ -18,4 +18,3 @@
*/
export { fetchSoon } from './fetch_soon';
export { getEsClient, LegacyApiCaller } from './es_client';

View file

@ -35,12 +35,6 @@ function createStartContract(): jest.Mocked<ISearchStart> {
aggs: searchAggsStartMock(),
search: jest.fn(),
searchSource: searchSourceMock,
__LEGACY: {
esClient: {
search: jest.fn(),
msearch: jest.fn(),
},
},
};
}

View file

@ -17,11 +17,11 @@
* under the License.
*/
import { Plugin, CoreSetup, CoreStart, PackageInfo } from 'src/core/public';
import { Plugin, CoreSetup, CoreStart } from 'src/core/public';
import { BehaviorSubject } from 'rxjs';
import { ISearchSetup, ISearchStart, SearchEnhancements } from './types';
import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source';
import { getEsClient, LegacyApiCaller } from './legacy';
import { AggsService, AggsStartDependencies } from './aggs';
import { IndexPatternsContract } from '../index_patterns/index_patterns';
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
@ -33,9 +33,8 @@ import { ExpressionsSetup } from '../../../expressions/public';
/** @internal */
export interface SearchServiceSetupDependencies {
packageInfo: PackageInfo;
usageCollection?: UsageCollectionSetup;
expressions: ExpressionsSetup;
usageCollection?: UsageCollectionSetup;
}
/** @internal */
@ -45,28 +44,18 @@ export interface SearchServiceStartDependencies {
}
export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private esClient?: LegacyApiCaller;
private readonly aggsService = new AggsService();
private searchInterceptor!: ISearchInterceptor;
private usageCollector?: SearchUsageCollector;
public setup(
{ http, getStartServices, injectedMetadata, notifications, uiSettings }: CoreSetup,
{ expressions, packageInfo, usageCollection }: SearchServiceSetupDependencies
{ expressions, usageCollection }: SearchServiceSetupDependencies
): ISearchSetup {
const esApiVersion = injectedMetadata.getInjectedVar('esApiVersion') as string;
const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number;
const packageVersion = packageInfo.version;
this.usageCollector = createUsageCollector(getStartServices, usageCollection);
this.esClient = getEsClient({
esRequestTimeout,
esApiVersion,
http,
packageVersion,
});
/**
* A global object that intercepts all searches and provides convenience methods for cancelling
* all pending search requests, as well as getting the number of pending search requests.
@ -107,15 +96,16 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
return this.searchInterceptor.search(request, options);
}) as ISearchGeneric;
const legacySearch = {
esClient: this.esClient!,
};
const loadingCount$ = new BehaviorSubject(0);
http.addLoadingCountSource(loadingCount$);
const searchSourceDependencies: SearchSourceDependencies = {
getConfig: uiSettings.get.bind(uiSettings),
// TODO: we don't need this, apply on the server
esShardTimeout: injectedMetadata.getInjectedVar('esShardTimeout') as number,
search,
legacySearch,
http,
loadingCount$,
};
return {
@ -127,7 +117,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
return new SearchSource({}, searchSourceDependencies);
},
},
__LEGACY: legacySearch,
};
}

View file

@ -22,7 +22,8 @@ import { SearchSourceDependencies } from './search_source';
import { IIndexPattern } from '../../../common/index_patterns';
import { IndexPatternsContract } from '../../index_patterns/index_patterns';
import { Filter } from '../../../common/es_query/filters';
import { dataPluginMock } from '../../mocks';
import { coreMock } from '../../../../../core/public/mocks';
import { BehaviorSubject } from 'rxjs';
describe('createSearchSource', () => {
const indexPatternMock: IIndexPattern = {} as IIndexPattern;
@ -31,13 +32,12 @@ describe('createSearchSource', () => {
let createSearchSource: ReturnType<typeof createSearchSourceFactory>;
beforeEach(() => {
const data = dataPluginMock.createStartContract();
dependencies = {
getConfig: jest.fn(),
search: jest.fn(),
legacySearch: data.search.__LEGACY,
esShardTimeout: 30000,
http: coreMock.createStart().http,
loadingCount$: new BehaviorSubject(0),
};
indexPatternContractMock = ({

View file

@ -17,7 +17,8 @@
* under the License.
*/
import { uiSettingsServiceMock } from '../../../../../core/public/mocks';
import { BehaviorSubject } from 'rxjs';
import { httpServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks';
import { ISearchSource, SearchSource } from './search_source';
import { SearchSourceFields } from './types';
@ -54,10 +55,6 @@ export const createSearchSourceMock = (fields?: SearchSourceFields) =>
getConfig: uiSettingsServiceMock.createStartContract().get,
esShardTimeout: 30000,
search: jest.fn(),
legacySearch: {
esClient: {
search: jest.fn(),
msearch: jest.fn(),
},
},
http: httpServiceMock.createStartContract(),
loadingCount$: new BehaviorSubject(0),
});

View file

@ -17,12 +17,12 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { Observable, BehaviorSubject } from 'rxjs';
import { GetConfigFn } from 'src/plugins/data/common';
import { SearchSource, SearchSourceDependencies } from './search_source';
import { IndexPattern, SortDirection } from '../..';
import { fetchSoon } from '../legacy';
import { dataPluginMock } from '../../../../data/public/mocks';
import { coreMock } from '../../../../../core/public/mocks';
jest.mock('../legacy', () => ({
fetchSoon: jest.fn().mockResolvedValue({}),
@ -54,8 +54,6 @@ describe('SearchSource', () => {
let searchSourceDependencies: SearchSourceDependencies;
beforeEach(() => {
const data = dataPluginMock.createStartContract();
mockSearchMethod = jest.fn(() => {
return new Observable((subscriber) => {
setTimeout(() => {
@ -70,8 +68,9 @@ describe('SearchSource', () => {
searchSourceDependencies = {
getConfig: jest.fn(),
search: mockSearchMethod,
legacySearch: data.search.__LEGACY,
esShardTimeout: 30000,
http: coreMock.createStart().http,
loadingCount$: new BehaviorSubject(0),
};
});

View file

@ -72,6 +72,8 @@
import { setWith } from '@elastic/safer-lodash-set';
import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash';
import { map } from 'rxjs/operators';
import { HttpStart } from 'src/core/public';
import { BehaviorSubject } from 'rxjs';
import { normalizeSortRequest } from './normalize_sort_request';
import { filterDocvalueFields } from './filter_docvalue_fields';
import { fieldWildcardFilter } from '../../../../kibana_utils/common';
@ -95,7 +97,6 @@ import { getHighlightRequest } from '../../../common/field_formats';
import { GetConfigFn } from '../../../common/types';
import { fetchSoon } from '../legacy';
import { extractReferences } from './extract_references';
import { ISearchStartLegacy } from '../types';
/** @internal */
export const searchSourceRequiredUiSettings = [
@ -116,8 +117,9 @@ export const searchSourceRequiredUiSettings = [
export interface SearchSourceDependencies {
getConfig: GetConfigFn;
search: ISearchGeneric;
legacySearch: ISearchStartLegacy;
http: HttpStart;
esShardTimeout: number;
loadingCount$: BehaviorSubject<number>;
}
/** @public **/
@ -248,7 +250,7 @@ export class SearchSource {
* @return {Promise<SearchResponse<unknown>>}
*/
private async legacyFetch(searchRequest: SearchRequest, options: ISearchOptions) {
const { esShardTimeout, legacySearch, getConfig } = this.dependencies;
const { http, getConfig, loadingCount$ } = this.dependencies;
return await fetchSoon(
searchRequest,
@ -257,9 +259,9 @@ export class SearchSource {
...options,
},
{
legacySearchService: legacySearch,
http,
config: { get: getConfig },
esShardTimeout,
loadingCount$,
}
);
}

View file

@ -19,7 +19,6 @@
import { Observable } from 'rxjs';
import { PackageInfo } from 'kibana/server';
import { LegacyApiCaller } from './legacy/es_client';
import { ISearchInterceptor } from './search_interceptor';
import { ISearchSource, SearchSourceFields } from './search_source';
import { SearchUsageCollector } from './collectors';
@ -47,10 +46,6 @@ export type ISearchGeneric = <
options?: ISearchOptions
) => Observable<SearchStrategyResponse>;
export interface ISearchStartLegacy {
esClient: LegacyApiCaller;
}
export interface SearchEnhancements {
searchInterceptor: ISearchInterceptor;
}
@ -74,11 +69,6 @@ export interface ISearchStart {
create: (fields?: SearchSourceFields) => Promise<ISearchSource>;
createEmpty: () => ISearchSource;
};
/**
* @deprecated
* @internal
*/
__LEGACY: ISearchStartLegacy;
}
export { SEARCH_EVENT_TYPE } from './collectors';

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { getEsClient } from './get_es_client';
export { LegacyApiCaller } from './types';
export * from './msearch';
export * from './search';

View file

@ -0,0 +1,150 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import {
CoreSetup,
RequestHandlerContext,
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
import {
coreMock,
httpServerMock,
pluginInitializerContextConfigMock,
} from '../../../../../../src/core/server/mocks';
import { registerMsearchRoute, convertRequestBody } from './msearch';
import { DataPluginStart } from '../../plugin';
import { dataPluginMock } from '../../mocks';
describe('msearch route', () => {
let mockDataStart: MockedKeys<DataPluginStart>;
let mockCoreSetup: MockedKeys<CoreSetup<{}, DataPluginStart>>;
let getStartServices: jest.Mocked<StartServicesAccessor<{}, DataPluginStart>>;
let globalConfig$: Observable<SharedGlobalConfig>;
beforeEach(() => {
mockDataStart = dataPluginMock.createStartContract();
mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart });
getStartServices = mockCoreSetup.getStartServices;
globalConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
});
it('handler calls /_msearch with the given request', async () => {
const response = { id: 'yay' };
const mockClient = { transport: { request: jest.fn().mockResolvedValue(response) } };
const mockContext = {
core: {
elasticsearch: { client: { asCurrentUser: mockClient } },
uiSettings: { client: { get: jest.fn() } },
},
};
const mockBody = { searches: [{ header: {}, body: {} }] };
const mockQuery = {};
const mockRequest = httpServerMock.createKibanaRequest({
body: mockBody,
query: mockQuery,
});
const mockResponse = httpServerMock.createResponseFactory();
registerMsearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ });
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.post.mock.calls[0][1];
await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
expect(mockClient.transport.request.mock.calls[0][0].method).toBe('GET');
expect(mockClient.transport.request.mock.calls[0][0].path).toBe('/_msearch');
expect(mockClient.transport.request.mock.calls[0][0].body).toEqual(
convertRequestBody(mockBody as any, { timeout: '0ms' })
);
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
body: response,
});
});
it('handler throws an error if the search throws an error', async () => {
const response = {
message: 'oh no',
body: {
error: 'oops',
},
};
const mockClient = {
transport: { request: jest.fn().mockReturnValue(Promise.reject(response)) },
};
const mockContext = {
core: {
elasticsearch: { client: { asCurrentUser: mockClient } },
uiSettings: { client: { get: jest.fn() } },
},
};
const mockBody = { searches: [{ header: {}, body: {} }] };
const mockQuery = {};
const mockRequest = httpServerMock.createKibanaRequest({
body: mockBody,
query: mockQuery,
});
const mockResponse = httpServerMock.createResponseFactory();
registerMsearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ });
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.post.mock.calls[0][1];
await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse);
expect(mockClient.transport.request).toBeCalled();
expect(mockResponse.customError).toBeCalled();
const error: any = mockResponse.customError.mock.calls[0][0];
expect(error.body.message).toBe('oh no');
expect(error.body.attributes.error).toBe('oops');
});
describe('convertRequestBody', () => {
it('combines header & body into proper msearch request', () => {
const request = {
searches: [{ header: { index: 'foo', preference: 0 }, body: { test: true } }],
};
expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(`
"{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0}
{\\"timeout\\":\\"30000ms\\",\\"test\\":true}
"
`);
});
it('handles multiple searches', () => {
const request = {
searches: [
{ header: { index: 'foo', preference: 0 }, body: { test: true } },
{ header: { index: 'bar', preference: 1 }, body: { hello: 'world' } },
],
};
expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(`
"{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0}
{\\"timeout\\":\\"30000ms\\",\\"test\\":true}
{\\"ignore_unavailable\\":true,\\"index\\":\\"bar\\",\\"preference\\":1}
{\\"timeout\\":\\"30000ms\\",\\"hello\\":\\"world\\"}
"
`);
});
});
});

View file

@ -0,0 +1,136 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { first } from 'rxjs/operators';
import { schema } from '@kbn/config-schema';
import { IRouter } from 'src/core/server';
import { UI_SETTINGS } from '../../../common';
import { SearchRouteDependencies } from '../search_service';
import { getDefaultSearchParams } from '..';
interface MsearchHeaders {
index: string;
preference?: number | string;
}
interface MsearchRequest {
header: MsearchHeaders;
body: any;
}
interface RequestBody {
searches: MsearchRequest[];
}
/** @internal */
export function convertRequestBody(
requestBody: RequestBody,
{ timeout }: { timeout?: string }
): string {
return requestBody.searches.reduce((req, curr) => {
const header = JSON.stringify({
ignore_unavailable: true,
...curr.header,
});
const body = JSON.stringify({
timeout,
...curr.body,
});
return `${req}${header}\n${body}\n`;
}, '');
}
/**
* The msearch route takes in an array of searches, each consisting of header
* and body json, and reformts them into a single request for the _msearch API.
*
* The reason for taking requests in a different format is so that we can
* inject values into each request without needing to manually parse each one.
*
* This route is internal and _should not be used_ in any new areas of code.
* It only exists as a means of removing remaining dependencies on the
* legacy ES client.
*
* @deprecated
*/
export function registerMsearchRoute(router: IRouter, deps: SearchRouteDependencies): void {
router.post(
{
path: '/internal/_msearch',
validate: {
body: schema.object({
searches: schema.arrayOf(
schema.object({
header: schema.object(
{
index: schema.string(),
preference: schema.maybe(schema.oneOf([schema.number(), schema.string()])),
},
{ unknowns: 'allow' }
),
body: schema.object({}, { unknowns: 'allow' }),
})
),
}),
},
},
async (context, request, res) => {
const client = context.core.elasticsearch.client.asCurrentUser;
// get shardTimeout
const config = await deps.globalConfig$.pipe(first()).toPromise();
const { timeout } = getDefaultSearchParams(config);
const body = convertRequestBody(request.body, { timeout });
try {
const ignoreThrottled = !(await context.core.uiSettings.client.get(
UI_SETTINGS.SEARCH_INCLUDE_FROZEN
));
const maxConcurrentShardRequests = await context.core.uiSettings.client.get(
UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS
);
const response = await client.transport.request({
method: 'GET',
path: '/_msearch',
body,
querystring: {
rest_total_hits_as_int: true,
ignore_throttled: ignoreThrottled,
max_concurrent_shard_requests:
maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined,
},
});
return res.ok({ body: response });
} catch (err) {
return res.customError({
statusCode: err.statusCode || 500,
body: {
message: err.message,
attributes: {
error: err.body?.error || err.message,
},
},
});
}
}
);
}

View file

@ -17,19 +17,34 @@
* under the License.
*/
import { CoreSetup, RequestHandlerContext } from '../../../../../src/core/server';
import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks';
import { registerSearchRoute } from './routes';
import { DataPluginStart } from '../plugin';
import { dataPluginMock } from '../mocks';
import { Observable } from 'rxjs';
import {
CoreSetup,
RequestHandlerContext,
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
import {
coreMock,
httpServerMock,
pluginInitializerContextConfigMock,
} from '../../../../../../src/core/server/mocks';
import { registerSearchRoute } from './search';
import { DataPluginStart } from '../../plugin';
import { dataPluginMock } from '../../mocks';
describe('Search service', () => {
let mockDataStart: MockedKeys<DataPluginStart>;
let mockCoreSetup: MockedKeys<CoreSetup<object, DataPluginStart>>;
let mockCoreSetup: MockedKeys<CoreSetup<{}, DataPluginStart>>;
let getStartServices: jest.Mocked<StartServicesAccessor<{}, DataPluginStart>>;
let globalConfig$: Observable<SharedGlobalConfig>;
beforeEach(() => {
mockDataStart = dataPluginMock.createStartContract();
mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart });
getStartServices = mockCoreSetup.getStartServices;
globalConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
});
it('handler calls context.search.search with the given request and strategy', async () => {
@ -44,7 +59,7 @@ describe('Search service', () => {
});
const mockResponse = httpServerMock.createResponseFactory();
registerSearchRoute(mockCoreSetup);
registerSearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ });
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.post.mock.calls[0][1];
@ -75,7 +90,7 @@ describe('Search service', () => {
});
const mockResponse = httpServerMock.createResponseFactory();
registerSearchRoute(mockCoreSetup);
registerSearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ });
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.post.mock.calls[0][1];

View file

@ -18,13 +18,14 @@
*/
import { schema } from '@kbn/config-schema';
import { CoreSetup } from '../../../../core/server';
import { getRequestAbortedSignal } from '../lib';
import { DataPluginStart } from '../plugin';
export function registerSearchRoute(core: CoreSetup<object, DataPluginStart>): void {
const router = core.http.createRouter();
import { IRouter } from 'src/core/server';
import { getRequestAbortedSignal } from '../../lib';
import { SearchRouteDependencies } from '../search_service';
export function registerSearchRoute(
router: IRouter,
{ getStartServices }: SearchRouteDependencies
): void {
router.post(
{
path: '/internal/search/{strategy}/{id?}',
@ -44,7 +45,7 @@ export function registerSearchRoute(core: CoreSetup<object, DataPluginStart>): v
const { strategy, id } = request.params;
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
const [, , selfStart] = await core.getStartServices();
const [, , selfStart] = await getStartServices();
try {
const response = await selfStart.search.search(
@ -85,7 +86,7 @@ export function registerSearchRoute(core: CoreSetup<object, DataPluginStart>): v
async (context, request, res) => {
const { strategy, id } = request.params;
const [, , selfStart] = await core.getStartServices();
const [, , selfStart] = await getStartServices();
const searchStrategy = selfStart.search.getSearchStrategy(strategy);
if (!searchStrategy.cancel) return res.ok();

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { Observable } from 'rxjs';
import {
CoreSetup,
CoreStart,
@ -24,13 +25,15 @@ import {
Plugin,
PluginInitializerContext,
RequestHandlerContext,
} from '../../../../core/server';
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types';
import { AggsService, AggsSetupDependencies } from './aggs';
import { FieldFormatsStart } from '../field_formats';
import { registerSearchRoute } from './routes';
import { registerMsearchRoute, registerSearchRoute } from './routes';
import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search';
import { DataPluginStart } from '../plugin';
import { UsageCollectionSetup } from '../../../usage_collection/server';
@ -55,6 +58,12 @@ export interface SearchServiceStartDependencies {
fieldFormats: FieldFormatsStart;
}
/** @internal */
export interface SearchRouteDependencies {
getStartServices: StartServicesAccessor<{}, DataPluginStart>;
globalConfig$: Observable<SharedGlobalConfig>;
}
export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private readonly aggsService = new AggsService();
private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY;
@ -66,11 +75,19 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
) {}
public setup(
core: CoreSetup<object, DataPluginStart>,
core: CoreSetup<{}, DataPluginStart>,
{ registerFunction, usageCollection }: SearchServiceSetupDependencies
): ISearchSetup {
const usage = usageCollection ? usageProvider(core) : undefined;
const router = core.http.createRouter();
const routeDependencies = {
getStartServices: core.getStartServices,
globalConfig$: this.initializerContext.config.legacy.globalConfig$,
};
registerSearchRoute(router, routeDependencies);
registerMsearchRoute(router, routeDependencies);
this.registerSearchStrategy(
ES_SEARCH_STRATEGY,
esSearchStrategyProvider(
@ -85,8 +102,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
registerUsageCollector(usageCollection, this.initializerContext);
}
registerSearchRoute(core);
return {
__enhance: (enhancements: SearchEnhancements) => {
if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) {

View file

@ -28,6 +28,7 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./saved_objects_management'));
loadTestFile(require.resolve('./saved_objects'));
loadTestFile(require.resolve('./scripts'));
loadTestFile(require.resolve('./search'));
loadTestFile(require.resolve('./shorten'));
loadTestFile(require.resolve('./suggestions'));
loadTestFile(require.resolve('./status'));

View file

@ -17,14 +17,10 @@
* under the License.
*/
import { SearchResponse } from 'elasticsearch';
import { SearchRequest } from '../../fetch';
import { FtrProviderContext } from '../../ftr_provider_context';
export interface LegacyApiCaller {
search: (searchRequest: SearchRequest) => LegacyApiCallerResponse;
msearch: (searchRequest: SearchRequest) => LegacyApiCallerResponse;
}
interface LegacyApiCallerResponse extends Promise<SearchResponse<any>> {
abort: () => void;
export default function ({ loadTestFile }: FtrProviderContext) {
describe('search', () => {
loadTestFile(require.resolve('./search'));
});
}

View file

@ -0,0 +1,88 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('msearch', () => {
describe('post', () => {
it('should return 200 when correctly formatted searches are provided', async () =>
await supertest
.post(`/internal/_msearch`)
.send({
searches: [
{
header: { index: 'foo' },
body: {
query: {
match_all: {},
},
},
},
],
})
.expect(200));
it('should return 400 if you provide malformed content', async () =>
await supertest
.post(`/internal/_msearch`)
.send({
foo: false,
})
.expect(400));
it('should require you to provide an index for each request', async () =>
await supertest
.post(`/internal/_msearch`)
.send({
searches: [
{ header: { index: 'foo' }, body: {} },
{ header: {}, body: {} },
],
})
.expect(400));
it('should not require optional params', async () =>
await supertest
.post(`/internal/_msearch`)
.send({
searches: [{ header: { index: 'foo' }, body: {} }],
})
.expect(200));
it('should allow passing preference as a string', async () =>
await supertest
.post(`/internal/_msearch`)
.send({
searches: [{ header: { index: 'foo', preference: '_custom' }, body: {} }],
})
.expect(200));
it('should allow passing preference as a number', async () =>
await supertest
.post(`/internal/_msearch`)
.send({
searches: [{ header: { index: 'foo', preference: 123 }, body: {} }],
})
.expect(200));
});
});
}

View file

@ -11330,11 +11330,6 @@ elastic-apm-node@^3.7.0:
traceparent "^1.0.0"
unicode-byte-truncate "^1.0.0"
elasticsearch-browser@^16.7.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/elasticsearch-browser/-/elasticsearch-browser-16.7.0.tgz#1f32a402cd36a9bb14a9ea6cb70f8e126d4cb9b1"
integrity sha512-UES2Fbnzy4Ivq4QvES4sfk/a5UytJczeJdfxRWa4kuHEllKOffKQLTxJ8Ti86OREpACQxppqvYgzctJuEiIr7Q==
elasticsearch@^16.4.0:
version "16.5.0"
resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-16.5.0.tgz#619a48040be25d345fdddf09fa6042a88c3974d6"