Skip to content
ExponaBLOG
ELASTICSEARCH & SEARCH ENGINEERING · DAY 021 / 100

Highlighting search results for Elasticsearch developers

Show users why a result matched.

Expona Admin7 min read

Highlighting search results for Elasticsearch developers

Users click search results because they want to understand why a document matched their query. Without visual cues, they're left scanning paragraphs trying to find the word "kubernetes" they just searched for. Highlighting search results for Elasticsearch developers means embedding context directly into the response, showing exactly which fields and fragments matched the query terms.

We've all experienced search UIs where results appear relevant until you read them. The document title says "Deploy" but the query was for "monitoring," and you wonder why this ranked high. Highlighting bridges that gap. In e-commerce, a product search for "wireless" highlights the term in the description. In SaaS dashboards, it highlights matched log lines. In travel platforms, it highlights the city name in the hotel description.

Elasticsearch's highlight feature doesn't just improve UX—it reduces support tickets, increases dwell time, and builds user trust. When a user sees their search term highlighted, they immediately understand the relevance.

The highlight block in queries

The highlight block sits at the root level of your search query, separate from query and aggs. It tells Elasticsearch which fields to highlight and how to format the output.

Here's a baseline example:

json
{
  "query": {
    "multi_match": {
      "query": "kubernetes monitoring",
      "fields": ["title", "description", "tags"]
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "description": {},
      "tags": {}
    }
  }
}

Elasticsearch wraps matched terms with <em> tags by default, so you'll get responses like:

json
{
  "hits": {
    "hits": [
      {
        "_id": "1",
        "_source": {
          "title": "Kubernetes Best Practices",
          "description": "Learn to monitor Kubernetes clusters..."
        },
        "highlight": {
          "title": ["<em>Kubernetes</em> Best Practices"],
          "description": ["Learn to monitor <em>Kubernetes</em> clusters..."]
        }
      }
    ]
  }
}

The highlight block applies to all fields listed. If a field has no matches, Elasticsearch omits it from the highlight response by default. This keeps responses compact.

Custom pre and post tags for styling

Default <em> tags work for plain HTML, but modern UIs often need custom markup. A React component might expect <mark> tags. A mobile app might expect custom delimiters. That's where pre_tags and post_tags come in.

json
{
  "highlight": {
    "pre_tags": ["<mark>"],
    "post_tags": ["</mark>"],
    "fields": {
      "title": {},
      "description": {}
    }
  }
}

If you need to avoid HTML altogether—say, for a plaintext API or log output—use custom delimiters:

json
{
  "highlight": {
    "pre_tags": ["***"],
    "post_tags": ["***"],
    "fields": {
      "title": {},
      "content": {}
    }
  }
}

Now matches appear as Learn to ***monitor*** Kubernetes. This pattern is common when building search APIs that feed multiple downstream clients with different rendering needs.

A real-world scenario: we once built a trading platform's trade-log search. The frontend needed <strong> tags for desktop, but the mobile app needed plaintext with colons. Using custom pre and post tags, we served both from one Elasticsearch response.

Fragment size and fragment count

By default, Elasticsearch returns the entire field. For a 5,000-character product description, that's wasteful. The fragment_size parameter controls how many characters appear around each match, and number_of_fragments controls how many matches you get back.

json
{
  "highlight": {
    "fields": {
      "description": {
        "fragment_size": 150,
        "number_of_fragments": 3
      }
    }
  }
}

This returns up to 3 fragments, each around 150 characters, centered on the matched term. The response looks like:

json
{
  "highlight": {
    "description": [
      "...real-time alerting and <mark>monitoring</mark> capabilities. Integrates with Prometheus...",
      "...reduce MTTR through intelligent <mark>monitoring</mark> and auto-remediation rules...",
      "...The platform supports distributed <mark>monitoring</mark> across multiple cloud regions..."
    ]
  }
}

Fragment size is approximate—Elasticsearch tries to break on word boundaries. A fragment_size of 150 might return 140–160 characters. Plan accordingly in your frontend.

Setting number_of_fragments to 0 returns the entire field, which is useful for short fields like titles where truncation makes no sense.

In a CRM system, we used fragments heavily. Search results showed up to 5 snippets from customer notes, each 100 characters, so users could scan context without loading the full record. This cut page-load times by 30% versus returning full fields.

Escape HTML in highlights

Here's a trap many teams hit: if your source data contains HTML or special characters, Elasticsearch's highlight wraps them without escaping. If a user searches for "<script>" or your product name is "C++" and you're returning HTML, you need to be careful.

Consider this source document:

json
{
  "title": "C++ Guide",
  "description": "Learn C++ & memory management"
}

A query for "C++" with default highlighting returns:

json
{
  "highlight": {
    "title": ["<em>C++</em> Guide"],
    "description": ["Learn <em>C++</em> & memory management"]
  }
}

The ampersand is unescaped. If you render this directly as HTML, you're safe (the ampersand renders fine in HTML). But if your frontend does any HTML-to-text conversion, or if you're building a system where double-escaping matters, you need awareness here.

More critically: if your source data contains user input like reviews or comments, and you're returning highlights, always escape the highlight output in your frontend before rendering. Elasticsearch doesn't HTML-escape the surrounding text, only the tags it inserts.

Best practice is to treat the highlight output as untrusted in your application layer. Use your framework's escaping functions—React's built-in escaping, Django's escape(), .NET's SecurityUtility, etc.

In a SaaS platform with user-generated search data, we always run highlights through a sanitization library on the backend before returning to the frontend, even though Elasticsearch itself wasn't inserting malicious content. This defended against edge cases where specially-crafted queries might be stored and replayed.

Putting it together: practical example

Here's a realistic query from an e-commerce search API:

json
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "waterproof hiking boots",
            "fields": ["title^3", "description", "category", "tags"]
          }
        }
      ],
      "filter": [
        {
          "range": {
            "price": {
              "lte": 200
            }
          }
        }
      ]
    }
  },
  "highlight": {
    "pre_tags": ["<mark>"],
    "post_tags": ["</mark>"],
    "fields": {
      "title": {
        "number_of_fragments": 0
      },
      "description": {
        "fragment_size": 120,
        "number_of_fragments": 2
      },
      "tags": {
        "fragment_size": 50,
        "number_of_fragments": 1
      }
    }
  },
  "size": 20
}

This returns:

  • Full title with highlights (titles are short, so we show everything)
  • Up to 2 snippets from description, 120 chars each
  • Up to 1 snippet from tags, 50 chars
  • Custom <mark> tags for CSS styling

Common mistakes

Forgetting to handle missing highlights: Not every result will have highlights for every field. If a result matches only on price range (not text), you won't get a highlight. Always check if the highlight key exists before rendering.

Over-fragmenting: Requesting 10 fragments per field multiplies response size and can confuse users. Three to five fragments per field is usually optimal.

Ignoring performance: Highlighting is CPU-intensive. Millions of documents with large fields slow down both indexing and search. Profile your queries with and without highlights. Sometimes highlighting only the top 10 results is a better trade-off than highlighting all 100.

Unsafe HTML rendering: Passing highlight output directly into React JSX without proper escaping or using dangerouslySetInnerHTML is a common XSS vector, even though Elasticsearch itself is safe.

Not testing with special characters: Characters like <, >, &, quotes, and Unicode need testing in both your Elasticsearch config and your frontend.

Key takeaways

  • The highlight block at query root level tells Elasticsearch which fields to highlight and how to format them
  • Use pre_tags and post_tags to customize markup for your frontend's styling or API contract needs
  • Control response size and performance with fragment_size and number_of_fragments; aim for 2–5 fragments per field
  • Treat highlight output as untrusted in your application layer and escape or sanitize before rendering to users
  • Test highlighting with special characters, user input, and HTML entities to avoid surprises in production
  • Profile query performance with highlights enabled—they add CPU cost, especially on large result sets
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles