Loading

ES|QL HIGHLIGHT command

The HIGHLIGHT processing command extracts and highlights matching text snippets from one or more fields based on a full-text query. Matching terms are wrapped in highlight tags, bringing the highlighting features of the Elasticsearch _search API to ES|QL.

HIGHLIGHT [prefix = "<prefix>"] query ON field [, field, ...] [WITH { "option": value [, ...] }]
		
prefix
(Optional) A quoted string literal used to name the output columns. Each highlighted field is written to <prefix><field>. Defaults to highlight_ (for example, HIGHLIGHT "fox" ON content produces highlight_content). If a generated column name matches an existing column, the existing column is replaced. To overwrite the source column in place, specify an empty prefix (prefix = ""). Unlike the query and the WITH option values, prefix cannot be a query parameter.
query

The query used to find matching terms to highlight. This can be a string literal (which uses query_string syntax) or a full-text search function such as MATCH, MATCH_PHRASE, QSTR, KQL, or the match operator :. You can combine full-text functions using AND, OR, and NOT. Unqualified strings and QSTR expressions are evaluated against all fields listed in ON. All fields referenced in the query must also be listed in ON. If an unlisted field is referenced, the query is rejected before execution (for example, HIGHLIGHT query field [title] is not in ON fields [body]).

Queries without positive match conditions (such as NOT MATCH(...)) have no terms to highlight and return null, unless no_match_size is configured.

field
One or more comma-separated columns to highlight. Fields must be text or keyword types (semantic_text fields are supported and treated as text). Wildcard column names are not supported. If a field has no matching terms, its output is null unless you set no_match_size.

All option values passed in the WITH clause must be constants. Both literals and query parameters that resolve to a literal are accepted; column references are not.

pre_tags
(Optional) Opening tag inserted before each highlighted term. Accepts a string or a single-element array of strings. Defaults to <em>. Multiple rotating tags are not supported.
post_tags
(Optional) Closing tag inserted after each highlighted term. Accepts a string or a single-element array of strings. Defaults to </em>.
encoder
(Optional) Text encoding applied before adding highlight tags. Accepts default (no encoding) or html (HTML-escapes snippet text). Defaults to default. As in the _search API, this value is case-sensitive, so html is valid but HTML is rejected. boundary_scanner and order are case-insensitive.
analyzer
(Optional) Analyzer used on both the query and field text. Defaults to the standard analyzer. Only built-in and node-level plugin analyzers are supported. If a full-text search function specifies its own analyzer, it must match the analyzer specified here.
number_of_fragments
(Optional) Maximum number of snippets (fragments) to return per field. Set to 0 to return the entire field value with matching terms highlighted without fragmenting. Must be >= 0. Defaults to 5.
fragment_size
(Optional) Approximate character length of each snippet. Must be >= 0. Defaults to 100.
no_match_size
(Optional) Approximate number of leading characters to return from the field when there are no matching terms. This is a minimum, not an exact limit: the returned text extends to the next boundary set by boundary_scanner, so the result can be longer than the requested size. Must be >= 0. Defaults to 0 (returns null).
boundary_scanner
(Optional) Boundary scanner used to split text into fragments. Accepts sentence or word, case-insensitively. Defaults to sentence.
boundary_scanner_locale
(Optional) Locale used by the boundary scanner, given as an IETF BCP 47 language tag such as en-US or ja-JP. Use hyphens as separators. Defaults to the root locale. This is the same format accepted by the _search API's boundary_scanner_locale.
order
(Optional) Sort order of returned fragments. Accepts none (preserves document order) or score (orders fragments by descending relevance score), case-insensitively. Defaults to none.
max_analyzed_offset
(Optional) Maximum number of characters to analyze per field value. Accepts a positive integer, or -1 to leave the limit unset. Defaults to -1. HIGHLIGHT analyzes at most 1 million characters per field value regardless of this setting, and the index's index.highlight.max_analyzed_offset setting does not apply. Text beyond the effective offset is not highlighted.

Use HIGHLIGHT to find and display matching snippets in text fields, typically after filtering rows with a full-text search condition in WHERE.

HIGHLIGHT processes each row, analyzes the specified fields against the query, and generates new keyword columns containing matching terms wrapped in highlight tags. By default, output columns are named highlight_<field>. If a field contains no matching terms, the result is null unless you specify no_match_size.

Because HIGHLIGHT re-analyzes text values at query time, you can highlight source fields from an index as well as computed columns created by earlier commands like EVAL, DISSECT, GROK, STATS, ENRICH, or LOOKUP JOIN.

For multivalued fields, each value is highlighted independently:

  • Phrase queries and fragment boundaries do not cross values.
  • When a field produces multiple fragments, the output column contains a multivalued list of snippets.
  • Multivalued keyword fields loaded from doc values are sorted and deduplicated before highlighting, which can result in a different snippet order compared to the _search API.
Tip

Learn more about using ES|QL for search use cases.

  • HIGHLIGHT re-analyzes text with the standard analyzer by default, rather than the analyzer configured in the index mapping. If your field uses a custom or language analyzer, specify it with the analyzer option in the WITH clause.
  • The analyzer option only supports built-in and node-level plugin analyzers. Analyzers configured in index settings are not supported.
  • On keyword fields, HIGHLIGHT tokenizes text and breaks it into snippets like a text field, rather than treating the value as a single term.
  • On semantic_text fields, HIGHLIGHT performs lexical matching against the underlying text. Semantic vector matches without literal keyword overlap are not highlighted.
  • Fields are analyzed up to a maximum of 1 million characters. Text beyond this limit is not analyzed or highlighted.

The following examples show common ways to highlight search terms and customize snippet output.

Wrap matching terms in the default <em> tags:

ROW content = "The quick brown fox jumps over the lazy dog."
| HIGHLIGHT "fox" ON content
| KEEP highlight_content
		
highlight_content:keyword
The quick brown <em>fox</em> jumps over the lazy dog.

Filter rows with a WHERE clause, then highlight the matching terms in the output:

FROM books
| WHERE MATCH(title, "Return")
| HIGHLIGHT "return" ON title
| KEEP book_no, highlight_title
| SORT book_no
		
book_no:keyword highlight_title:keyword
2714 <em>Return</em> of the King Being the Third Part of The Lord of the Rings
7350 <em>Return</em> of the Shadow

Use a full-text function like MATCH_PHRASE to highlight an exact phrase in a single tag pair:

FROM books
| WHERE MATCH(title, "Return")
| HIGHLIGHT MATCH_PHRASE(title, "Return of the") ON title
| KEEP book_no, highlight_title
| SORT book_no
		
book_no:keyword highlight_title:keyword
2714 <em>Return of the</em> King Being the Third Part of The Lord of the Rings
7350 <em>Return of the</em> Shadow

Use QSTR to highlight terms using Lucene query syntax with boolean operators and field qualifiers:

ROW title = "The quick fox", body = "A loyal dog"
| HIGHLIGHT QSTR("title:fox OR body:dog") ON title, body
| KEEP highlight_title, highlight_body
		
highlight_title:keyword highlight_body:keyword
The quick <em>fox</em> A loyal <em>dog</em>

Use KQL to highlight terms using Kibana Query Language syntax, optionally combined with other full-text functions:

FROM books
| WHERE MATCH(title, "Return")
| HIGHLIGHT KQL("title: shad*") OR (MATCH(title, "return") AND MATCH(title, "king")) ON title
| KEEP book_no, highlight_title
| SORT book_no
		
book_no:keyword highlight_title:keyword
2714 <em>Return</em> of the <em>King</em> Being the Third Part of The Lord of the Rings
7350 Return of the <em>Shadow</em>

Use the analyzer option to apply language-specific stemming rules. In this example, the english analyzer stems Rings to ring:

ROW title = "The Lord of the Rings"
| HIGHLIGHT "ring" ON title WITH { "analyzer": "english" }
| KEEP highlight_title
		
highlight_title:keyword
The Lord of the <em>Rings</em>

Highlight multiple columns at once by listing them in ON:

ROW title = "Return of the King", body = "Tolkien wrote the epic saga."
| HIGHLIGHT "king tolkien" ON title, body
| KEEP highlight_title, highlight_body
		
highlight_title:keyword highlight_body:keyword
Return of the <em>King</em> <em>Tolkien</em> wrote the epic saga.

HIGHLIGHT re-analyzes field values at query time, so it works on columns created earlier in the pipeline:

ROW raw = "2024 Sauron Mordor"
| DISSECT raw "%{yr} %{name} %{place}"
| HIGHLIGHT "sauron" ON name
| KEEP name, highlight_name
		
name:keyword highlight_name:keyword
Sauron <em>Sauron</em>

Use "encoder": "html" to escape HTML tags and special characters in the text while keeping the highlight tags intact:

ROW content = "Use <b>bold</b> tags & special chars with the Ring."
| HIGHLIGHT "ring" ON content WITH { "encoder": "html" }
| KEEP highlight_content
		
highlight_content:keyword
Use <b>bold</b> tags & special chars with the <em>Ring</em>.

Set "number_of_fragments": 0 to return the complete text value with matches highlighted rather than returning individual snippets:

ROW content = "Elasticsearch is fast. Elasticsearch is scalable. Elasticsearch is open."
| HIGHLIGHT "elasticsearch" ON content WITH { "number_of_fragments": 0 }
| KEEP highlight_content
		
highlight_content:keyword
<em>Elasticsearch</em> is fast. <em>Elasticsearch</em> is scalable. <em>Elasticsearch</em> is open.

Use pre_tags and post_tags to specify custom wrapping tags:

ROW content = "The quick brown fox jumps over the lazy dog."
| HIGHLIGHT "fox" ON content WITH { "pre_tags": ["<b>"], "post_tags": ["</b>"] }
| KEEP highlight_content
		
highlight_content:keyword
The quick brown <b>fox</b> jumps over the lazy dog.

Use prefix to change the column name prefix:

ROW content = "The One Ring was forged by Sauron."
| HIGHLIGHT prefix = "hl_" "ring" ON content
| KEEP content, hl_content
		
content:keyword hl_content:keyword
The One Ring was forged by Sauron. The One <em>Ring</em> was forged by Sauron.

Set an empty prefix (prefix = "") to replace the source column with the highlighted output:

ROW content = "The quick brown fox jumps over the lazy dog."
| HIGHLIGHT prefix = "" "fox" ON content
| KEEP content
		
content:keyword
The quick brown <em>fox</em> jumps over the lazy dog.

By default, non-matching fields evaluate to null. Set no_match_size to return text from the start of the field instead:

ROW content = "Gardens and flowers bloom in spring."
| HIGHLIGHT "elasticsearch" ON content WITH { "no_match_size": 200 }
| KEEP highlight_content
		
highlight_content:keyword
Gardens and flowers bloom in spring.

Use "order": "score" to sort snippets by relevance score rather than document order:

ROW content = ["fast search", "fast and fast results"]
| HIGHLIGHT "fast" ON content WITH { "order": "score" }
| KEEP highlight_content
		
highlight_content:keyword
[<em>fast</em> and <em>fast</em> results, <em>fast</em> search]