From f3834dda01b1c3452674005d79d10c66bb6853a1 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 12 Dec 2019 14:00:09 +0300 Subject: [PATCH] value suggestions server route -> data plugin Closes #52842 --- .../core_plugins/elasticsearch/index.d.ts | 1 + .../api/suggestions/{index.js => index.ts} | 6 +- ...tions.js => register_value_suggestions.ts} | 89 ++++++++++++------- .../data/common/index_patterns/types.ts | 1 + src/plugins/data/server/index.ts | 1 + .../data/server/index_patterns/index.ts | 2 + .../data/server/index_patterns/utils.ts | 51 +++++++++++ 7 files changed, 114 insertions(+), 37 deletions(-) rename src/legacy/core_plugins/kibana/server/routes/api/suggestions/{index.js => index.ts} (84%) rename src/legacy/core_plugins/kibana/server/routes/api/suggestions/{register_value_suggestions.js => register_value_suggestions.ts} (57%) create mode 100644 src/plugins/data/server/index_patterns/utils.ts diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index 4cbb1c82cc1e40c..611289aa1c8cba0 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -526,4 +526,5 @@ export interface ElasticsearchPlugin { getCluster(name: string): Cluster; createCluster(name: string, config: ClusterConfig): Cluster; waitUntilReady(): Promise; + handleESError: (error: any) => Error; } diff --git a/src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.js b/src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.ts similarity index 84% rename from src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.js rename to src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.ts index 15110b192ee33c1..a15d709b79987c8 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/suggestions/index.ts @@ -17,8 +17,4 @@ * under the License. */ -import { registerValueSuggestions } from './register_value_suggestions'; - -export function registerSuggestionsApi(server) { - registerValueSuggestions(server); -} +export { registerValueSuggestions } from './register_value_suggestions'; 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.ts similarity index 57% rename from src/legacy/core_plugins/kibana/server/routes/api/suggestions/register_value_suggestions.js rename to src/legacy/core_plugins/kibana/server/routes/api/suggestions/register_value_suggestions.ts index 1f5ed443fee80e1..e5f358f692c9992 100644 --- 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.ts @@ -17,42 +17,47 @@ * under the License. */ +import { Legacy } from 'kibana'; import { get, map } from 'lodash'; + +import { + IFieldType, + indexPatternsUtils, + esFilters, +} from '../../../../../../../plugins/data/server'; + +interface ISuggestionsValuesPayload { + fieldName: string; + query: string; + boolFilter: esFilters.Filter[]; +} + +// @ts-ignore 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'); +const PATH = '/api/kibana/suggestions/values/{index}'; + +export function registerValueSuggestions(server: Legacy.Server) { server.route({ - path: '/api/kibana/suggestions/values/{index}', + path: PATH, method: ['POST'], - handler: abortableRequestHandler(async function (signal, req) { + handler: abortableRequestHandler(async (signal: AbortSignal, req: Legacy.Request) => { const { index } = req.params; - const { field: fieldName, query, boolFilter } = req.payload; + const { fieldName, query, boolFilter } = req.payload as ISuggestionsValuesPayload; 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 - ); + + const indexPattern = await indexPatternsUtils.findIndexPatternById(savedObjectsClient, index); + const field = indexPatternsUtils.getFieldByName(fieldName, indexPattern); + const body = getBody(server, field || fieldName, query, boolFilter); 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; + const buckets: any[] = + get(response, 'aggregations.suggestions.buckets') || + get(response, 'aggregations.nestedSuggestions.suggestions.buckets'); + + return map(buckets || [], 'key'); } catch (error) { throw server.plugins.elasticsearch.handleESError(error); } @@ -60,7 +65,12 @@ export function registerValueSuggestions(server) { }); } -function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) { +function getBody( + server: Legacy.Server, + field: IFieldType | string, + query: string, + boolFilter: esFilters.Filter[] +) { // Helps ensure that the regex is not evaluated eagerly against the terms dictionary const executionHint = 'map'; @@ -68,10 +78,12 @@ function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) { // the amount of information that needs to be transmitted to the coordinating node const shardSize = 10; + const { timeout, terminate_after } = getAutocompleteOptions(server); + const body = { size: 0, - timeout: `${timeout}ms`, - terminate_after: terminateAfter, + timeout, + terminate_after, query: { bool: { filter: boolFilter, @@ -80,7 +92,7 @@ function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) { aggs: { suggestions: { terms: { - field: field.name || field, + field: isFieldObject(field) ? field.name : field, include: `${getEscapedQuery(query)}.*`, execution_hint: executionHint, shard_size: shardSize, @@ -89,7 +101,7 @@ function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) { }, }; - if (field.subType && field.subType.nested) { + if (isFieldObject(field) && field.subType && field.subType.nested) { return { ...body, aggs: { @@ -99,14 +111,27 @@ function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) { }, aggs: body.aggs, }, - } + }, }; } return body; } -function getEscapedQuery(query = '') { +function getAutocompleteOptions(server: Legacy.Server) { + const serverConfig = server.config(); + + return { + terminate_after: serverConfig.get('kibana.autocompleteTerminateAfter'), + timeout: `${serverConfig.get('kibana.autocompleteTimeout')}ms`, + }; +} + +function isFieldObject(field: any): field is IFieldType { + return Boolean(field && field.name); +} + +function getEscapedQuery(query: string = '') { // 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 0a1ba4834224429..98cdd20ea4b8463 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/index.ts b/src/plugins/data/server/index.ts index 81906a63bd49dd4..00d855298251a8f 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -29,6 +29,7 @@ export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues, + indexPatternsUtils, } from './index_patterns'; export * from './search'; diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 6937fa22c4e5d12..c20bb719612326b 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 indexPatternsUtils from './utils'; export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; export { IndexPatternsService } from './index_patterns_service'; +export { indexPatternsUtils }; 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 000000000000000..937d8c67db96365 --- /dev/null +++ b/src/plugins/data/server/index_patterns/utils.ts @@ -0,0 +1,51 @@ +/* + * 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 { Server } from 'kibana'; +import { IIndexPattern, IFieldType } from '../../common'; + +export const getFieldByName = ( + fieldName: string, + indexPattern?: IIndexPattern +): IFieldType | undefined => { + if (indexPattern) { + const fields: IFieldType[] = indexPattern && JSON.parse(indexPattern.attributes.fields); + const field = fields && fields.find(f => f.name === fieldName); + + if (field) { + return field; + } + } +}; + +export const findIndexPatternById = async ( + savedObjectsClient: Server.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; + } +};