Search extras: highlighting, suggesters, collapse, percolation & more like this
|
This section documents the current Elasticsearch 9.x line (with 8.19 as the final 8.x release) as published at the Elasticsearch documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Kibana-only UIs, the ML/NLP model-management workflow, cross-cluster replication, and parts of the paid / serverless-only surface) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Elasticsearch iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Once a query returns the right hits in the right order, a second set of features shapes what the user actually sees: snippets with the query terms marked, autocomplete and spelling suggestions, one row per group instead of many near-duplicates, and parameterised or stored query bodies. A final feature turns search around entirely — storing queries and matching an incoming document against all of them.
Highlighting
A highlight section next to query asks Elasticsearch to return, per hit, short fragments of the
requested fields with the matched terms wrapped in tags. It re-runs the query against the field’s
content, so highlights stay consistent with what matched. See
Highlighting.
GET /articles/_search
{
"query": { "match": { "body": "distributed search" } },
"highlight": {
"fields": {
"body": {
"fragment_size": 150,
"number_of_fragments": 3,
"pre_tags": ["<mark>"],
"post_tags": ["</mark>"]
}
}
}
}
Each hit gains a highlight object: "body": ["…a <mark>distributed</mark> <mark>search</mark>
engine…"]. fragment_size is the target fragment length in characters; number_of_fragments
caps how many are returned (set it to 0 to highlight and return the whole field, unfragmented).
pre_tags / post_tags default to <em> / </em>.
Choosing a highlighter
Three implementations exist; pick per field with "type".
| Type | When to use |
|---|---|
|
The default. Breaks the field into sentences with a |
|
Re-analyses the field in memory and re-runs a Lucene query against it. Accurate for complex queries on small fields; slow on large fields because it has no term positions to work from. |
|
The Fast Vector Highlighter. Needs |
GET /articles/_search
{
"query": { "match_phrase": { "body": "fault tolerant" } },
"highlight": {
"type": "fvh",
"fields": { "body": {} }
}
}
Other useful knobs: require_field_match: false highlights a field even when the match came from a
different field (common with copy_to or multi_match); order: "score" returns the best-scoring
fragments first; highlight_query highlights against a different query than the one that selected
the hit (for example, mark synonyms the main query expanded but the user did not type). All options
are listed on the
highlighting reference.
Suggesters: autocomplete and did-you-mean
A suggest section (with or without a query) asks for alternative text. The three suggesters
solve different problems.
term and phrase suggesters (did-you-mean)
The term suggester returns per-token spelling corrections drawn from a field’s actual terms, ranked by edit distance and frequency. It does not consider whether the corrected tokens make sense together.
POST /articles/_search
{
"suggest": {
"text": "elasticsach clstr",
"spelling": {
"term": { "field": "body" }
}
}
}
The phrase suggester builds on it: it generates whole candidate phrases, scores them with an n-gram language model over the field, and returns the most likely correction as a unit — the right tool for a "Did you mean: elasticsearch cluster?" line.
POST /articles/_search
{
"suggest": {
"text": "elasticsach clstr",
"did_you_mean": {
"phrase": {
"field": "body.trigram",
"size": 1,
"gram_size": 3,
"direct_generator": [
{ "field": "body.trigram", "suggest_mode": "always" }
],
"highlight": { "pre_tag": "<em>", "post_tag": "</em>" }
}
}
}
}
The phrase suggester needs a shingled sub-field (here body.trigram, an analyzer with a shingle
token filter). See
Suggesters
for the term, phrase, and completion parameters.
completion suggester (prefix autocomplete)
The completion suggester is a separate, in-memory FST structure built from a completion field.
It matches by prefix only, returns in a few milliseconds, and is meant to drive a search box’s
dropdown as the user types. Define the field, index inputs (optionally weighted), then query with
_search + suggest.
PUT /places
{
"mappings": {
"properties": {
"name": { "type": "text" },
"suggest": { "type": "completion" }
}
}
}
PUT /places/_doc/1?refresh
{
"name": "Madrid Barajas Airport",
"suggest": { "input": ["Madrid Barajas", "Barajas Airport", "MAD"], "weight": 10 }
}
POST /places/_search
{
"suggest": {
"place-autocomplete": {
"prefix": "bara",
"completion": { "field": "suggest", "size": 5, "skip_duplicates": true }
}
}
}
A completion field can carry contexts — a category or geo filter stored with each entry so
the same index serves, say, only airports near the user:
PUT /places
{
"mappings": {
"properties": {
"suggest": {
"type": "completion",
"contexts": [
{ "name": "kind", "type": "category" },
{ "name": "location", "type": "geo", "precision": 4 }
]
}
}
}
}
Then each suggest request must supply the context values it wants. fuzzy on the completion query
allows a typo or two in the prefix.
search_as_you_type (infix, ranked results)
completion matches prefixes of whole inputs only and returns suggestion strings, not scored
documents. When you want ordinary ranked hits that also match a substring in the middle of a field
as the user types, map the field as
search_as_you_type.
It transparently creates shingle and prefix sub-fields and is queried with a normal multi_match of
type bool_prefix.
PUT /articles
{ "mappings": { "properties": { "title": { "type": "search_as_you_type" } } } }
GET /articles/_search
{
"query": {
"multi_match": {
"query": "distr sea",
"type": "bool_prefix",
"fields": [ "title", "title._2gram", "title._3gram" ]
}
}
}
|
Both |
Shaping the result set
collapse and inner_hits
collapse returns only the top hit for each distinct value of a keyword or numeric field — one
product per model_id, one message per thread_id — without a terms aggregation. inner_hits
then fetches a few of the collapsed-away documents per group. Sorting and search_after still work;
only from/size count the collapsed rows. See
Collapse search results.
GET /listings/_search
{
"query": { "match": { "description": "road bike" } },
"collapse": {
"field": "model_id",
"inner_hits": {
"name": "cheapest_variants",
"size": 3,
"sort": [ { "price": "asc" } ]
}
},
"sort": [ { "_score": "desc" } ]
}
Second-level collapse is allowed inside inner_hits but not at the top level. Unlike an
aggregation, collapse does not give an exact group count; add a cardinality aggregation on the
same field if you need one.
_msearch: many searches, one round trip
_msearch sends several independent searches in a single request using newline-delimited JSON
(NDJSON): a header line (index and options) then a body line, repeated. Each search is executed
independently and the responses array preserves order. See
Multi search API.
GET /_msearch
{ "index": "articles" }
{ "query": { "match": { "body": "kibana" } }, "size": 1 }
{ "index": "listings" }
{ "query": { "match_all": {} }, "size": 0, "aggs": { "avg_price": { "avg": { "field": "price" } } } }
The trailing newline after the last body line is required. _msearch shares one thread-pool slot
and one network round trip, so it is cheaper than firing the same searches concurrently;
max_concurrent_searches caps how many run in parallel on the cluster.
Search templates
A search template is a search body written as a Mustache document with
\{{placeholders}}, rendered with per-request parameters. It keeps the query shape on the server:
the application sends only values. Register it under _scripts, then call _search/template. See
Search templates.
PUT /_scripts/articles-by-tag
{
"script": {
"lang": "mustache",
"source": {
"query": {
"bool": {
"must": [ { "match": { "body": "{{q}}" } } ],
"filter": [ { "term": { "tags": "{{tag}}" } } ]
}
},
"size": "{{size}}{{^size}}10{{/size}}"
}
}
}
GET /articles/_search/template
{
"id": "articles-by-tag",
"params": { "q": "replication", "tag": "internals", "size": 5 }
}
\{{^size}}…\{{/size}} is a Mustache "inverted section" supplying a default when size is
absent. \{{#toJson}}myParam\{{/toJson}} injects an array or object parameter, and
\{{#join}}myArray\{{/join}} comma-joins a list. Use _render/template to see the query a set of
params produces without running it:
POST /_render/template
{
"id": "articles-by-tag",
"params": { "q": "replication", "tag": "internals" }
}
An inline (non-stored) template is passed as "source" in the same call instead of "id". Stored
templates are cluster state, so every node has them and they need no redeploy.
Percolation: matching documents against stored queries
The percolate query inverts search. Instead of running one query over many documents, you store many queries as documents and, given one incoming document, find every stored query that document would match. This is how you build "email me when a listing matches my saved search" alerting or content classification/tagging. See Percolate query.
The index needs a percolator field plus the same mappings the stored queries will run against:
PUT /saved-searches
{
"mappings": {
"properties": {
"query": { "type": "percolator" },
"title": { "type": "text" },
"price": { "type": "double" },
"tags": { "type": "keyword" }
}
}
}
PUT /saved-searches/_doc/alert-1?refresh
{
"query": {
"bool": {
"must": [ { "match": { "title": "road bike" } } ],
"filter": [ { "range": { "price": { "lte": 800 } } } ]
}
}
}
Now percolate a candidate document — it is not indexed, just matched:
GET /saved-searches/_search
{
"query": {
"percolate": {
"field": "query",
"document": {
"title": "Carbon road bike, barely used",
"price": 650,
"tags": ["cycling"]
}
}
}
}
Each returned hit is a stored query that matched; add highlight to see which terms of the
candidate triggered each one. percolate also accepts documents (an array, matched in one call)
or index + id to pull an already-indexed document. The percolator field parses and stores each
query at index time, so mapping changes to the queried fields after queries are stored can require
reindexing the percolator index.
More Like This: finding similar documents
The more_like_this query ("MLT") extracts representative terms from one or more input
texts/documents using a TF-IDF-style selection, forms a disjunction (should) query from those
terms, and runs it — a lexical, term-based similarity search. See
More like this query.
Fields referenced via like/unlike must be text or keyword. There are two ways to supply the
"like" input:
-
inline free text —
like: "text to match", or a per-field{ "text": "…", "fields": […] }entry — which needs no special indexing beyond the field being analyzed; -
a reference to an existing document —
like: [{ "_index": "…", "_id": "…" }]— which needs either_sourceenabled (the default mapping, works out of the box) or the field mapped"store": true/"term_vector": "yes"(orwith_positions/with_positions_offsets) so Elasticsearch reuses stored term statistics instead of re-analyzing_sourceat query time on every request — the same trade-off as thefvhhighlighter’sterm_vectornote in Choosing a highlighter above.
Key tuning parameters, with their current defaults:
| Parameter | Default |
|---|---|
|
|
|
(required / none) — free text, per-field text, or document references |
|
|
|
|
|
|
|
|
|
none |
|
the analyzer of the first field in |
|
|
|
|
|
|
Best uses: "related articles/products" recommendations, near-duplicate/duplicate-content detection, lightweight content-based recommendations, and moderation (grouping similar spam/abuse reports) — a cheap alternative to embeddings when semantic nuance isn’t required.
MLT matches shared vocabulary, not shared meaning: it is purely lexical, needs no embedding model or
inference cost, and works immediately on existing text mappings. When queries and documents may
share meaning without sharing words, see
Vector & semantic search for the
embedding-based alternative.
PUT /articles
{
"mappings": {
"properties": {
"title": { "type": "text" },
"body": { "type": "text", "term_vector": "yes" },
"tags": { "type": "keyword" }
}
}
}
GET /articles/_search
{
"query": {
"more_like_this": {
"fields": ["title", "body"],
"like": "vector databases and approximate nearest neighbour search",
"min_term_freq": 1,
"max_query_terms": 12
}
}
}
GET /articles/_search
{
"query": {
"more_like_this": {
"fields": ["title", "body"],
"like": [
{ "_index": "articles", "_id": "42" }
],
"unlike": [
{ "_index": "articles", "_id": "7" }
],
"min_term_freq": 2,
"min_doc_freq": 3
}
}
}
The first query matches on free text; the second finds articles similar to document 42 while
steering away from document 7, both re-using body’s stored term vectors. `include: true returns
the input documents themselves alongside the results (default false excludes them), useful when
testing relevance tuning.
Related pages
-
Text analysis — analyzers,
edge_ngram/shinglefilters, and the manual autocomplete approach contrasted above. -
Full-text queries and Compound queries & relevance — the queries that highlighting, collapse, and percolation operate on.
-
Aggregations —
termsandcardinalityfor exact group counts alongsidecollapse. -
Search API & pagination —
search_after, PIT, andsort, all compatible withcollapse. -
Vector & semantic search — the embedding-based alternative to
more_like_thisfor matching on shared meaning rather than shared vocabulary.