✏ MongoDB Aggregation: $project stage
This stage takes a collection of documents, and projects each document on a (usually) smaller document. You can include/exclude, rename and restructure fields in this stage. Typically, this stage comes after $match
and $group
, and is meant to re-format the output documents.
In my understanding, $project
is closest to Array.prototype.map
, because it doesn't filter out any documents, and doesn't restructure the collection (array), but restructures each document (array item).
In that regard, it's very close to .find({})
with a chained .select()
. I've shown yesterday how you can "mimic" the same behaviour with $group
if you use it without any accumulators, but that's not really the purpose of $group
.
Syntax
The syntax is the following:
<collection>.aggregate([
{ $project: {
<field1> : 0,
<field2>: 1,
<newField>: <expression>
}
}
]);
The values can be either 0
, 1
or an expression:
0
excludes the field1
includes the field<expression>
creates a new field
Some additional rules that apply:
the
_id
field is always implicitly included, unless explicitly excludedif only exclusions are specified, all other fields are implicitly included
if only inclusions are specified, all other fields are implicitly excluded
if you exclude any field other than
_id
, you cannot explicitly include any other field
Might sound complicated but it's not, let's see some examples. Here's the model of the collection again that I'm working on:
const IngredientSchema = new Schema(
{
name: String,
amount: Number,
unit: String
}
);
const RecipeSchema = new Schema(
{
title: String,
ingredients: [ IngredientSchema ],
instructions: String,
info: {
category: String
time: {
preparation: Number,
cooking: Number,
}
}
}
);
✏ Including/excluding fields
If I'm only interested in a list of recipes with their titles and ingredients, I'd explicitly include those fields:
Recipe.aggregate([
{ $project: { _id: 0, title: 1, ingredients: 1 } }
}
]);
// result
[
{ title: 'Soup', ingredients: [ [Object], [Object], [Object] ] },
{ title: 'Pasta', ingredients: [ [Object], [Object], [Object] ] },
{ title: 'Salad', ingredients: [ [Object], [Object], [Object] ] },
{ title: 'Dessert', ingredients: [ [Object] ] }
]
I could've achieved the same result by explicitly excluding the fields for instructions and info:
Recipe.aggregate([
{ $project: { _id: 0, info: 0, instructions: 0 } }
}
]);
✏ Renaming fields
If I'm only interested in the recipe titles and a list of the ingredient names:
Recipe.aggregate([
{ $project: {
_id: 0,
title: 1,
ingredientsList: '$ingredients.name'
}
}
}
]);
// result
[
{ title: 'Soup', ingredientsList: [ 'tomatos', 'salt', 'pepper' ] },
{ title: 'Pasta', ingredientsList: [ 'spaghetti', 'salt', 'pesto' ] },
{ title: 'Salad', ingredientsList: [ 'tomatos', 'oil', 'pepper' ] },
{ title: 'Dessert', ingredientsList: [ 'tomatos' ] }
]
✏ Operators in $project stage
Typical operators in this stage are arithmetic, array and type operators.
$add
To get a list of recipe titles together with the total time (the sum of preparation and cooking time):
Recipe.aggregate([
{ $project: {
_id: 0,
title: 1,
totalTime: { $add: ['$info.time.preparation', '$info.time.cooking'] }
}
}
}
]);
// result
[
{ title: 'Soup', totalTime: 17 },
{ title: 'Pasta', totalTime: 9 },
{ title: 'Salad', totalTime: 10 },
{ title: 'Dessert', totalTime: 0 }
]
$size
To get a list of recipe titles along with the number of ingredients for each:
Recipe.aggregate([
{ $project: {
_id: 0,
title: 1,
numIngredients: { $size: '$ingredients' }
}
}
}
]);
// result
[
{ title: 'Soup', numIngredients: 3 },
{ title: 'Pasta', numIngredients: 3 },
{ title: 'Salad', numIngredients: 3 },
{ title: 'Dessert', numIngredients: 1 }
]
$toBool
This is a bit of a contrived example, but to list all recipes along with a Boolean to indicate whether they have a preparation time of 0
or > 0
:
Recipe.aggregate([
{ $project: {
_id: 0,
title: 1,
prepTime: { $toBool: '$info.time.preparation' }
}
}
}
]);
// result
[
{ title: 'Soup', prepTime: true },
{ title: 'Pasta', prepTime: true },
{ title: 'Salad', prepTime: true },
{ title: 'Dessert', prepTime: false }
]
That's it for the $project
stage. Next: handling array fields with $unwind
.
✏ Recap
This post covered:
- MongoDB aggregation:
$project
stage
✏ Thanks for reading!
I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subscribe to my newsletter. Until next time 👋
✏ Previous Posts
You can find an overview of all previous posts with tags and tag search here: