Skip to content

Commit

Permalink
[ES|QL] Recommended queries (elastic#194418)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic#187325

<img width="927" alt="image"
src="https://github.com/user-attachments/assets/46220c26-f54c-4bd7-9a8b-d1d29591dc68">

<img width="539" alt="image"
src="https://github.com/user-attachments/assets/f4d938af-f2b6-400d-918f-3dcf1d22618f">


This is the first iteration of this feature. We want to help the users
familiarize themselves with popular operations. This PR:
- adds the recommended queries list in the help menu of unified search
- adds the list after the users select a datasource with the from
command
- adds the list in the editor's empty state

### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
stratoula authored Oct 8, 2024
1 parent 560d561 commit 149e801
Show file tree
Hide file tree
Showing 16 changed files with 424 additions and 122 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-esql-validation-autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ export {
} from './src/shared/resources_helpers';

export { wrapAsEditorMessage } from './src/code_actions/utils';

export { getRecommendedQueries } from './src/autocomplete/recommended_queries/templates';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { METADATA_FIELDS } from '../../shared/constants';
import { setup, indexes, integrations } from './helpers';
import { getRecommendedQueries } from '../recommended_queries/templates';

const visibleIndices = indexes
.filter(({ hidden }) => !hidden)
Expand Down Expand Up @@ -72,8 +73,17 @@ describe('autocomplete.suggest', () => {
const metadataFieldsAndIndex = metadataFields.filter((field) => field !== '_index');

test('on <kbd>SPACE</kbd> without comma ",", suggests adding metadata', async () => {
const recommendedQueries = getRecommendedQueries({
fromCommand: '',
timeField: 'dateField',
});
const { assertSuggestions } = await setup();
const expected = ['METADATA $0', ',', '| '].sort();
const expected = [
'METADATA $0',
',',
'| ',
...recommendedQueries.map((query) => query.queryString),
].sort();

await assertSuggestions('from a, b /', expected);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,9 @@ const setup = async (caret = '?') => {
};

describe('autocomplete.suggest', () => {
test('does not load fields when suggesting within a single FROM, SHOW, ROW command', async () => {
test('does not load fields when suggesting within a single SHOW, ROW command', async () => {
const { suggest, callbacks } = await setup();

await suggest('FROM kib, ? |');
await suggest('FROM ?');
await suggest('FROM ? |');
await suggest('sHoW ?');
await suggest('row ? |');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ import {
import { METADATA_FIELDS } from '../shared/constants';
import { ESQL_COMMON_NUMERIC_TYPES, ESQL_STRING_TYPES } from '../shared/esql_types';
import { log10ParameterTypes, powParameterTypes } from './__tests__/constants';
import { getRecommendedQueries } from './recommended_queries/templates';

const commandDefinitions = unmodifiedCommandDefinitions.filter(({ hidden }) => !hidden);

const getRecommendedQueriesSuggestions = (fromCommand: string, timeField?: string) =>
getRecommendedQueries({
fromCommand,
timeField,
});

describe('autocomplete', () => {
type TestArgs = [
string,
Expand Down Expand Up @@ -82,10 +90,11 @@ describe('autocomplete', () => {
const sourceCommands = ['row', 'from', 'show'];

describe('New command', () => {
testSuggestions(
'/',
sourceCommands.map((name) => name.toUpperCase() + ' $0')
);
const recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField');
testSuggestions('/', [
...sourceCommands.map((name) => name.toUpperCase() + ' $0'),
...recommendedQuerySuggestions.map((q) => q.queryString),
]);
testSuggestions(
'from a | /',
commandDefinitions
Expand Down Expand Up @@ -523,10 +532,11 @@ describe('autocomplete', () => {
*/
describe('Invoke trigger kind (all commands)', () => {
// source command
testSuggestions(
'f/',
sourceCommands.map((cmd) => `${cmd.toUpperCase()} $0`)
);
let recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField');
testSuggestions('f/', [
...sourceCommands.map((cmd) => `${cmd.toUpperCase()} $0`),
...recommendedQuerySuggestions.map((q) => q.queryString),
]);

// pipe command
testSuggestions(
Expand Down Expand Up @@ -575,7 +585,13 @@ describe('autocomplete', () => {
]);

// FROM source METADATA
testSuggestions('FROM index1 M/', [',', 'METADATA $0', '| ']);
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField');
testSuggestions('FROM index1 M/', [
',',
'METADATA $0',
'| ',
...recommendedQuerySuggestions.map((q) => q.queryString),
]);

// FROM source METADATA field
testSuggestions('FROM index1 METADATA _/', METADATA_FIELDS);
Expand Down Expand Up @@ -710,12 +726,12 @@ describe('autocomplete', () => {
...s,
asSnippet: true,
});

let recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField');
// Source command
testSuggestions(
'F/',
['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet)
);
testSuggestions('F/', [
...['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet),
...recommendedQuerySuggestions.map((q) => q.queryString),
]);

// Pipe command
testSuggestions(
Expand Down Expand Up @@ -787,11 +803,14 @@ describe('autocomplete', () => {
);
});

recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField');

// PIPE (|)
testSuggestions('FROM a /', [
attachTriggerCommand('| '),
',',
attachAsSnippet(attachTriggerCommand('METADATA $0')),
...recommendedQuerySuggestions.map((q) => q.queryString),
]);

// Assignment
Expand Down Expand Up @@ -833,13 +852,15 @@ describe('autocomplete', () => {
],
]
);
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('index1', 'dateField');

testSuggestions(
'FROM index1/',
[
{ text: 'index1 | ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND },
{ text: 'index1, ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND },
{ text: 'index1 METADATA ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND },
...recommendedQuerySuggestions.map((q) => q.queryString),
],
undefined,
[
Expand All @@ -851,12 +872,14 @@ describe('autocomplete', () => {
]
);

recommendedQuerySuggestions = getRecommendedQueriesSuggestions('index2', 'dateField');
testSuggestions(
'FROM index1, index2/',
[
{ text: 'index2 | ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND },
{ text: 'index2, ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND },
{ text: 'index2 METADATA ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND },
...recommendedQuerySuggestions.map((q) => q.queryString),
],
undefined,
[
Expand All @@ -872,6 +895,7 @@ describe('autocomplete', () => {
// meaning that Monaco by default will only set the replacement
// range to cover "bar" and not "foo$bar". We have to make sure
// we're setting it ourselves.
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('foo$bar', 'dateField');
testSuggestions(
'FROM foo$bar/',
[
Expand All @@ -894,18 +918,21 @@ describe('autocomplete', () => {
command: TRIGGER_SUGGESTION_COMMAND,
rangeToReplace: { start: 6, end: 13 },
},
...recommendedQuerySuggestions.map((q) => q.queryString),
],
undefined,
[, [{ name: 'foo$bar', hidden: false }]]
);

// This is an identifier that matches multiple sources
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('i*', 'dateField');
testSuggestions(
'FROM i*/',
[
{ text: 'i* | ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND },
{ text: 'i*, ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND },
{ text: 'i* METADATA ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND },
...recommendedQuerySuggestions.map((q) => q.queryString),
],
undefined,
[
Expand All @@ -918,11 +945,13 @@ describe('autocomplete', () => {
);
});

recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField');
// FROM source METADATA
testSuggestions('FROM index1 M/', [
',',
attachAsSnippet(attachTriggerCommand('METADATA $0')),
'| ',
...recommendedQuerySuggestions.map((q) => q.queryString),
]);

describe('ENRICH', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
} from '@kbn/esql-ast';
import { i18n } from '@kbn/i18n';
import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types';
import type { EditorContext, ItemKind, SuggestionRawDefinition } from './types';
import type { EditorContext, ItemKind, SuggestionRawDefinition, GetFieldsByTypeFn } from './types';
import {
getColumnForASTNode,
getCommandDefinition,
Expand Down Expand Up @@ -113,12 +113,8 @@ import {
import { metadataOption } from '../definitions/options';
import { comparisonFunctions } from '../definitions/builtin';
import { countBracketsUnclosed } from '../shared/helpers';
import { getRecommendedQueriesSuggestions } from './recommended_queries/suggestions';

type GetFieldsByTypeFn = (
type: string | string[],
ignored?: string[],
options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean }
) => Promise<SuggestionRawDefinition[]>;
type GetFieldsMapFn = () => Promise<Map<string, ESQLRealField>>;
type GetPoliciesFn = () => Promise<SuggestionRawDefinition[]>;
type GetPolicyMetadataFn = (name: string) => Promise<ESQLPolicy | undefined>;
Expand Down Expand Up @@ -176,7 +172,7 @@ export async function suggest(
);

const { getFieldsByType, getFieldsMap } = getFieldsByTypeRetriever(
queryForFields,
queryForFields.replace(EDITOR_MARKER, ''),
resourceRetriever
);
const getSources = getSourcesHelper(resourceRetriever);
Expand All @@ -187,7 +183,26 @@ export async function suggest(
// filter source commands if already defined
const suggestions = commandAutocompleteDefinitions;
if (!ast.length) {
return suggestions.filter(isSourceCommand);
// Display the recommended queries if there are no commands (empty state)
const recommendedQueriesSuggestions: SuggestionRawDefinition[] = [];
if (getSources) {
let fromCommand = '';
const sources = await getSources();
const visibleSources = sources.filter((source) => !source.hidden);
if (visibleSources.find((source) => source.name.startsWith('logs'))) {
fromCommand = 'FROM logs*';
} else fromCommand = `FROM ${visibleSources[0].name}`;

const { getFieldsByType: getFieldsByTypeEmptyState } = getFieldsByTypeRetriever(
fromCommand,
resourceRetriever
);
recommendedQueriesSuggestions.push(
...(await getRecommendedQueriesSuggestions(getFieldsByTypeEmptyState, fromCommand))
);
}
const sourceCommandsSuggestions = suggestions.filter(isSourceCommand);
return [...sourceCommandsSuggestions, ...recommendedQueriesSuggestions];
}

return suggestions.filter((def) => !isSourceCommand(def));
Expand Down Expand Up @@ -519,6 +534,7 @@ async function getExpressionSuggestionsByType(
const optArg = optionsAlreadyDeclared.find(({ name: optionName }) => optionName === name);
return (!optArg && !optionsAlreadyDeclared.length) || (optArg && index > optArg.index);
});
const hasRecommendedQueries = Boolean(commandDef?.hasRecommendedQueries);
// get the next definition for the given command
let argDef = commandDef.signature.params[argIndex];
// tune it for the variadic case
Expand Down Expand Up @@ -910,6 +926,11 @@ async function getExpressionSuggestionsByType(

if (lastIndex && lastIndex.text && lastIndex.text !== EDITOR_MARKER) {
const sources = await getSources();

const recommendedQueriesSuggestions = hasRecommendedQueries
? await getRecommendedQueriesSuggestions(getFieldsByType)
: [];

const suggestionsToAdd = await handleFragment(
innerText,
(fragment) =>
Expand Down Expand Up @@ -952,8 +973,13 @@ async function getExpressionSuggestionsByType(
asSnippet: false, // turn this off because $ could be contained within the source name
rangeToReplace,
},
...recommendedQueriesSuggestions.map((suggestion) => ({
...suggestion,
rangeToReplace,
filterText: fragment,
text: fragment + suggestion.text,
})),
];

return _suggestions;
}
}
Expand Down Expand Up @@ -1005,6 +1031,11 @@ async function getExpressionSuggestionsByType(
}));
suggestions.push(...finalSuggestions);
}

// handle recommended queries for from
if (hasRecommendedQueries) {
suggestions.push(...(await getRecommendedQueriesSuggestions(getFieldsByType)));
}
}
// Due to some logic overlapping functions can be repeated
// so dedupe here based on text string (it can differ from name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ export function strictlyGetParamAtPosition(
export function getQueryForFields(queryString: string, commands: ESQLCommand[]) {
// If there is only one source command and it does not require fields, do not
// fetch fields, hence return an empty string.
return commands.length === 1 && ['from', 'row', 'show'].includes(commands[0].name)
? ''
: queryString;
return commands.length === 1 && ['row', 'show'].includes(commands[0].name) ? '' : queryString;
}

export function getSourcesFromCommands(commands: ESQLCommand[], sourceType: 'index' | 'policy') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { SuggestionRawDefinition, GetFieldsByTypeFn } from '../types';
import { getRecommendedQueries } from './templates';

export const getRecommendedQueriesSuggestions = async (
getFieldsByType: GetFieldsByTypeFn,
fromCommand: string = ''
): Promise<SuggestionRawDefinition[]> => {
const fieldSuggestions = await getFieldsByType('date', [], {
openSuggestions: true,
});
let timeField = '';
if (fieldSuggestions.length) {
timeField =
fieldSuggestions?.find((field) => field.label === '@timestamp')?.label ||
fieldSuggestions[0].label;
}

const recommendedQueries = getRecommendedQueries({ fromCommand, timeField });

const suggestions: SuggestionRawDefinition[] = recommendedQueries.map((query) => {
return {
label: query.label,
text: query.queryString,
kind: 'Issue',
detail: query.description,
sortText: 'D',
};
});

return suggestions;
};
Loading

0 comments on commit 149e801

Please sign in to comment.