How do you return Atlas Search facet counts alongside the matching documents?
answer
- Counts describe the whole match set, not the page
- A dedicated stage returns metadata only
- The facet mapping differs from the searchable mapping
- Metadata reaches later stages via a system variable
basics
~20 sUse the facet collector inside $search and read the buckets from the $$SEARCH_META variable in a later stage, or run $searchMeta when you want the counts alone. Facet paths must be mapped as stringFacet, numberFacet or dateFacet.
solid answer
~50 sThere are two entry points. `$searchMeta` runs the same query but returns only a metadata document — counts and facet buckets, no matching documents — which suits a sidebar refresh or a "how many results" call. To get documents and facets from one query, wrap the query in the `facet` collector inside `$search`: the collector takes an `operator` (the real query) and a `facets` object naming each facet, its `type` and `path`. The documents flow down the pipeline as usual, and the metadata is exposed through the `$$SEARCH_META` system variable, which a later stage can project or attach to the response. Facetable paths are not free: the index definition must map them as `stringFacet`, `numberFacet` or `dateFacet`, which is a different mapping from an ordinary searchable string. Number and date facets need explicit `boundaries`; string facets take an optional `numBuckets`.
code
javascript · 10 linesdb.products.aggregate([
{ $searchMeta: {
index: "default",
facet: {
operator: { text: { query: "laptop", path: "title" } },
facets: { brandFacet: { type: "string", path: "brand", numBuckets: 5 } }
},
count: { type: "lowerBound", threshold: 1000 }
} }
]);go deeper
Recall that facet buckets come from the search engine itself rather than a separate aggregation, and that the field must be mapped for faceting in the index definition.
Explain the two shapes — the metadata-only stage versus the facet collector plus $$SEARCH_META — and what the facet definition needs for string, number and date types.
Show judgment on cost: exact versus lower-bound counts, cardinality limits on facetable fields, and the staleness facet counts inherit from the search index.
Own the response contract: whether counts must be exact for the product, how facet latency is budgeted at peak, and whether sidebar refreshes should be a separate call.
## What faceting is for Faceted navigation is the sidebar on a search results page: brand (Acme 42, Globex 17), price bands, publication year. Computing it means asking, for the *current query*, how many matching documents fall into each bucket — a question about the whole result set, not the page of ten documents you are showing. Atlas Search computes this inside `mongot`, where the matching set already lives, instead of making you re-aggregate the collection. ## The index side Facets need dedicated structures, so the path must be declared in the search index definition with a facet type: `stringFacet`, `numberFacet` or `dateFacet`. A field mapped only as a searchable `string` cannot be faceted, and asking for it produces an error rather than an empty facet. A field frequently needs both mappings — one variant to search on, one to facet on — which the `multi` option or a two-entry array in the `fields` block provides. ## The query side, option one: $searchMeta `$searchMeta` takes the same argument shape as `$search` but returns a single metadata document instead of matching documents. It is the right call when the client only wants counts: a result-count badge, a facet sidebar refreshed independently of the result list, or a "did this filter combination find anything" probe. Because no documents are fetched from `mongod`, it avoids the id-then-lookup round trip entirely. Its `count` option controls how the total is computed: `{ count: { type: "total" } }` returns an exact count, while `{ count: { type: "lowerBound", threshold: N } }` counts exactly up to a threshold and reports a lower bound beyond it. That distinction matters at scale, because exact counting over a huge match set is real work — the same reason web search engines say "about 4,000,000 results". ## The query side, option two: the facet collector When one round trip should return both the page of results and the facet buckets, use the `facet` collector inside `$search`. Its shape is: ```javascript { $search: { index: "default", facet: { operator: { text: { query: "laptop", path: "title" } }, facets: { brandFacet: { type: "string", path: "brand", numBuckets: 10 }, priceFacet: { type: "number", path: "price", boundaries: [0, 500, 1000, 2000] } } } } } ``` The `operator` key holds the query that would otherwise be the whole `$search` argument, and each entry in `facets` names a bucket set. String facets take `numBuckets` (how many top values to return); number and date facets take explicit `boundaries` plus an optional `default` bucket for values outside them. Matching documents continue down the pipeline normally, so `$limit`, `$project` and `$sort` work as usual. The metadata arrives through the **`$$SEARCH_META`** system variable, readable in any later stage — typically attached once with a `$set`/`$facet` combination, or read by a `$group` that emits a single response envelope. Because it is a query-level variable, not a per-document field, referencing it inside `$project` on every document duplicates the same object onto each result; most applications instead run a `$facet` whose one branch pages the documents and whose other branch emits `$$SEARCH_META` once. ## Operational notes Facet counts describe the mongot index, so they inherit its eventual consistency: a document written a moment ago may not yet be counted. On sharded collections `$$SEARCH_META` has restrictions, so validate the pattern against your topology before relying on it. And facets over very high-cardinality string fields are expensive — `numBuckets` caps what is *returned*, not the work of ranking the values, so faceting on something like a free-text field is a design error. ## What to say in an interview Name both stages, state that the facet path needs a facet mapping in the index definition, and explain the `$$SEARCH_META` variable as the channel through which query-level metadata reaches later stages. Mentioning `lowerBound` counting shows you have thought about the cost of exact counts.
- When would you use $searchMeta instead of the facet collector inside $search?When the client needs counts without documents — a results badge, a facet sidebar refreshed on its own, or a cheap check that a filter combination matches anything. `$searchMeta` skips fetching documents from `mongod` entirely, so it is markedly cheaper than running a full `$search` and discarding the hits.
- What does count type lowerBound trade away compared with total?`total` returns an exact match count; `lowerBound` counts precisely up to the given `threshold` and otherwise reports that at least that many matched. Exact counting over a very large match set costs real work in mongot, so `lowerBound` is the right default for result-count displays where "1000+" is good enough.
saying these in an interview costs you the question
- Expects to facet a field mapped only as a searchable string
- Recomputes facets with a separate $group over the collection
- Thinks $searchMeta also returns the matching documents
- Projects $$SEARCH_META onto every document in the result page
- Facets a high-cardinality free-text field and blames the latency