Highlighting #
Ask for matched fragments of your text fields back alongside each hit, with the matched terms wrapped in tags — ready to render as a search result list.
POST /my-collection/_search
How it works #
Highlighting is an engine-native enrich step that runs on the returned page
only — after ranking and truncation, exactly like
script
fields. It has two phases (lib/engine/src/search/highlight/):
1. Term extraction. The engine walks the query tree and collects
(field, term) pairs worth highlighting: term, terms, fuzzy,
span_term, intervals, match, multi_match, match_phrase, and
bool clauses. Text queries (match, multi_match, phrase) are
re-tokenized through the field’s search analyzer (or the query’s explicit
analyzer override), so the highlighted terms mirror what the query
scored. must_not clauses never contribute — documents excluded by them
get no tags from them. match_all, range, geo and
vector queries carry nothing to highlight.
2. Substring marking. For each requested field the stored text of the
hit is scanned case-insensitively (ASCII) with word-boundary checks on
both edges — cat matches “cat” but not “category”. When several terms
match at the same spot the longest wins, and matches never overlap.
Fragments are byte windows of about fragment_size centered on a match,
snapped to UTF-8 character boundaries (multibyte text is never split
mid-character, though a fragment may end mid-word); the walk pivots on
the first match, then on the next match beyond the previous window, so
fragments cover matches in document order without overlapping repeats.
Multi-value fields fan out their fragments across the array values.
The result lands on each hit under highlight (serialized as
_highlight), keyed by field name. Fields with no matches emit nothing
unless no_match_size is set — and no_match_size applies to any
highlighted field without matches, including fields the query never
touched. The leading snippet it returns is trimmed back to the last
whitespace, so no partial word is shown.
Verified on logs: highlighting message for the query
cluster node with fragment_size: 60 returns
"<em>cluster</em> <em>node</em> left the region unex" — two tagged
terms, a 60-byte window, and a mid-word cut at the boundary.
Examples #
{
"query": { "match": { "field": "message", "query": "search" } },
"highlight": {
"fields": {
"message": {
"fragment_size": 100,
"number_of_fragments": 3
}
}
},
"size": 5
}
Each returned hit carries a highlight object whose keys are the requested
fields; values are fragment strings with matched terms wrapped in the tags
(default <em> … </em>). Custom tags — same idea with <b> wrapping:
Use cases #
- Result-list snippets. Show why each hit matched without shipping the whole document — the bread and butter of every search UI.
- Tag-rendered summaries.
pre_tags/post_tagsemit whatever your frontend consumes (<mark>,**for Markdown,[h]…[/h]), so no post-processing is needed on the client. - Whole-value flagging.
number_of_fragments: 0tags every match in the full field value — useful for short fields (titles, names, single log lines) where cutting context away hurts. - Fallback lead-ins.
no_match_sizekeeps a uniform result-card height: matched hits show a snippet, unmatched ones show the start of the field instead of a hole. - Bandwidth trimming. Combine with
fields/_sourcefiltering to return highlights without the full source when lines are long (thelogsmessages are short; production log lines usually are not).
Parameters for highlight
#
Top level:
fields
(Required, object) Map of field name (or"*"for every field in the schema that the query collected terms for) to per-field options below.pre_tags
(Optional, array of strings, default:["<em>"]) Tag(s) emitted before a matched term. The first tag is used.post_tags
(Optional, array of strings, default:["</em>"]) Tag(s) emitted after a matched term. The first tag is used.
Per field:
fragment_size
(Optional, integer, default:100) Target length of each fragment in characters.0returns the whole field value instead of fragments.number_of_fragments
(Optional, integer, default:5) Maximum fragments returned per field.0returns the whole field.pre_tags/post_tags
(Optional, array of strings) Per-field tag override; falls back to the request-level tags.no_match_size
(Optional, integer) When the document has the field but nothing matched, return this many leading characters of the value (whitespace-trimmed) instead of nothing.
Notes #
- Matching runs on the stored text of the hit, so it works for stored-only fields too — no positions index is consulted.
fuzzyhighlights the query term as typed, not the variants the query expanded to.- Fragments follow match order (first match first), not score; tag every
occurrence by choosing
number_of_fragments: 0. - The conformance suite pins this behavior (
search.highlightfamily).
Related #
- Analysis — the analyzers that decide which
terms a
matchproduces, and therefore which terms get tagged. - Script fields — the sibling fetch-time enrich: computed values instead of tagged fragments, same page-sized cost.
- Sort and pagination — highlight runs on the final page, so sorting changes which hits get tagged, not how.
- Hybrid search — vector-only legs carry no highlightable
terms; a
bm25leg does.