Skip to content

Commit

Permalink
value suggestions server route -> data plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
alexwizp committed Dec 12, 2019
1 parent 989a349 commit f3834dd
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 37 deletions.
1 change: 1 addition & 0 deletions src/legacy/core_plugins/elasticsearch/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,4 +526,5 @@ export interface ElasticsearchPlugin {
getCluster(name: string): Cluster;
createCluster(name: string, config: ClusterConfig): Cluster;
waitUntilReady(): Promise<void>;
handleESError: (error: any) => Error;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -17,61 +17,73 @@
* 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);
}
}),
});
}

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';

// 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 { timeout, terminate_after } = getAutocompleteOptions(server);

const body = {
size: 0,
timeout: `${timeout}ms`,
terminate_after: terminateAfter,
timeout,
terminate_after,
query: {
bool: {
filter: boolFilter,
Expand All @@ -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,
Expand All @@ -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: {
Expand All @@ -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<number>('kibana.autocompleteTerminateAfter'),
timeout: `${serverConfig.get<number>('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}`);
}
1 change: 1 addition & 0 deletions src/plugins/data/common/index_patterns/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { IFieldType } from './fields';

export interface IIndexPattern {
[key: string]: any;
fields: IFieldType[];
title: string;
id?: string;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
IndexPatternsFetcher,
FieldDescriptor,
shouldReadFieldFromDocValues,
indexPatternsUtils,
} from './index_patterns';

export * from './search';
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data/server/index_patterns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
51 changes: 51 additions & 0 deletions src/plugins/data/server/index_patterns/utils.ts
Original file line number Diff line number Diff line change
@@ -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<IIndexPattern | undefined> => {
const savedObjectsResponse = await savedObjectsClient.find<any>({
type: 'index-pattern',
fields: ['fields'],
search: `"${index}"`,
searchFields: ['title'],
});

if (savedObjectsResponse.total > 0) {
return (savedObjectsResponse.saved_objects[0] as unknown) as IIndexPattern;
}
};

0 comments on commit f3834dd

Please sign in to comment.