#100DaysOfMERN - Day 50

#100DaysOfMERN - Day 50

·

4 min read

✏ 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 field

  • 1 includes the field

  • <expression> creates a new field

Some additional rules that apply:

  • the _id field is always implicitly included, unless explicitly excluded

  • if 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:

#100DaysOfMERN - The App