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 }