Highlighting

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_tags emit whatever your frontend consumes (<mark>, ** for Markdown, [h]…[/h]), so no post-processing is needed on the client.
  • Whole-value flagging. number_of_fragments: 0 tags 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_size keeps 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/_source filtering to return highlights without the full source when lines are long (the logs messages 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. 0 returns the whole field value instead of fragments.
  • number_of_fragments
    (Optional, integer, default: 5) Maximum fragments returned per field. 0 returns 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.
  • fuzzy highlights 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.highlight family).
  • Analysis — the analyzers that decide which terms a match produces, 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 bm25 leg does.
Calendar October 6, 2026
Edit Edit this page