$facet and $bucket / $bucketAuto
$facet runs several independent sub-pipelines over the same input documents in a single pass, returning all their results together — perfect for building a search results page that needs matched items, a total count, and price-range breakdowns all at once. $bucket/$bucketAuto group documents into ranges — the aggregation equivalent of a histogram.
$facet — Multiple Sub-Pipelines in One Pass
Each key inside $facet names an independent sub-pipeline. All of them read from the same set of documents entering the $facet stage — none of them see each other's output, and each produces its own array of results under its key name.
Faceted search: results + total count + category counts, in one query
db.products.aggregate([
{ $match: { category: { $in: ["widgets", "gadgets"] }, inStock: true } },
{
$facet: {
results: [
{ $sort: { price: 1 } },
{ $skip: 0 },
{ $limit: 20 }
],
totalCount: [
{ $count: "count" }
],
categoryCounts: [
{ $group: { _id: "$category", count: { $sum: 1 } } }
],
priceRanges: [
{ $bucket: {
groupBy: "$price",
boundaries: [0, 25, 50, 100, 500],
default: "500+",
output: { count: { $sum: 1 } }
} }
]
}
}
])[
{
results: [ { _id: ..., name: 'Small Widget', price: 4.99, ... }, /* ...19 more */ ],
totalCount: [ { count: 87 } ],
categoryCounts: [
{ _id: 'widgets', count: 52 },
{ _id: 'gadgets', count: 35 }
],
priceRanges: [
{ _id: 0, count: 30 },
{ _id: 25, count: 40 },
{ _id: 50, count: 12 },
{ _id: '500+', count: 5 }
]
}
]$facet always returns a single output document whose fields are the named sub-pipeline results — this is the building block behind almost every e-commerce or search "results + filters sidebar" API response, computed with one round-trip instead of three or four separate queries.$facet runs against the same input, but $facet stages cannot use indexes for anything after the initial $match — all the sub-pipelines operate on documents already pulled into the $facet stage. Keep the leading $match selective so the amount of data flowing into $facet stays reasonable.$bucket — Fixed Boundaries
$bucket groups documents into buckets you define explicitly with boundaries — an ascending array of values marking the edges of each range. Every bucket is [boundary[i], boundary[i+1]) — inclusive of the lower edge, exclusive of the upper.
Price histogram with fixed boundaries
db.products.aggregate([
{
$bucket: {
groupBy: "$price",
boundaries: [0, 25, 50, 100, 500],
default: "500+", // catches anything outside all boundaries
output: {
count: { $sum: 1 },
avgPrice: { $avg: "$price" },
names: { $push: "$name" }
}
}
}
])groupBy value must fall within boundaries or match default — omit default and a document outside every boundary range causes the entire stage to error.$bucketAuto — Let MongoDB Pick the Boundaries
$bucketAuto doesn't require you to specify boundaries — you just say how many buckets you want, and MongoDB distributes documents into that many groups, trying to keep bucket sizes roughly even.
Automatic 4-bucket price histogram
db.products.aggregate([
{
$bucketAuto: {
groupBy: "$price",
buckets: 4,
output: { count: { $sum: 1 }, avgPrice: { $avg: "$price" } }
}
}
])
// [
// { _id: { min: 1.99, max: 15.00 }, count: 62, avgPrice: 8.20 },
// { _id: { min: 15.00, max: 40.00 }, count: 58, avgPrice: 27.10 },
// ...
// ]Stage | Boundaries | Best For |
|---|---|---|
$bucket | You specify exact boundaries | Known, meaningful ranges — price tiers, age brackets, score bands |
$bucketAuto | MongoDB picks boundaries for roughly even bucket sizes | Exploratory analysis — quick histograms without deciding ranges up front |
Full E-Commerce Faceted Search Example
Putting it all together — a single aggregation that powers a complete search results page: filtered results, pagination info, category filter counts, and a price-range histogram for the sidebar.
Complete faceted search query
const PAGE_SIZE = 20
const page = 1
db.products.aggregate([
{ $match: {
$text: { $search: "wireless keyboard" },
inStock: true
} },
{
$facet: {
results: [
{ $sort: { score: { $meta: "textScore" } } },
{ $skip: (page - 1) * PAGE_SIZE },
{ $limit: PAGE_SIZE },
{ $project: { name: 1, price: 1, category: 1, rating: 1 } }
],
totalCount: [{ $count: "count" }],
byCategory: [
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
],
byBrand: [
{ $group: { _id: "$brand", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
],
priceHistogram: [
{ $bucketAuto: { groupBy: "$price", buckets: 5 } }
]
}
},
{
$project: {
results: 1,
totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
byCategory: 1,
byBrand: 1,
priceHistogram: 1
}
}
])$facet shares the same upstream $match, the results, counts, and filter breakdowns are always consistent with each other — no risk of the result count and the filter sidebar disagreeing because they ran as separate queries at slightly different times.$facetruns multiple named sub-pipelines over the same input, returning all their results together in one document.$bucketgroups by explicit boundaries;$bucketAutolets MongoDB choose roughly even-sized buckets.Keep the
$matchbefore$facetselective — sub-pipelines inside$facetcan't use indexes.This combination is the standard pattern behind faceted e-commerce / search UIs: results + counts + filters in one round-trip.