Grouping & collapse
|
This section documents the current Solr line (10.0; 9.10.x the maintained 9.x branch), written and verified against the Apache Solr Reference Guide. No specific patch version is pinned. Some capabilities (the Solr Operator on Kubernetes, the package-manager ecosystem, Learning To Rank model training, and expert plugin development) 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. This section’s bibliography lists the reference material consulted while preparing these pages. |
Solr has two different features for "one document per bucket" results, and they are not
interchangeable. Result grouping (group=true) is a request-time feature of the search component
that buckets the whole result set and returns several documents per bucket. Field collapsing — the Collapsing query parser plus the Expand component — is a post-filter that reduces the result
set to one document per bucket before scoring/faceting/paging see it, with a separate opt-in request
to fetch the rest of each bucket. Reach for collapsing whenever the goal is deduplication (one row per
product, per author, per host) and you still want normal faceting and paging math to work on the
collapsed set; reach for grouping when you explicitly want multiple documents per bucket returned
inline, or when the bucket key is a function or an arbitrary query rather than a stored field.
Result grouping
group=true reruns field/query/function buckets over the matching documents and nests group.limit
(default 1) documents under each bucket, instead of returning one flat list. See
Result Grouping.
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=*:*" \
--data-urlencode "group=true" \
--data-urlencode "group.field=genre_ss" \
--data-urlencode "group.limit=3" \
--data-urlencode "group.sort=stars_i desc" \
--data-urlencode "sort=score desc" \
--data-urlencode "rows=5"
# One bucket per distinct genre_ss value, top 3 books per bucket by stars_i,
# buckets themselves ordered by rows/sort against the ungrouped relevance order.
# https://solr.apache.org/guide/solr/latest/query-guide/result-grouping.html
group.field needs a single-valued, non-tokenized (string-like) field — the same
indexed/docValues shape a facet field needs; see
Schema & fields and
Faceting for that requirement in the sibling feature.
Grouping by function and by query
group.func buckets by the value a function query returns
for each document — useful when the bucket key is derived (a price band, a rounded date) rather than
stored verbatim. group.query instead defines each bucket as matching a query, so a document can
appear in more than one bucket, and the "no bucket key field" restriction disappears entirely:
# Bucket by a computed price band instead of a stored field.
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=*:*" \
--data-urlencode "group=true" \
--data-urlencode "group.func=map(price_f,0,20,\"budget\",\"premium\")" \
--data-urlencode "group.limit=3"
# Bucket by arbitrary queries -- a document matching several appears in each.
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=*:*" \
--data-urlencode "group=true" \
--data-urlencode "group.query=genre_ss:classic" \
--data-urlencode "group.query=in_stock_b:true" \
--data-urlencode "group.limit=3"
# https://solr.apache.org/guide/solr/latest/query-guide/result-grouping.html
Paging and sorting grouped results
Grouped results page along two independent axes: sort orders the buckets themselves (which top-
level page of buckets start/rows returns), while group.sort orders the documents within each
bucket (which top group.limit documents that bucket shows) — defaulting to the main sort when
omitted. group.ngroups adds the total bucket count to the response (an extra counting pass over the
whole result set), and group.main=true flattens the response back into a plain, ungrouped-looking
document list — one bucket’s group.limit documents merged per top-level position — for clients that
cannot consume the nested grouped response shape.
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=*:*" \
--data-urlencode "group=true" \
--data-urlencode "group.field=genre_ss" \
--data-urlencode "group.ngroups=true" \
--data-urlencode "sort=count(*) desc" \
--data-urlencode "rows=10"
# https://solr.apache.org/guide/solr/latest/query-guide/result-grouping.html
The Collapsing query parser
The Collapsing query parser runs as a post-filter — fq=\{!collapse field=…} — reducing the
result set to one document (the group head) per distinct value of field before scoring, faceting,
and paging see it, rather than nesting several documents per bucket the way group=true does. See
Collapse and
Expand Results.
# One book per author -- pick the head by highest stars_i, ties broken by score.
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=title:earth" \
--data-urlencode 'fq={!collapse field=author_s max=stars_i}' \
--data-urlencode "sort=score desc"
# https://solr.apache.org/guide/solr/latest/query-guide/collapse-and-expand-results.html
field must be a single-valued string or numeric docValues field (declare it in the schema per
Schema & fields). The group head is picked by exactly one
of:
| Local param | Picks the head as |
|---|---|
(none) |
The document with the highest |
|
The document with the lowest/highest value of a given numeric field or function query. |
|
The document sorted first by an arbitrary |
nullPolicy decides what happens to documents missing field entirely — ignore (default, drop
them), expand (each null-valued document is its own untouched group of one), or collapse (all
null-valued documents collapse together into a single group). hint=top_fc optimizes collapsing on a
low-cardinality field cached via the field cache; hint=block (with size) optimizes collapsing on
nested/block-indexed documents' root
field, which is markedly faster than the general path for that specific case.
# Collapse null-author documents together instead of dropping them.
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=*:*" \
--data-urlencode 'fq={!collapse field=author_s nullPolicy=collapse}'
# https://solr.apache.org/guide/solr/latest/query-guide/collapse-and-expand-results.html
The Expand component
Collapsing discards every non-head document from the result set — the Expand component is a separate,
opt-in way to fetch them back, per group, alongside the collapsed results. It reruns the query without
the \{!collapse} filter, groups what it finds by expand.field (defaulting to the collapse field),
and returns up to expand.rows (default 5) documents per group in its own expanded block of the
response, ordered by expand.sort (default score desc):
curl --get "http://localhost:8983/solr/books/select" \
--data-urlencode "q=title:earth" \
--data-urlencode 'fq={!collapse field=author_s max=stars_i}' \
--data-urlencode "expand=true" \
--data-urlencode "expand.rows=3" \
--data-urlencode "expand.sort=stars_i desc"
# response.response -> one collapsed head document per author_s value
# response.expanded -> { "<author_s value>": { "docs": [...] }, ... } the rest of each group
# https://solr.apache.org/guide/solr/latest/query-guide/collapse-and-expand-results.html
expand.q and expand.fq let the expansion re-run against a different query/filter than the main
request — for example, expanding under a looser query than the one that picked the collapsed heads.
Grouping and collapsing gotchas
-
Distributed grouping.
group.funcis not supported at all across shards.group.ngroupsandgroup.facetneed every document that shares a bucket key to live on the same shard — true by default only in a single-shard/non-cloud deployment, or in SolrCloud if you routed documents by that key. Collapsing has no such restriction: the post-filter runs per shard and the coordinating node merges collapsed results normally, which is why collapsing scales to a sharded collection more predictably than grouping does. See Distributed indexing & search and SolrCloud architecture for shard routing. -
Faceting on groups.
group.facetrecomputes facet counts per group (one document’s field values count once per bucket it heads, not once per matching document) and is markedly more expensive than ordinary faceting — it re-walks the un-collapsed result set once per facet field. Field collapsing has no equivalent switch: facets computed on a\{!collapse}-filtered result set count only the collapsed heads, which is normal (cheap) faceting but is a different number than faceting on the full uncollapsed set — pick whichever semantics the UI actually needs. See Faceting. -
Performance. Grouping executes an extra sort pass per bucket and, with
group.ngroups, an extra counting pass over the whole matching set — cost that grows with the number of distinct bucket values, not justrows. Collapsing’s per-shard post-filter is generally cheaper per query but still scans every matching document to pick each group’s head;hint=top_fc(low-cardinality fields) andhint=block(nested documents) are the two collapsing paths with dedicated fast implementations — everything else pays the general-purpose cost. See Indexing internals & performance. -
Neither is a substitute for deduplicating at index time. If a "one row per X" shape is the only shape you ever need, collapsing/grouping re-does that work on every query; consider denormalizing so one document per X exists in the index to begin with.