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. At most 256 characters.
post_tags
(Optional) Closing tag inserted after each highlighted term. Accepts a string or a single-element array of strings. Defaults to </em>. At most 256 characters.
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 to use across all highlighted fields, overriding each field's own analyzer. Also analyzes query terms, unless a full-text search function specifies its own analyzer. When omitted, HIGHLIGHT uses each field's analyzer (see Choose an analyzer). Only built-in and node-level plugin analyzers are supported.
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. Index text fields use their mapped analyzer, while computed and other columns default to standard unless overridden. Refer to Choose an analyzer.

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.

HIGHLIGHT analyzes each field with its own analyzer, using the first rule that applies:

  • The analyzer option in the WITH clause (overrides all ON fields).
  • For an index text field, its mapped analyzer or the index default analyzer.
  • For a column created with TO_TEXT, the analyzer set in its analyzer option.
  • The standard analyzer for all other columns, including keyword fields.

An index text field keeps its mapped analyzer when you rename it with RENAME, copy it with EVAL (such as EVAL t = title), group by it with STATS or INLINE STATS (such as BY t = title), or pass it unchanged through FORK, FUSE, or subqueries in FROM. Columns computed from expressions do not inherit a mapped analyzer. They use the analyzer declared with TO_TEXT, or default to standard. To analyze a computed column like the original field, set the analyzer option in WITH.

If the queried indices map a field with different analyzers, each row uses the analyzer of the index it comes from.

HIGHLIGHT returns an error when:

  • Queried indices map the field with different analyzers and HIGHLIGHT cannot determine which index supplied the value (for example, after STATS, DEDUP, or a FUSE whose KEY BY includes neither _index nor a copy of it, or across a LOOKUP JOIN).
  • Branches of FORK, or subqueries in FROM, define conflicting analyzers for the same column (for example, when one branch reads the field from an index and another computes the column with a different analyzer).

To resolve this, set the analyzer option in the WITH clause. For a computed column, you can instead set the analyzer option of TO_TEXT to match the analyzer of the index field.

Analyzers defined in index settings, and analyzers that are not reported, fall back to standard with a warning, as described later in this section. If another queried index uses an analyzer that does not fall back, the field still counts as having different analyzers.

Query terms use the target field's analyzer. An analyzer specified on a full-text search function, such as MATCH(title, "rings", {"analyzer": "whitespace"}), applies only to the query text. If the query and field analyzers produce different tokens, some matches might not be highlighted.

HIGHLIGHT falls back to standard and returns a warning when:

  • The analyzer is defined in index settings (such as a custom analyzer or index-level default) rather than globally on the node.
  • The analyzer is not registered on the coordinating node (for example, because a required plugin is missing).
  • The field type does not report an analyzer, such as semantic_text or pattern_text.

When falling back to standard, highlights might not match the terms that matched your search. To avoid the warning, set the analyzer option in the WITH clause.

Tip

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

  • HIGHLIGHT supports only built-in and node-level plugin analyzers. Specifying an analyzer defined in index settings in WITH or in a full-text search function returns an error. If an implicitly reused WHERE query references an unsupported analyzer, write the query explicitly in HIGHLIGHT without that analyzer option.
  • HIGHLIGHT ignores search_analyzer and search_quote_analyzer settings. Because query terms are analyzed with the field's index analyzer, fields configured with separate search analyzers might produce unexpected highlights. To analyze query terms with a different analyzer, specify the analyzer option on the full-text search function.
  • On keyword fields, HIGHLIGHT tokenizes text using the standard analyzer and breaks it into snippets like a text field, rather than treating the value as a single atomic term.
  • On semantic_text fields, HIGHLIGHT only performs lexical matching against the underlying text. Pure semantic vector matches that lack literal term 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>

HIGHLIGHT automatically uses each field's mapped analyzer. In this example, the title field in the books_english index uses the english analyzer, which stems Rings to ring. Even without extra options, HIGHLIGHT highlights Rings for the query term ring:

FROM books_english
| WHERE MATCH(title, "ring") AND book_no == "2714"
| 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 Return of the King Being the Third Part of The Lord of the <em>Rings</em>

Use the analyzer option to override the analyzer across all ON fields. This is especially useful for computed columns, which default to the standard analyzer. In this example, specifying the english analyzer stems Rings to match the query term 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]