Question

I have a collection with a schema that includes a date. I would like to shape the results of a query so that it is grouped by year and month, but not aggregated, just sorted.

So my model that looks like this:

var RoundSchema = new Schema({
    user: {type: Schema.Types.ObjectId, ref: 'User', required: '{PATH} is required!'},
    course: {type: Schema.Types.ObjectId, ref: 'Course', required: '{PATH} is required!'},
    date: {type: Date, required: '{PATH} is required!'},
    score: Number
});

gets transformed like this:

[
    {
        "year": 2014,
        "month": "April",
        "rounds": [
            {
                "user": "5334d6650685f68c22aa460b",
                "date": "2014-04-23T05:00:00.000Z",
                "course": "5340ab6000806e2433864cfc",
                "score": 73,
                "_id": "534f102667d635381834367b"
            },
            {
                "user": "5334d6650685f68c22aa460b",
                "date": "2014-04-21T05:00:00.000Z",
                "course": "5340ab6000806e2433864cfc",
                "score": 75,
                "_id": "534f100067d6353818343671"
            }
        ]
    },
    {
        "year": 2014,
        "month": "May",
        "rounds": [
            {
                "user": "5334d6650685f68c22aa460b",
                "date": "2014-05-05T05:00:00.000Z",
                "course": "5337611d8d03819024515cf9",
                "score": 81,
                "_id": "534dc38780f1a854236203f3"
            },
            {
                "user": "5334d6650685f68c22aa460b",
                "date": "2014-05-04T05:00:00.000Z",
                "course": "5337611d8d03819024515cf9",
                "score": 77,
                "_id": "534dc22c80f1a854236203e9"
            }
        ]
    }
]

I am guessing that I can use map-reduce or the aggregation pipeline in MongoDB, but I can't get my head around the syntax. Feeling sharp as a bowling ball right now.

Anyone have an idea to get me kick-started?

Was it helpful?

Solution

This can be done quite simply using the aggregation framework. Which is not just for "summing" values, but also excels at document re-shaping.

Which is your best option as though the "non-aggregating" code can look a little simpler using mapReduce, as that process relies on a JavaScript interpreter, as opposed to the native code of the aggregation framework, and a mapReduce will therefore run much slower. Considerably so over large data.

db.collection.aggregate([

    // Place all items in an array by year and month
    { "$group": {
        "_id": {
            "year": { "$year": "$date" },
            "month": { "$month": "$date" },
        },
        "rounds": { "$push": {
           "user": "$user",
           "date": "$date",
           "course": "$course",
           "score": "$score",
           "_id": "$_id"
        }}
    }},

    // Sort the results by year and month
    { "$sort": { "_id.year": 1, "_id.date": 1 } }, 

    // Optional project to your exact form
    { "$project": {
        "_id": 0,
        "year": "$_id.year",
        "month": "$_id.month",
        "rounds": 1
    }}

])

Or possibly without the arrays you specify and leave everything just in a flat form:

db.collection.aggregate([
    { "$project": {
        "year": { "$year": "$date" },
        "month": { "$month": "$date" },
        "user": 1,
        "date": 1,
        "course": 1,
        "score": 1
    }},
    { "$sort": { "year": 1, "month": 1 } }
])

Or even do it to exactly how you specify with the month names:

db.collection.aggregate([

    // Place all items in an array by year and month
    { "$group": {
        "_id": {
            "year": { "$year": "$date" },
            "month": { "$month": "$date" },
        },
        "rounds": { "$push": {
           "user": "$user",
           "date": "$date",
           "course": "$course",
           "score": "$score",
           "_id": "$_id"
        }}
    }},

    // Sort the results by year and month
    { "$sort": { "_id.year": 1, "_id.date": 1 } }, 

    // Optionally project to your exact form
    { "$project": {
      "_id": 0,
      "year": "$_id.year",
      "month": { "$cond": [
        { "$eq": [ "$_id.month": 1 ] },                 
        "January",
        { "$cond": [
          { "$eq": [ "$_id.month": 2 ] },
          "February",
          { "$cond": [
            { "$eq": [ "$_id.month": 3 ] },
            "March",
            { "$cond": [
              { "$eq": [ "$_id.month": 4 ] },
              "April",
              { "$cond": [
                { "$eq": [ "$_id.month": 5 ] },
                "May",
                { "$cond": [
                  { "$eq": [ "$_id.month": 6 ] },
                  "June",
                  { "$cond": [
                    { "$eq": [ "$_id.month": 7 ] },
                    "July",
                    { "$cond": [
                      { "$eq": [ "$_id.month": 8 ] },
                      "August",
                      { "$cond": [
                        { "$eq": [ "$_id.month": 9 ] },
                        "September",
                        { "$cond": [
                          { "$eq": [ "$_id.month": 10 ] },
                          "October",
                          { "$cond": [
                            { "$eq": [ "$_id.month": 11 ] },
                            "November",
                            "December"
                          ]}
                        ]}
                      ]} 
                    ]}
                  ]} 
                ]}
              ]}
            ]}
          ]}                 
        ]}
      ]},
      "rounds": 1
    }}

])

Which is overkill of course, but just to show that it can be done.

And there is of course the Operator reference so you can understand the usages of the operators used. And you may well want to alter this by adding your own $match condition to restrict the date range that you are looking at. Or indeed for other purposes.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top