Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flatten MongoDB nested array in aggregation

I have documents like this:

{
    "many" : {},
    "other" : {},
    "fields" : {},
    "phases" : [ 
        {
            "type" : 10,
            "states" : [ 
                {
                    "type" : 10,
                    "time" : ISODate("2018-04-25T13:06:42.990+02:00")
                }, 
                {
                    "type" : 20,
                    "time" : ISODate("2018-04-25T13:26:12.122+02:00")
                }, 
                {
                    "type" : 30,
                    "time" : ISODate("2018-04-25T13:26:30.124+02:00")
                }
            ]
        }, 
        {
            "type" : 20,
            "states" : [ 
                {
                    "type" : 10,
                    "time" : ISODate("2018-04-25T13:27:58.201+02:00")
                }
            ]
        }
    ]
}

Within an aggregation, I'm trying to flatten the states including the parent's type like this (desired output):

"states" : [ 
    {
        "phase": 10,
        "type" : 10,
        "time" : ISODate("2018-04-25T13:06:42.990+02:00")
    }, 
    {
        "phase": 10,
        "type" : 20,
        "time" : ISODate("2018-04-25T13:26:12.122+02:00")
    }, 
    {
        "phase": 10,
        "type" : 30,
        "time" : ISODate("2018-04-25T13:26:30.124+02:00")
    }, 
    {
        "phase": 20,
        "type" : 10,
        "time" : ISODate("2018-04-25T13:27:58.201+02:00")
    }
]

I already accomplished the additional states field without the phase field with this aggregation:

db.docs.aggregate([
{
    $addFields: {
        states: {
            $reduce: {
                  input: "$phases",
                  initialValue: [],
                  in: { $concatArrays: ["$$value", "$$this.states"] }
               }
           }
    }
}
])

The 'many other fields' should be preserved, so I believe grouping is not an option.
MongoDB version is 3.4.

I tried many things without result. I wonder if and how this is possible.

like image 429
Clouren Avatar asked Sep 04 '18 11:09

Clouren


4 Answers

db.state.aggregate(

// Pipeline
[
    // Stage 1
    {
        $unwind: {
            path : "$phases",
            includeArrayIndex : "arrayIndex", // optional
            preserveNullAndEmptyArrays : false // optional
        }
    },

    // Stage 2
    {
        $unwind: {
            path : "$phases.states",
            includeArrayIndex : "arrayIndex", // optional
            preserveNullAndEmptyArrays : false // optional
        }
    },

    // Stage 3
    {
        $project: {
            "phases":"$phases.type",
            "type":"$phases.states.type",
            "time":"$phases.states.time",
        }
    },

    // Stage 4
    {
        $group: {
            "_id":"$_id",
            states: { $push:  { phases: "$phases", type: "$type",time: "$time" } }
        }
    },
]);
like image 62
Mohit Kumar Bordia Avatar answered Oct 09 '22 00:10

Mohit Kumar Bordia


You can use below aggregation. Use $map to format the states array to include phase field.

db.docs.aggregate([
  {"$addFields":{
    "states":{
      "$reduce":{
        "input":"$phases",
        "initialValue":[],
        "in":{
          "$concatArrays":[
            "$$value",
            {"$map":{
              "input":"$$this.states",
              "as":"state",
              "in":{"phase":"$$this.type","type":"$$state.type","time":"$$state.time"}
            }}
          ]
        }
      }
    }
  }}
])
like image 30
s7vr Avatar answered Oct 08 '22 23:10

s7vr


The code of Mohit Kumar Bordia looks great. In addition, if you want to also return the old object (based in Mohit code):

db.getCollection('flatten').aggregate(

// Pipeline
[
    // Stage 1
    {
        $unwind: {
            path : "$phases",
            includeArrayIndex : "arrayIndex", // optional
            preserveNullAndEmptyArrays : false // optional
        }
    },

    // Stage 2
    {
        $unwind: {
            path : "$phases.states",
            includeArrayIndex : "arrayIndex", // optional
            preserveNullAndEmptyArrays : false // optional
        }
    },

    // Stage 3
    {
        $project: {
            "_id" : "$_id",
            "many" : "$many",
            "other" :"$other",
            "fields" : "$fields",
            "phases":"$phases.type",
            "type":"$phases.states.type",
            "time":"$phases.states.time",
        }
    },

    // Stage 4
    {
        $group: {
            "_id":"$_id",
            many : { $first: "$many"},
            other: { "$first": "$other" },
            fields: { "$first": "$fields" },
            states: { $push:  { phases: "$phases", type: "$type",time: "$time" } }
        }
    },
]);
like image 37
Juan Bermudez Avatar answered Oct 09 '22 00:10

Juan Bermudez


You need to loop over each array using $map and then you can use $reduce to $concat the final arrays

db.collection.aggregate([
  { "$addFields": {
    "states": { "$reduce": {
      "input": { "$map": {
        "input": "$phases",
        "as": "pa",
        "in": { "$map": {
          "input": "$$pa.states",
          "as": "st",
          "in": { "type": "$$st.type", "time": "$$st.time", "phase": "$$pa.type" }
        }}
      }},
      "initialValue": [],
      "in": { "$concatArrays": ["$$value", "$$this"] }
    }}
  }}
])
like image 30
Ashh Avatar answered Oct 08 '22 23:10

Ashh