diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8dc470e20c6198..dd741197376575 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -27,7 +27,6 @@ import { exportApi } from './server/routes/api/export'; import { homeApi } from './server/routes/api/home'; import { managementApi } from './server/routes/api/management'; import { scriptsApi } from './server/routes/api/scripts'; -import { registerSuggestionsApi } from './server/routes/api/suggestions'; import { registerKqlTelemetryApi } from './server/routes/api/kql_telemetry'; import { registerFieldFormats } from './server/field_formats/register'; import { registerTutorials } from './server/tutorials/register'; @@ -331,7 +330,6 @@ export default function(kibana) { exportApi(server); homeApi(server); managementApi(server); - registerSuggestionsApi(server); registerKqlTelemetryApi(server); registerFieldFormats(server); registerTutorials(server); diff --git a/src/legacy/core_plugins/kibana/server/routes/api/suggestions/register_value_suggestions.js b/src/legacy/core_plugins/kibana/server/routes/api/suggestions/register_value_suggestions.js deleted file mode 100644 index 6a6276234e5503..00000000000000 --- a/src/legacy/core_plugins/kibana/server/routes/api/suggestions/register_value_suggestions.js +++ /dev/null @@ -1,117 +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 { get, map } from 'lodash'; -import { abortableRequestHandler } from '../../../../../elasticsearch/lib/abortable_request_handler'; - -export function registerValueSuggestions(server) { - const serverConfig = server.config(); - const autocompleteTerminateAfter = serverConfig.get('kibana.autocompleteTerminateAfter'); - const autocompleteTimeout = serverConfig.get('kibana.autocompleteTimeout'); - server.route({ - path: '/api/kibana/suggestions/values/{index}', - method: ['POST'], - handler: abortableRequestHandler(async function(signal, req) { - const { index } = req.params; - const { field: fieldName, query, boolFilter } = req.payload; - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - - const savedObjectsClient = req.getSavedObjectsClient(); - const savedObjectsResponse = await savedObjectsClient.find({ - type: 'index-pattern', - fields: ['fields'], - search: `"${index}"`, - searchFields: ['title'], - }); - const indexPattern = - savedObjectsResponse.total > 0 ? savedObjectsResponse.saved_objects[0] : null; - const fields = indexPattern ? JSON.parse(indexPattern.attributes.fields) : null; - const field = fields ? fields.find(field => field.name === fieldName) : fieldName; - - const body = getBody( - { field, query, boolFilter }, - autocompleteTerminateAfter, - autocompleteTimeout - ); - - try { - const response = await callWithRequest(req, 'search', { index, body }, { signal }); - const buckets = - get(response, 'aggregations.suggestions.buckets') || - get(response, 'aggregations.nestedSuggestions.suggestions.buckets') || - []; - const suggestions = map(buckets, 'key'); - return suggestions; - } catch (error) { - throw server.plugins.elasticsearch.handleESError(error); - } - }), - }); -} - -function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) { - // Helps ensure that the regex is not evaluated eagerly against the terms dictionary - const executionHint = 'map'; - - // We don't care about the accuracy of the counts, just the content of the terms, so this reduces - // the amount of information that needs to be transmitted to the coordinating node - const shardSize = 10; - - const body = { - size: 0, - timeout: `${timeout}ms`, - terminate_after: terminateAfter, - query: { - bool: { - filter: boolFilter, - }, - }, - aggs: { - suggestions: { - terms: { - field: field.name || field, - include: `${getEscapedQuery(query)}.*`, - execution_hint: executionHint, - shard_size: shardSize, - }, - }, - }, - }; - - if (field.subType && field.subType.nested) { - return { - ...body, - aggs: { - nestedSuggestions: { - nested: { - path: field.subType.nested.path, - }, - aggs: body.aggs, - }, - }, - }; - } - - return body; -} - -function getEscapedQuery(query = '') { - // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators - return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, match => `\\${match}`); -} diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 614ae7dd34efd6..7d93bc5b84a5c3 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -20,6 +20,7 @@ import { IFieldType } from './fields'; export interface IIndexPattern { + [key: string]: any; fields: IFieldType[]; title: string; id?: string; diff --git a/src/plugins/data/server/autocomplete/autocomplete_service.ts b/src/plugins/data/server/autocomplete/autocomplete_service.ts new file mode 100644 index 00000000000000..1b85321aa2185e --- /dev/null +++ b/src/plugins/data/server/autocomplete/autocomplete_service.ts @@ -0,0 +1,29 @@ +/* + * 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 { CoreSetup, Plugin } from 'kibana/server'; +import { registerRoutes } from './routes'; + +export class AutocompleteService implements Plugin { + public setup(core: CoreSetup) { + registerRoutes(core); + } + + public start() {} +} diff --git a/src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.js b/src/plugins/data/server/autocomplete/index.ts similarity index 83% rename from src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.js rename to src/plugins/data/server/autocomplete/index.ts index 15110b192ee33c..6c10a8c98bdbf7 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.js +++ b/src/plugins/data/server/autocomplete/index.ts @@ -17,8 +17,4 @@ * under the License. */ -import { registerValueSuggestions } from './register_value_suggestions'; - -export function registerSuggestionsApi(server) { - registerValueSuggestions(server); -} +export { AutocompleteService } from './autocomplete_service'; diff --git a/src/plugins/data/server/autocomplete/routes.ts b/src/plugins/data/server/autocomplete/routes.ts new file mode 100644 index 00000000000000..9134287d2b8ff4 --- /dev/null +++ b/src/plugins/data/server/autocomplete/routes.ts @@ -0,0 +1,27 @@ +/* + * 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 { CoreSetup } from 'kibana/server'; +import { registerValueSuggestionsRoute } from './value_suggestions_route'; + +export function registerRoutes({ http }: CoreSetup): void { + const router = http.createRouter(); + + registerValueSuggestionsRoute(router); +} diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts new file mode 100644 index 00000000000000..b415e83becf930 --- /dev/null +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -0,0 +1,135 @@ +/* + * 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 { get, map } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; + +import { IFieldType, indexPatterns, esFilters } from '../index'; + +export function registerValueSuggestionsRoute(router: IRouter) { + router.post( + { + path: '/api/kibana/suggestions/values/{index}', + validate: { + params: schema.object( + { + index: schema.string(), + }, + { allowUnknowns: false } + ), + body: schema.object( + { + field: schema.string(), + query: schema.string(), + boolFilter: schema.maybe(schema.any()), + }, + { allowUnknowns: false } + ), + }, + }, + async (context, request, response) => { + const { client: uiSettings } = context.core.uiSettings; + const { field: fieldName, query, boolFilter } = request.body; + const { index } = request.params; + const { dataClient } = context.core.elasticsearch; + + const autocompleteSearchOptions = { + timeout: await uiSettings.get('kibana.autocompleteTimeout'), + terminate_after: await uiSettings.get('kibana.autocompleteTerminateAfter'), + }; + + const indexPattern = await indexPatterns.findIndexPatternById( + context.core.savedObjects.client, + index + ); + + const field = indexPattern && indexPatterns.getFieldByName(fieldName, indexPattern); + const body = await getBody(autocompleteSearchOptions, field || fieldName, query, boolFilter); + + try { + const result = await dataClient.callAsCurrentUser('search', { index, body }); + + const buckets: any[] = + get(result, 'aggregations.suggestions.buckets') || + get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); + + return response.ok({ body: map(buckets || [], 'key') }); + } catch (error) { + return response.internalError({ body: error }); + } + } + ); +} + +async function getBody( + { timeout, terminate_after }: Record, + field: IFieldType | string, + query: string, + boolFilter: esFilters.Filter[] = [] +) { + const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators + const getEscapedQuery = (q: string = '') => + q.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, match => `\\${match}`); + + // Helps ensure that the regex is not evaluated eagerly against the terms dictionary + const executionHint = 'map'; + + // We don't care about the accuracy of the counts, just the content of the terms, so this reduces + // the amount of information that needs to be transmitted to the coordinating node + const shardSize = 10; + const body = { + size: 0, + timeout, + terminate_after, + query: { + bool: { + filter: boolFilter, + }, + }, + aggs: { + suggestions: { + terms: { + field: isFieldObject(field) ? field.name : field, + include: `${getEscapedQuery(query)}.*`, + execution_hint: executionHint, + shard_size: shardSize, + }, + }, + }, + }; + + if (isFieldObject(field) && field.subType && field.subType.nested) { + return { + ...body, + aggs: { + nestedSuggestions: { + nested: { + path: field.subType.nested.path, + }, + aggs: body.aggs, + }, + }, + }; + } + + return body; +} diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 022eb0ae502958..fe96c494bd9ff4 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -57,6 +57,7 @@ export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues, + indexPatterns, } from './index_patterns'; export * from './search'; export { diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 6937fa22c4e5d1..b303ae30ea810b 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import * as indexPatterns from './utils'; export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; export { IndexPatternsService } from './index_patterns_service'; +export { indexPatterns }; diff --git a/src/plugins/data/server/index_patterns/utils.ts b/src/plugins/data/server/index_patterns/utils.ts new file mode 100644 index 00000000000000..b7adafaeb3e94b --- /dev/null +++ b/src/plugins/data/server/index_patterns/utils.ts @@ -0,0 +1,47 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import { IIndexPattern, IFieldType } from '../../common'; + +export const getFieldByName = ( + fieldName: string, + indexPattern: IIndexPattern +): IFieldType | undefined => { + const fields: IFieldType[] = indexPattern && JSON.parse(indexPattern.attributes.fields); + const field = fields && fields.find(f => f.name === fieldName); + + return field; +}; + +export const findIndexPatternById = async ( + savedObjectsClient: SavedObjectsClientContract, + index: string +): Promise => { + const savedObjectsResponse = await savedObjectsClient.find({ + type: 'index-pattern', + fields: ['fields'], + search: `"${index}"`, + searchFields: ['title'], + }); + + if (savedObjectsResponse.total > 0) { + return (savedObjectsResponse.saved_objects[0] as unknown) as IIndexPattern; + } +}; diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index e81250e653ebd3..6df0a11e7538dd 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -21,18 +21,25 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../.. import { IndexPatternsService } from './index_patterns'; import { ISearchSetup } from './search'; import { SearchService } from './search/search_service'; +import { AutocompleteService } from './autocomplete'; export interface DataPluginSetup { search: ISearchSetup; } + export class DataServerPlugin implements Plugin { private readonly searchService: SearchService; + private readonly autocompleteService = new AutocompleteService(); private readonly indexPatterns = new IndexPatternsService(); + constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); } + public setup(core: CoreSetup) { this.indexPatterns.setup(core); + this.autocompleteService.setup(core); + return { search: this.searchService.setup(core), };