mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
This commit is contained in:
parent
1f179bbbc1
commit
e934fb8b36
32 changed files with 539 additions and 405 deletions
|
@ -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",
|
||||
|
|
|
@ -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$/,
|
||||
],
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {},
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -18,4 +18,3 @@
|
|||
*/
|
||||
|
||||
export { fetchSoon } from './fetch_soon';
|
||||
export { getEsClient, LegacyApiCaller } from './es_client';
|
||||
|
|
|
@ -35,12 +35,6 @@ function createStartContract(): jest.Mocked<ISearchStart> {
|
|||
aggs: searchAggsStartMock(),
|
||||
search: jest.fn(),
|
||||
searchSource: searchSourceMock,
|
||||
__LEGACY: {
|
||||
esClient: {
|
||||
search: jest.fn(),
|
||||
msearch: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = ({
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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$,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { getEsClient } from './get_es_client';
|
||||
export { LegacyApiCaller } from './types';
|
||||
export * from './msearch';
|
||||
export * from './search';
|
150
src/plugins/data/server/search/routes/msearch.test.ts
Normal file
150
src/plugins/data/server/search/routes/msearch.test.ts
Normal 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\\"}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
136
src/plugins/data/server/search/routes/msearch.ts
Normal file
136
src/plugins/data/server/search/routes/msearch.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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];
|
|
@ -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();
|
||||
|
|
@ -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)) {
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
88
test/api_integration/apis/search/search.ts
Normal file
88
test/api_integration/apis/search/search.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue