An Introduction to Mongoose Aggregate

May 18, 2020

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 }

Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!

Did you find this tutorial useful? Say thanks by starring our repo on GitHub!

More Mongoose Tutorials