$facet
\$facet runs multiple aggregation pipelines over the same input set in a single pass. The classic use is a search results page that needs both the paged hits AND aggregate counts (by category, by price bucket, by rating). One round trip, one shared scan — vastly faster than firing N queries from the app.
Search results + facets in one aggregation
EXAMPLE
// products in 'shop': { name, category, price, rating, brand }
const pipeline = [
// 1) Filter once; $facet sees the filtered set
{ $match: {
$text: { $search: 'wool jacket' },
price: { $gte: 50, $lte: 500 },
status: 'active',
} },
// 2) Fan out into multiple pipelines, each independent
{ $facet: {
hits: [
{ $sort: { score: { $meta: 'textScore' }, _id: 1 } },
{ $skip: 0 },
{ $limit: 20 },
{ $project: { name: 1, category: 1, price: 1, rating: 1, brand: 1,
score: { $meta: 'textScore' } } },
],
total: [
{ $count: 'value' },
],
categories: [
{ $group: { _id: '$category', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 },
],
brands: [
{ $group: { _id: '$brand', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 },
],
price_buckets: [
{ $bucket: {
groupBy: '$price',
boundaries: [0, 50, 100, 200, 500, 1000, Infinity],
default: 'other',
output: { count: { $sum: 1 } },
} },
],
avg_rating: [
{ $group: { _id: null, avg: { $avg: '$rating' } } },
],
} },
// 3) Flatten the single-element scalar facets for the client
{ $project: {
hits: 1,
categories: 1,
brands: 1,
price_buckets: 1,
total: { $ifNull: [{ $arrayElemAt: ['$total.value', 0] }, 0] },
avg_rating: { $ifNull: [{ $arrayElemAt: ['$avg_rating.avg', 0] }, null] },
} },
];
const [result] = await db.collection('products').aggregate(pipeline).toArray();
// result.hits -> 20 matching products
// result.total -> total matching count
// result.categories -> [{ _id: 'Outerwear', count: 312 }, ...]
// result.price_buckets-> [{ _id: 50, count: 90 }, ...]
Why it matters
\$facet runs each sub-pipeline serially on the server, so do not throw 20 facets in there — three or four is the right ceiling. If you need many, run the heavy ones on background materialised views (\$merge) and only compute the cheap counts on each request.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Run several pipelines on the same input
{ $facet: {
paid: [{ $match: { status: 'paid' } }],
counts: [{ $count: 'n' }],
} }
Try it Yourself »
Discussion
Loading…