MongoDB aggregations are easy
Tags: mongodb, node, javascript, database
Published: 2021-08-01 06:01:15

Building aggregation pipelines sometimes just seems so hard, especially when you're working with NoSQL database such as MongoDB, since there is no defined schema, and there are just so many nested fields

Well in this article i'm not going to go deep into what aggregation query is, since i think  [MongoDB Docs](https://docs.mongodb.com/manual/core/aggregation-pipeline/)  explained this perfectly, but instead i will go through a kind of seeming-complex aggregation query, which is you probably going to face if you're currently with MongoDB and i will use this example here to just show you how aggregation pipelines are just logically easy, and it's always depends on how you think while building it

## The example

Well for the sake of our example, let's consider that you have this silly `products` collection which has these 2 documents:

```
{
  "_id": {
    "$oid": "55c30ff62cfa09af198b465a"
  },
  "name": "Awesome Tshirt",
  "currency": "usd",
  "one_size": "xl",
  "variants": [
    {
      "type": "color",
      "base_sku": 132145,
      "items": [
        {
          "color": "Grey Melange",
          "price": 80,
          "sku": 1243252369
        },
        {
          "color": "Bottle Green",
          "price": 90,
          "sku": 1243252368
        },
        {
          "color": "Deep Charcoal Grey",
          "price": 80,
          "sku": 1243252376
        },
        {
          "color": "White",
          "price": 80,
          "sku": 1243252363
        },
        {
          "color": "Black",
          "price": 80,
          "sku": 1243252362
        }
      ]
    }
  ]
},
{
  "_id": {
    "$oid": "55c30ff62cfa09af198b465c"
  },
  "name": "Hacker Tshirt",
  "currency": "usd",
  "one_size": false,
  "variants": [
    {
      "type": "color",
      "base_sku": 132155,
      "items": [
        {
          "color": "Black",
          "price": 100,
          "sku": 87987963
        }
      ]
    },
    {
      "type": "size",
      "base_sku": 342434,
      "items": [
        {
          "size": "sm",
          "price": 100,
          "sku": 97896796
        },
        {
          "size": "xl",
          "price": 100,
          "sku": 43534534
        },
        {
          "size": "xxl",
          "price": 100,
          "sku": 76576532
        }
      ]
    }
  ]
}
```

and now our goal is to get the price of all grey T-shirts, so since as you can see that each product has it's price vary based on the variant itself, so we have to get both from variants items `item` object. Yes in this collection its so silly that somehow you have to choose of either having black T-shirt or XXl tshirt, but not both :D

## Stage #1

The very first step when you create a pipeline is to always `match` your query, this to narrow down querying the whole collection to just a limited number of documents that match your criteria

> You always want to narrow down the number of documents you're searching within at the very beginning of the aggregation pipeline, this will lead to faster queries

So let's do this, first we want to use only the documents that has `color` variant, and it has also grey color inside it's variant items. So this is how we're translating this:

```
    {
        '$match': {
            'variants': {
                '$elemMatch': {
                    'type': 'color', 
                    'items': {
                        '$elemMatch': {
                            'color': /grey/i
                        }
                    }
                }
            }
        }
    }
```

>  [$elemMatch](https://docs.mongodb.com/manual/reference/operator/query/elemMatch/) operator matches documents that contain an array field with at least one element that matches all the specified query criteria.

We're using $elemMatch here to find a variant of `type` **color** first, and then we're using it again to find an element of color that contains **grey** and its case insensitive - notice the regex `/grey/i` - 

So this was our first step, notice that only 1 item will be returned in this case that has ID `55c30ff62cfa09af198b465a`, since it is the only one with variants of type color that has grey color

Document `55c30ff62cfa09af198b465c` it has variants of type color, but it has only black color

## Stage #2

Now we still need to query a nested object that is inside variants *(array)* and also inside items *(array)*, so its more like this `variants -> items -> {color, price}`

This seems complicated, and since we're dealing with array of objects here, what could make it easier? .. to just deal with this array as an object instead, so we can just leverage the dot notation syntax in Mongo

Well we can do that by just unwinding the array! simple as that, just flatten this array into objects by using `$unwind`

>  [$unwind](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/) Deconstructs an array field from the input documents to output a document for each element

$unwind is so simple that the only required parameter to pass is `path` which is the path of the array you want to flatten

```
  {
    '$unwind': {
      'path': '$variants'
    }
  }
```

Notice the dollar sign before `variants`, we have to prefix the field name with it so Mongo can interpret it, it just tells Mongo to inject the actual value of `variants`

Now by then we will have also 1 document still, since `variants` array has only 1 element

![Stage #2 output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wsi9okmw3je79gsi6f0z.jpeg)

Notice that `variants` is now object instead of array

## Stage #3

Now what? we still need to query `variants.items` elements which is the same case as variants before we unwind it. So i guess we will have to flatten `variants.items` too, so next stage will be

```
  {
    '$unwind': {
      'path': '$variants.items'
    }
  }
```

Notice now that we can easily access `items` with dot notation, since `variants` is an object and not an array anymore, now these are the the new documents returned after this stage

![Stage #3 output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0o7lngz0t9p7plbeg55c.jpeg)

`items` is an object now with 1 document per `items` element which is exactly what we need, but did you notice something strange?

## Stage #4

Now we have documents with `items.color` value that does not contains `grey` we have Black, White, and Bottle Green as well, why is that?

Well that would be because our first `$match` stage was  only getting the documents that have items with grey color, having this does not necessary means that it will magically just filter other colors from `items`, this our job to do now

So now we will need to get only the documents that has `variants.items.color` with greyish color, looks like another `$match` query, right?

```
  {
    '$match': {
    'variants.type': 'color', 
    'variants.items.color': /grey/i
  }
```

It is so simple now, here we're just saying, out of the result of stage 3 we just want the documents which has variants of type `color` and any item that has `grey` with case insensitive, that will return us these documents:

![Stage #4 output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bbf1xrqfihu8f1ic5vxv.jpeg)
 
## Stage #5

Now we have the documents, but we don't really care about all of these properties right? We only care about the color and the price

So since we're having a couple levels of nesting here `variants -> items` and we only care about `price` and `color` properties, then we need to focus/project these properties only and ignore any other stuff we don't need

>  [$project](https://docs.mongodb.com/manual/reference/operator/aggregation/project/) Passes along the documents with the requested fields to the next stage in the pipeline.

$project is so simple also, you'll probably just need to know that:

- It takes field name as a key and 1 or 0 as value (1 to show, 0 to hide)
- `_id` is shown by default, unless you specify to hide it
- You have to specify at least 1 output field

So our stage implementation will be simple as

```
  {
    '_id': 0,
    'color': '$variants.items.color',
    'price': '$variants.items.price'
  }
```

Notice that we passed `'_id': 0` because we don't really care about the document ID - at least not in this example, normally you'll need it though - so we just hid it

So now this will be the final result

![Stage #5 output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3eb5jdo9bae39xaio4xg.jpeg)

## The aggregation query

```
db.getCollection('products').aggregate([
  {
    '$match': {
      'variants': {
        '$elemMatch': {
          'type': 'color', 
          'items': {
            '$elemMatch': {
              'color': new RegExp('grey', 'i')
            }
          }
        }
      }
    }
  },
  {
    '$unwind': {
      'path': '$variants'
    }
  },
  {
    '$unwind': {
      'path': '$variants.items'
    }
  },
  {
    '$match': {
      'variants.type': 'color', 
      'variants.items.color': new RegExp('grey', 'i')
    }
  },
  {
    $project:  {
      '_id': 0,
      'color': '$variants.items.color',
      'price': '$variants.items.price'
    }
  }
])
```

## Final words

As you can see it's pretty straight forward, and it's quite easy and seems quite logical too, building aggregation pipelines stages is just like actually talking to rubber duck while coding, instead this time you're just talking to Mongo shell 

This post was originally published on my blog [blog.mrg.sh](https://blog.mrg.sh/mongodb-aggregations-are-easy)