Highlighting in Elasticsearch: Making Search Results Stand Out
AI generated
_doc
_index
Elasticsearch · Highlighting · Search UX
Highlighting: Making Search Results Stand Out
Using the Unified, Plain and FVH highlighters correctly

A result list without highlighting forces users to scan every hit themselves for the term they searched for. Elasticsearch highlighting automatically marks relevant text passages in the search result, but the choice between Unified, Plain and FVH highlighter, along with fine-tuning fragment size, decides whether the highlighting is actually helpful or just distracting.

15 min read highlight · Unified · Plain · FVH · Fragment Size Elasticsearch 8.x · OpenSearch 2.x

1. Why highlighting decisively improves search UX

A result list that only shows title and description without any visual emphasis leaves users unclear why a document appeared as a hit in the first place. Highlighting solves this problem by marking the specific text passages that led to the match, usually bolded or colored through HTML tags like <em>. This lets users grasp within a split second whether a hit is actually relevant, without reading the entire text.

The effect of good highlighting goes beyond mere convenience: studies on search result perception show that highlighted search terms significantly increase click-through rates on relevant hits while simultaneously reducing bounce rates on irrelevant hits, because users recognize them as unfitting faster. Elasticsearch offers several highlighter implementations with different strengths, and the right choice depends heavily on the mapping of the affected fields.

It is important to understand from the start: highlighting is not a free add-on to search, it is its own processing step that costs compute time. Anyone who understands how the individual highlighters work and how fragment parameters affect performance and presentation can use highlighting deliberately, without unnecessarily burdening search speed.

2. The highlight parameter: basic structure

Highlighting is activated via the highlight parameter directly in the _search request, on the same level as query. For every field that should receive highlighting, a separate entry is defined under fields. The response contains an additional highlight object for every hit, with an array of text fragments per field, in which the matched terms are already wrapped with the configured tags.

By default, Elasticsearch uses the tags <em> and </em>, which can be freely customized via the pre_tags and post_tags parameters. It is important that highlighting only works on fields mapped as text, not on keyword fields without analysis, since word boundary detection is based on the analyzer's tokenization.


GET /products/_search
{
  "query": {
    "match": { "description": "waterproof hiking boots" }
  },
  "highlight": {
    "fields": {
      "description": {}
    }
  }
}

# Response (excerpt):
# "hits": [
#   {
#     "_source": { "description": "These waterproof hiking boots are suitable for..." },
#     "highlight": {
#       "description": [
#         "These <em>waterproof</em> <em>hiking</em> <em>boots</em> are suitable for..."
#       ]
#     }
#   }
# ]

A common beginner mistake: highlighting configuration is applied to a field defined as keyword in the mapping. In this case, the highlight object in the response either stays empty or marks the entire field value, because no meaningful tokenization took place. Only analyzed text fields or their multi-field variants are suitable for highlighting.

3. The three highlighters compared: Unified, Plain, FVH

Elasticsearch offers three highlighter implementations, selected via the type parameter. The Unified Highlighter has been the default since Elasticsearch 6 and works directly with Lucene offsets, making it efficient and suitable for most use cases. It supports both simple term queries and complex bool and phrase queries without additional configuration.

The Plain Highlighter is the oldest implementation and re-analyzes text at query time, which makes it noticeably slower than the alternatives for large documents. Its advantage lies in the granular control over highlighting behavior for complex query structures, which is why it is occasionally still used for edge cases with very specific highlighting requirements. The FVH (Fast Vector Highlighter) requires term_vector: with_positions_offsets to be enabled in the mapping, but in return is particularly performant on large text fields and supports multi-term highlighting with individual weighting of the hit order via boundary_scanner.


PUT /articles
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "term_vector": "with_positions_offsets"
      }
    }
  }
}

GET /articles/_search
{
  "query": { "match": { "content": "elasticsearch performance" } },
  "highlight": {
    "type": "fvh",
    "fields": {
      "content": {
        "number_of_fragments": 3,
        "fragment_size": 150
      }
    }
  }
}

The rule of thumb in practice: for most applications, the Unified Highlighter is the right choice, because it works without mapping changes and offers a good balance of performance and feature set. FVH pays off for very large text fields with high search frequency, where the extra storage cost for term vectors is justified by better highlighting performance.

4. Fine-tuning fragment size and number of fragments

For long text fields, the entire text is not returned, only short excerpts, so-called fragments, that show the hit in context. The fragment_size parameter controls the maximum character length of a fragment, 100 characters by default, while number_of_fragments defines how many such fragments are returned at most, five by default. Together, these two parameters largely determine how much context the user sees around a hit.

A fragment_size that is too small cuts sentences off mid-word or delivers fragments without understandable context, while a fragment_size that is too large unnecessarily bloats the response size and lets the actual hit position get lost in body text. For product descriptions with short, concise sentences, 100 to 150 characters is usually enough, while editorial articles with complex sentence structures benefit from 200 to 250 characters, to represent complete sentences.


GET /articles/_search
{
  "query": { "match": { "content": "cluster resharding" } },
  "highlight": {
    "fields": {
      "content": {
        "fragment_size": 180,
        "number_of_fragments": 2,
        "no_match_size": 150,
        "order": "score"
      }
    }
  }
}

The order: score parameter sorts the returned fragments by relevance instead of by their position in the document, which is especially useful for long texts with multiple hit clusters. The no_match_size parameter returns a default excerpt from the start of the document if no highlighting hit was found in the field, preventing a hit from appearing in the display with no preview text at all.

5. Highlighting on multi-fields and nested fields

Many mappings define multi-fields, for instance a field title of type text with a sub-field title.raw of type keyword for exact sorting and filtering. Highlighting only works on the analyzed text part, never on the keyword sub-field. If title.raw is accidentally specified instead of title in the highlight parameter, the result stays empty even though the search itself was successful.

Nested objects (nested type) require extra care: highlighting on fields inside a nested object requires either a nested query with a correctly referenced path or the explicit specification of the options.inner_hits parameter, so Elasticsearch knows in which nested document to look for the hit. Without this mapping, highlighting on nested fields simply cannot produce results, even if the query matches correctly.


PUT /catalog
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": { "raw": { "type": "keyword" } }
      },
      "reviews": {
        "type": "nested",
        "properties": {
          "comment": { "type": "text" }
        }
      }
    }
  }
}

GET /catalog/_search
{
  "query": {
    "nested": {
      "path": "reviews",
      "query": { "match": { "reviews.comment": "top quality" } },
      "inner_hits": {
        "highlight": {
          "fields": { "reviews.comment": {} }
        }
      }
    }
  }
}

6. Pre/post tags and custom highlighting markup

For custom styling in the frontend, the default tags <em> and </em> are often replaced with custom markup elements, for instance <mark class="hit">, to apply a specific color scheme via CSS classes. The pre_tags and post_tags parameters accept arrays, which allows staggered color coding when several different search terms are involved: the first term gets the first tag pair, the second term the second, and so on.

A particularly elegant option is "tags_schema": "styled", a predefined set of ten different CSS classes (hlt1 through hlt10) that automatically assigns different classes to different search terms without needing to manually define the tag arrays. This option works well when multiple search terms need to be highlighted at once and need to be visually distinguishable.


GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "waterproof breathable",
      "fields": ["description"]
    }
  },
  "highlight": {
    "pre_tags": ["<mark class=\"hit-a\">", "<mark class=\"hit-b\">"],
    "post_tags": ["</mark>", "</mark>"],
    "fields": {
      "description": {}
    }
  }
}

7. Highlighting on phrase and fuzzy queries

For a match_phrase query, highlighting by default only marks the exact word sequence that matches the phrase, not individual words outside the phrase. That is usually the desired behavior for quote or title search, because it clearly signals to the user which exact wording was matched. For a fuzzy query, on the other hand, which also matches slightly misspelled variants, highlighting can appear inconsistent, because the differently spelled term in the document does not exactly match the search term.

For consistent highlighting on fuzzy queries, the Unified Highlighter with the enabled option "highlight_query" is recommended, where a separate, usually more precise query is defined just for highlighting, independent of the actual search query. This allows a broad, error-tolerant query to be used for finding hits, while highlighting is based on a narrower query, appearing more precise as a result.


GET /products/_search
{
  "query": {
    "fuzzy": { "title": { "value": "hikng boots", "fuzziness": "AUTO" } }
  },
  "highlight": {
    "fields": {
      "title": {
        "highlight_query": {
          "match": { "title": "hiking boots" }
        }
      }
    }
  }
}

8. Performance cost of highlighting on large documents

Highlighting is not a trivial feature without cost. The Plain Highlighter re-analyzes the affected text on every request, which can lead to noticeable latency increases on large fields with several kilobytes of text per document, especially when many hits need highlighting at once. The Unified Highlighter is more efficient here, because it uses Lucene-internal offsets, but for very large fields it also causes measurable overhead compared to a search without highlighting.

FVH offers the best performance on large fields, but pays for it with extra storage requirements in the index, because term vectors with position and offset information must be stored for every document. This index size can grow noticeably for extensive text fields. A proven practice is to only enable highlighting on the fields actually shown in the UI, instead of enabling it broadly across all searchable fields, and to keep the size limit of search results consistently low, since highlighting is only calculated for the returned hits.

Highlighter Mapping requirement Performance Typical use
Unified None, works out of the box Good, default choice Most use cases
Plain None Slow on large fields Very specific highlighting logic
FVH term_vector: with_positions_offsets Very good on large fields Large text fields, high search frequency

If unsure, start with the Unified Highlighter and only switch to FVH once performance problems are demonstrable, since the additional mapping effort and storage requirement for FVH are only justified when actually needed.

9. Rendering highlighting correctly in the frontend (XSS safety)

Highlighting returns HTML fragments with embedded tags, which in the frontend are usually rendered through dangerouslySetInnerHTML in React, v-html in Vue, or x-html in Alpine.js. This is exactly where a serious security issue lurks: the original document text embedded in the highlighting fragment comes from user input or content management systems and may itself contain HTML or script content that leads to cross-site scripting when rendered directly.

The safe approach is two-staged: first, the entire fragment text, including the highlight tags inserted by Elasticsearch, is passed through a dedicated HTML escaping function that escapes every character except the known, deliberately inserted highlight tags. Afterward, only the known highlight tags, for instance <mark>, are deliberately turned back into real HTML, either through a regular expression after escaping or through a dedicated sanitizing library with an explicit allowlist of permitted tags.


// Example sanitizing strategy in the frontend (pseudocode)
// 1. Fully HTML-escape the raw fragment text
// 2. Deliberately re-allow only the known highlight tags

function renderHighlight(fragment) {
  const escaped = escapeHtml(fragment); // escapes ALL tags, including <mark>
  // Controlled conversion back of ONLY the expected highlight tags:
  return escaped
    .replace(/<mark>/g, "<mark>")
    .replace(/<\/mark>/g, "</mark>");
}

// Alternative: dedicated sanitizing library with allowlist ["mark"]

An allowlist-based sanitizing library is fundamentally more robust than a manual regex solution, because it correctly handles edge cases like nested or malformed tags as well. The most important principle remains: never render highlighting fragments as HTML unchecked, no matter how trustworthy the data source appears, since Elasticsearch itself performs no HTML escaping of the original document content.

Mironsoft

Elasticsearch search, highlighting and frontend integration

Search results nobody can properly make sense of?

We configure highlighting for your Elasticsearch search, choose the right highlighter for your data volume, and integrate secure rendering in the frontend.

Highlighter Choice

Choose Unified, Plain or FVH to match your data volume and query load

Fragment Tuning

Tune fragment size and count to your content type

Secure Frontend

Implement XSS-safe rendering of highlighting fragments

10. Summary

Highlighting turns a plain result list into a traceable, scannable search result display by directly marking relevant text passages. The highlight parameter only works on analyzed text fields, not on keyword fields. The choice between Unified, Plain and FVH highlighter depends on data volume and search frequency, with the Unified Highlighter being the right default choice for most cases. Fragment size and number of fragments determine how much context a user sees around a hit, and should be adapted to the respective content type.

Multi-fields and nested objects require extra care, since highlighting only works on the field that is actually analyzed. Pre and post tags allow custom styling, while highlight_query enables precise highlighting even for error-tolerant fuzzy queries. In the frontend, consistent HTML escaping with a deliberate allowlist of the known highlight tags is essential to avoid XSS vulnerabilities.

Highlighting in Elasticsearch, the essentials at a glance

Highlighter choice

Unified Highlighter as default, FVH for large fields with high search frequency.

Fragment tuning

Adapt fragment_size and number_of_fragments to content type, order:score for relevance.

Field choice

Only possible on analyzed text fields, never on keyword sub-fields.

Frontend security

Escape fragments, deliberately re-allow only known highlight tags, consistently avoid XSS.

11. FAQ: Highlighting in Elasticsearch

1Which highlighter as default?
Unified Highlighter, works without mapping changes with a good balance of performance and features.
2Highlighting on a keyword field?
Does not work, keyword fields are not tokenized, no meaningful word boundaries exist.
3When FVH instead of Unified?
For large fields with high search frequency, when the additional storage requirement pays off.
4fragment_size and number_of_fragments?
Control the maximum length and number of returned text excerpts per field.
5Highlighting on nested objects?
Requires a nested query with inner_hits and its own highlight configuration.
6Color-separate different terms?
Via tag arrays for pre_tags/post_tags or tags_schema: styled with ten classes.
7Highlighting on fuzzy queries?
highlight_query allows defining a separate, more precise query just for highlighting.
8Cost on large documents?
Plain is slowest, Unified more efficient, FVH fastest with higher index storage.
9Automatically XSS-safe?
No, the frontend must escape fragments and deliberately re-allow only known highlight tags.
10What does no_match_size do?
Returns default preview text if no highlighting hit was found in the field.