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, ...] | ON *] [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

(Optional) 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.

If you don't specify a query, HIGHLIGHT automatically reuses full-text search conditions from earlier WHERE commands in the query. Refer to Reuse a query from WHERE.

When you provide both a query and an ON clause, any field named in your query must also be listed in ON. For example, HIGHLIGHT MATCH(title, "fox") ON body is rejected because title is not in ON. When you let ES|QL determine the query or fields automatically, it handles this check for you.

Unqualified string literals and QSTR expressions are evaluated against whichever fields are being highlighted. Queries without positive search conditions (such as NOT MATCH(...)) have no terms to highlight and return null, unless you configure no_match_size.

field

(Optional) One or more comma-separated columns to highlight, or * to highlight every text and keyword column in the table. Fields must be text or keyword types (semantic_text fields are supported and treated as text). You can only use * by itself; wildcard patterns like title* and combining * with specific field names (such as ON *, title) are not supported.

If you omit ON, HIGHLIGHT determines which columns to highlight based on your query:

  • For queries targeting a specific column (such as MATCH or MATCH_PHRASE), only that column is highlighted.
  • For queries that don't target a single column (such as string literals, QSTR, or KQL), HIGHLIGHT checks all text and keyword columns in the table.

Refer to Choose fields with ON. 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 text 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.

Most search queries filter rows with a full-text condition in WHERE, then highlight matching terms in those same fields. To avoid repeating your search query, you can omit the query from HIGHLIGHT. When you do, HIGHLIGHT automatically finds and reuses full-text search conditions from earlier WHERE commands.

This works with any positive full-text search function, including MATCH, MATCH_PHRASE, QSTR, KQL, and the match operator :.

You can include intermediate commands between WHERE and HIGHLIGHT as long as each row still represents an individual document. For example, commands like KEEP, DROP, RENAME, EVAL, GROK, DISSECT, LIMIT, SORT, MV_EXPAND, and INLINE STATS pass through without issue.

However, commands that summarize, aggregate, or join rows—such as STATS, LOOKUP JOIN, or FORK—change the document context. If you use any of these commands between WHERE and HIGHLIGHT, you must provide the query explicitly in HIGHLIGHT.

If your query contains multiple WHERE clauses, HIGHLIGHT combines all of their full-text search conditions so that every searched field can produce snippets, even though the WHERE clauses filter your rows together using AND.

The following search conditions cannot be automatically reused:

  • Negated conditions, such as NOT MATCH(...) (there are no positive matches to highlight)
  • Conditions combined with non-text filters using OR, such as MATCH(title, "fox") OR year > 2020

If your query relies solely on conditions that cannot be reused, specify the query explicitly in HIGHLIGHT.

If you provide an explicit query in HIGHLIGHT, it takes precedence, and any conditions from earlier WHERE commands are ignored for highlighting.

The ON clause specifies which columns to highlight. You can choose specific columns, highlight all available text columns, or let ES|QL determine the columns automatically:

  • Highlight specific fields: Use ON field1, field2 to highlight only the specified columns.
  • Highlight all text and keyword fields: Use ON * to highlight every text and keyword column in the current table, including multi-fields (such as author.keyword) and semantic_text fields (highlighted lexically). Metadata columns such as _id and _index are not included.
  • Let ES|QL determine fields: If you omit ON, HIGHLIGHT chooses the columns based on your query:
    • If the query targets a specific field (such as MATCH(title, "fox")), only that field is highlighted.
    • If the query does not name a specific field (such as a string literal, QSTR, or KQL), HIGHLIGHT checks all text and keyword columns in the table.

If a highlighted field does not match any query terms, its output is null (or the leading text specified by no_match_size). If ES|QL cannot find any eligible text or keyword columns to highlight, you must provide an explicit ON clause.

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.
  • HIGHLIGHT cannot automatically reuse a WHERE query across commands that aggregate, summarize, or join rows, such as STATS, LOOKUP JOIN, or FORK. In those queries, specify the query directly on HIGHLIGHT.
  • If you drop a field targeted by the reused WHERE query before HIGHLIGHT, the implicit query can no longer highlight that field. If no other reusable fields remain, provide an explicit query and ON clause using columns that are still in scope.

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 matching terms in the output. You can specify the search condition again in HIGHLIGHT:

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

To avoid repeating your search query, omit the query from HIGHLIGHT. When you also omit ON, HIGHLIGHT automatically highlights matches in the field searched by WHERE (in this case, creating highlight_title):

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

To reuse the WHERE condition but choose which columns to highlight, provide an explicit ON clause:

FROM books
| WHERE MATCH(title, "Return")
| HIGHLIGHT 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

When your query targets a specific field (such as MATCH), you can omit ON. Only that field is highlighted, leaving other columns untouched:

ROW title = "Return of the King", body = "An ordinary description."
| HIGHLIGHT MATCH(title, "king")
		
title:keyword body:keyword highlight_title:keyword
Return of the King An ordinary description. Return of the <em>King</em>

When you use a query that doesn't target a specific field, such as a string literal or QSTR, omitting ON highlights all text and keyword columns:

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

Use ON * to highlight every text and keyword column in the table at once. Columns that do not match the query evaluate to null:

ROW title = "Return of the King", body = "An ordinary description."
| HIGHLIGHT MATCH(title, "king") ON *
| KEEP highlight_title, highlight_body
		
highlight_title:keyword highlight_body:keyword
Return of the <em>King</em> null

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]