An Introduction to Mongoose Aggregate
Mongoose's aggregate() function
is how you use MongoDB's aggregation framework with Mongoose. Mongoose's aggregate() is a thin wrapper, so any aggregation query that works in the MongoDB shell should work in Mongoose without any changes.
What is the Aggregation Framework?
Syntactically, an aggregation framework query is an array of stages. A
stage is an object description of how MongoDB should transform any
document coming into the stage. The first stage feeds documents into
the second stage, and so on, so you can compose transformations using
stages. The array of stages you pass to the aggregate() function
is called an aggregation pipeline.
The $match Stage
The $match stage filters out documents that don't match the given
filter parameter, similar to filters for Mongoose's find() function.
await Character.create([
{ name: 'Jean-Luc Picard', age: 59, rank: 'Captain' },
{ name: 'William Riker', age: 29, rank: 'Commander' },
{ name: 'Deanna Troi', age: 28, rank: 'Lieutenant Commander' },
{ name: 'Geordi La Forge', age: 29, rank: 'Lieutenant' },
{ name: 'Worf', age: 24, rank: 'Lieutenant' }
]);
const filter = { age: { $gte: 30 } };
let docs = await Character.aggregate([
{ $match: filter }
]);
docs.length; // 1
docs[0].name; // 'Jean-Luc Picard'
docs[0].age // 59
// `$match` is similar to `find()`
docs = await Character.find(filter);
docs.length; // 1
docs[0].name; // 'Jean-Luc Picard'
docs[0].age // 59
The $group Stage
Aggregations can do much more than just filter documents. You can also use
the aggregation framework to tranform documents. For example, the $group
stage behaves like a reduce() function. For example, the $group stage
lets you count how many characters have a given age.
let docs = await Character.aggregate([
{
$group: {
// Each `_id` must be unique, so if there are multiple
// documents with the same age, MongoDB will increment `count`.
_id: '$age',
count: { $sum: 1 }
}
}
]);
docs.length; // 4
docs.sort((d1, d2) => d1._id - d2._id);
docs[0]; // { _id: 24, count: 1 }
docs[1]; // { _id: 28, count: 1 }
docs[2]; // { _id: 29, count: 2 }
docs[3]; // { _id: 59, count: 1 }
Combining Multiple Stages
The aggregation pipeline's strength is its composability. For example,
you can combine the previous two examples to only group characters
by age if their age is < 30.
let docs = await Character.aggregate([
{ $match: { age: { $lt: 30 } } },
{
$group: {
_id: '$age',
count: { $sum: 1 }
}
}
]);
docs.length; // 3
docs.sort((d1, d2) => d1._id - d2._id);
docs[0]; // { _id: 24, count: 1 }
docs[1]; // { _id: 28, count: 1 }
docs[2]; // { _id: 29, count: 2 }
Mongoose Aggregate Class
Mongoose's aggregate() function returns an instance of Mongoose's
Aggregate class.
Aggregate instances are thenable,
so you can use them with await and promise chaining.
The Aggregate class also supports a chaining interface for building
aggregation pipelines. For example, the below code shows an alternative
syntax for building an aggregation pipeline with a $match followed by
a $group.
let docs = await Character.aggregate().
match({ age: { $lt: 30 } }).
group({ _id: '$age', count: { $sum: 1 } });
docs.length; // 3
docs.sort((d1, d2) => d1._id - d2._id);
docs[0]; // { _id: 24, count: 1 }
docs[1]; // { _id: 28, count: 1 }
docs[2]; // { _id: 29, count: 2 }
Mongoose middleware also
supports pre('aggregate') and post('aggregate') hooks. You can use
aggregation middleware to transform the aggregation pipeline.
const characterSchema = Schema({ name: String, age: Number });
characterSchema.pre('aggregate', function() {
// Add a `$match` to the beginning of the pipeline
this.pipeline().unshift({ $match: { age: { $lt: 30 } } });
});
const Character = mongoose.model('Character', characterSchema);
// The `pre('aggregate')` adds a `$match` to the pipeline.
let docs = await Character.aggregate().
group({ _id: '$age', count: { $sum: 1 } });
docs.length; // 3
docs.sort((d1, d2) => d1._id - d2._id);
docs[0]; // { _id: 24, count: 1 }
docs[1]; // { _id: 28, count: 1 }
docs[2]; // { _id: 29, count: 2 }