-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[ES|QL][Discover] Highlight results when using ES|QL highlight #271519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
733c2f4
highlighting PoC
sddonne fd5bb82
implement getColumnsToHighlight
sddonne 08b188d
Merge remote-tracking branch 'upstream/main' into highlight-results
sddonne 58cbc1e
fix typo
sddonne d839a05
Merge remote-tracking branch 'upstream/main' into highlight-results
sddonne 32add56
move highlighted columns map into the hit
sddonne 628c860
update formatters
sddonne 67e2fcf
clean
sddonne a92043f
clean
sddonne 4d8f508
clean
sddonne d51313b
Merge remote-tracking branch 'upstream/main' into highlight-results
sddonne f1b53c7
reorder field in the summary column
sddonne db1101b
Changes from node scripts/lint_ts_projects --fix
kibanamachine d682efa
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine 5d8e534
Changes from node scripts/check
kibanamachine 1f696bd
Merge remote-tracking branch 'upstream/main' into highlight-results
sddonne 3c9c10d
add fetch test
sddonne 47fb464
fix: push empty object
sddonne 7adea4d
fix circular dep
sddonne 7cb9b27
fix types
sddonne d41b7b7
Merge branch 'highlight-results' of https://github.com/sddonne/kibana…
sddonne 0e9991e
clean definitions files
sddonne 972b83f
clean dependencies
sddonne 01284d0
add test
sddonne 1ce8af9
Merge branch 'main' into highlight-results
stratoula 88f8892
Address feedback
sddonne 81c0703
Update comment
sddonne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
src/platform/packages/shared/kbn-esql-utils/src/utils/get_columns_with_highlights.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| /* | ||
| * 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 { | ||
| DEFAULT_HIGHLIGHT_POST_TAG, | ||
| DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| getColumnsWithHighlights, | ||
| } from './get_columns_with_highlights'; | ||
|
|
||
| describe('getColumnsWithHighlights', () => { | ||
| it('returns column with default em tags when highlight is enabled', () => { | ||
| const query = | ||
| 'FROM books | EVAL snippets = TOP_SNIPPETS(description, "Tolkien", { "highlight": true })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| snippets: { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('returns custom pre_tag and post_tag from TOP_SNIPPETS options', () => { | ||
| const query = | ||
| 'FROM books | EVAL snippets = TOP_SNIPPETS(description, "Tolkien", { "highlight": true, "pre_tag": "<mark>", "post_tag": "</mark>" })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| snippets: { | ||
| preTag: '<mark>', | ||
| postTag: '</mark>', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('ignores TOP_SNIPPETS without highlight option', () => { | ||
| const query = 'FROM books | EVAL snippets = TOP_SNIPPETS(description, "Tolkien")'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({}); | ||
| }); | ||
|
|
||
| it('returns multiple columns with their respective tags', () => { | ||
| const query = | ||
| 'FROM books | EVAL a = TOP_SNIPPETS(description, "one", { "highlight": true }) | EVAL b = TOP_SNIPPETS(title, "two", { "highlight": true, "pre_tag": "<mark>", "post_tag": "</mark>" })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| a: { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| b: { | ||
| preTag: '<mark>', | ||
| postTag: '</mark>', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('handles EVAL unamed columns scenarios', () => { | ||
| const query = 'FROM books | EVAL TOP_SNIPPETS(description, "one", { "highlight": true })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| 'TOP_SNIPPETS(description, "one", { "highlight": true })': { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('handles STATS user defined columns', () => { | ||
| const query = | ||
| 'FROM books | STATS count(*) BY col0 = TOP_SNIPPETS(description, "one", { "highlight": true })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| col0: { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('handles STATS unamed user defined columns', () => { | ||
| const query = | ||
| 'FROM books | STATS count(*) BY TOP_SNIPPETS(description, "one", { "highlight": true })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| 'TOP_SNIPPETS(description, "one", { "highlight": true })': { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('applies RENAME to resolved highlight column names', () => { | ||
| const query = | ||
| 'FROM books | EVAL col0 = TOP_SNIPPETS(description, "one", { "highlight": true }) | RENAME col0 AS renamed'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| renamed: { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('can handle columns defined within quotes', () => { | ||
| const query = | ||
| 'FROM books | EVAL `col0` = TOP_SNIPPETS(description, "one", { "highlight": true })'; | ||
| expect(getColumnsWithHighlights(query)).toEqual({ | ||
| col0: { | ||
| preTag: DEFAULT_HIGHLIGHT_PRE_TAG, | ||
| postTag: DEFAULT_HIGHLIGHT_POST_TAG, | ||
| }, | ||
| }); | ||
| }); | ||
| }); |
160 changes: 160 additions & 0 deletions
160
src/platform/packages/shared/kbn-esql-utils/src/utils/get_columns_with_highlights.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| /* | ||
| * 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 { | ||
| isAssignment, | ||
| isBooleanLiteral, | ||
| isColumn, | ||
| isMap, | ||
| isStringLiteral, | ||
| LeafPrinter, | ||
| Parser, | ||
| Walker, | ||
| } from '@elastic/esql'; | ||
|
|
||
| import type { ESQLAstQueryExpression, ESQLFunction, ESQLMap } from '@elastic/esql/types'; | ||
| import { replaceColumnNamesIfRenamed } from './query_parsing_helpers'; | ||
|
|
||
| export const DEFAULT_HIGHLIGHT_PRE_TAG = '<em>'; | ||
| export const DEFAULT_HIGHLIGHT_POST_TAG = '</em>'; | ||
|
|
||
| const HIGHLIGHT_OPTION_NAME = 'highlight'; | ||
| const PRE_TAG_OPTION_NAME = 'pre_tag'; | ||
| const POST_TAG_OPTION_NAME = 'post_tag'; | ||
|
|
||
| /** | ||
| * ES|QL functions that can produce highlight markup in output columns when | ||
| * called with `{ "highlight": true }`. | ||
| */ | ||
| export const FUNCTIONS_WITH_HIGHLIGHT_SUPPORT = ['top_snippets']; | ||
|
stratoula marked this conversation as resolved.
Outdated
|
||
|
|
||
| export interface ESQLHighlightTags { | ||
| preTag: string; | ||
| postTag: string; | ||
| } | ||
|
|
||
| export type EsqlColumnsWithHighlights = Record<string, ESQLHighlightTags>; | ||
|
|
||
| /** | ||
| * Returns columns built using a highlighting algorithm, | ||
| * including the opening and closing markup tags configured for each column. | ||
| * | ||
| * Example: | ||
| * ``` | ||
| * FROM books | ||
| * | EVAL snippets = TOP_SNIPPETS(description, "Tolkien", { "highlight": true }) | ||
| * | EVAL titles = TOP_SNIPPETS(title, "Tolkien", { "highlight": true, "pre_tag": "<mark>", "post_tag": "</mark>" }) | ||
| * ``` | ||
| * Will return the following map: | ||
| * ``` | ||
| * { | ||
| * snippets: { | ||
| * preTag: '<em>', | ||
| * postTag: '</em>', | ||
| * }, | ||
| * titles: { | ||
| * preTag: '<mark>', | ||
| * postTag: '</mark>', | ||
| * }, | ||
| * } | ||
| */ | ||
| export function getColumnsWithHighlights(query: string): EsqlColumnsWithHighlights { | ||
| const columnsWithHighlights: EsqlColumnsWithHighlights = {}; | ||
| const { root } = Parser.parse(query); | ||
|
|
||
| const highlightFunctionsCandidates = Walker.findAll( | ||
| root, | ||
| (node) => node.type === 'function' && FUNCTIONS_WITH_HIGHLIGHT_SUPPORT.includes(node.name) | ||
| ) as ESQLFunction[]; | ||
|
|
||
| for (const fn of highlightFunctionsCandidates) { | ||
| const optionsMap = fn.args.find(isMap); | ||
|
|
||
| if (!optionsMap || !isHighlightEnabled(optionsMap)) { | ||
| continue; | ||
| } | ||
|
|
||
| const columnName = getHighlightedColumnName(root, fn, query); | ||
| if (!columnName) { | ||
| continue; | ||
| } | ||
|
|
||
| const preTag = | ||
| getHighlightTagName(optionsMap, PRE_TAG_OPTION_NAME) ?? DEFAULT_HIGHLIGHT_PRE_TAG; | ||
| const postTag = | ||
| getHighlightTagName(optionsMap, POST_TAG_OPTION_NAME) ?? DEFAULT_HIGHLIGHT_POST_TAG; | ||
|
|
||
| // Check if the column name has been renamed in the query | ||
| const [resolvedColumnName] = replaceColumnNamesIfRenamed(root, [columnName]); | ||
|
|
||
| columnsWithHighlights[resolvedColumnName] = { | ||
| preTag, | ||
| postTag, | ||
| }; | ||
| } | ||
|
|
||
| return columnsWithHighlights; | ||
| } | ||
|
|
||
| /** | ||
| * Given a map of options, returns true if the `highlight` option is set to `true`. | ||
| */ | ||
| const isHighlightEnabled = (optionsMap: ESQLMap): boolean => { | ||
| const highlightEntry = optionsMap.entries.find( | ||
| (entry) => isStringLiteral(entry.key) && entry.key.valueUnquoted === HIGHLIGHT_OPTION_NAME | ||
| ); | ||
| if (!highlightEntry?.value) { | ||
| return false; | ||
| } | ||
|
|
||
| return ( | ||
| isBooleanLiteral(highlightEntry.value) && highlightEntry.value.value.toLowerCase() === 'true' | ||
|
stratoula marked this conversation as resolved.
|
||
| ); | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the tag name defined in the map options if it exists. | ||
| */ | ||
| const getHighlightTagName = (optionsMap: ESQLMap, optionName: string): string | undefined => { | ||
| const tagEntry = optionsMap.entries.find( | ||
| (entry) => isStringLiteral(entry.key) && entry.key.valueUnquoted === optionName | ||
| ); | ||
|
|
||
| if (!tagEntry?.value) { | ||
| return undefined; | ||
| } | ||
| if (!isStringLiteral(tagEntry.value)) { | ||
| return undefined; | ||
| } | ||
| return tagEntry.value.valueUnquoted; | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the name of the column that was created using the highlight function. | ||
| * | ||
| * This function has an heuristic part, some combination of function could remove the highlighting tokens from the result. | ||
| * But it assumes that if the user used highlight:true, it's not interested in removing them. | ||
| * Doing a 100% accurate check would involve knowing the semantics of every invoked function. | ||
| * In the worst case of having a false positive, | ||
|
stratoula marked this conversation as resolved.
Outdated
|
||
| */ | ||
| const getHighlightedColumnName = ( | ||
| root: ESQLAstQueryExpression, | ||
| highlightFunction: ESQLFunction, | ||
| query: string | ||
| ): string | undefined => { | ||
| // Created using an assignment | EVAL col = TOP_SNIPPETS( ... | ||
| for (const parent of Walker.parents(root, highlightFunction)) { | ||
| if (isAssignment(parent) && isColumn(parent.args[0])) { | ||
| return LeafPrinter.column(parent.args[0]); | ||
| } | ||
| } | ||
|
|
||
| // Created using an expression text | EVAL TOP_SNIPPETS( ... or STATS count(*) BY TOP_SNIPPETS( ... | ||
| return query.substring(highlightFunction.location.min, highlightFunction.location.max + 1); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used to order highlighted fields at the start of the summary column.