Search code examples
mongodbmongodb-queryaggregation-frameworkaggregate

MongoDB Aggregate: count occurrences in array of object and transform to object


I have a collection called notifications that has 2 fields (for simplicity):

  • targets is an array of strings

  • userReads is an array of object {userId, readAt}

Example :

{
   targets: ['group1', 'group2'],
   userReads: [
     {userId: 1, readAt: 'date1'},
     {userId: 2, readAt: 'date2'},
     {userId: 3, readAt: 'date3'},
   ]
}

{
   targets: ['group1'],
   userReads: [
     {userId: 1, readAt: 'date4'}
   ]
}

DESIRED OUTPUT:

{
   groupsNotificationsCount: {
     group1: 2,
     group2: 1
   },
   usersNotificationsCount: {
     1: 2,
     2: 1,
     3: 1
   }
}

At first I tried this aggregate:

[{
 $match: {
  targets: {
   $in: ['group/1','group/2']
  },
 }
}, {
 $project: {
  targets: 1,
  userReads: 1
 }
}, {
 $unwind: {
  path: '$targets'
 }
}, {
 $match: {
  targets: {
   $in: ['group/1','group/2']
  }
 }
}, {
 $group: {
  _id: '$targets',
  countForGroup: {
   $count: {}
  },
  userReads: {
   $push: '$userReads'
  }
 }
}, {
 $addFields: {
  userReads: {
   $reduce: {
    input: '$userReads',
    initialValue: [],
    'in': {
     $concatArrays: [
      '$$value',
      '$$this'
     ]
    }
   }
  }
 }
}]

I have the right counts for each group in my documents like so:

{
   _id: 'group1',
   countForGroup: 2,
   userReads: [
     {userId: 1, readAt: 'date1'},
     {userId: 2, readAt: 'date2'},
     {userId: 3, readAt: 'date3'},
     {userId: 1, readAt: 'date4'},
   ]
}

But now I have no idea how to go on from there to get my desired output


Solution

  • This is a perfect use case for using $facet. You can process groupsNotificationsCount and usersNotificationsCount separately by $unwind and $group the count. Afterwards by wrangling into array of k-v tuples, you can construct your expected form by using $arrayToObject

    db.collection.aggregate([
      {
        "$facet": {
          "groupsNotificationsCount": [
            {
              "$unwind": "$targets"
            },
            {
              $group: {
                _id: "$targets",
                count: {
                  $sum: 1
                }
              }
            }
          ],
          "usersNotificationsCount": [
            {
              "$unwind": "$userReads"
            },
            {
              $group: {
                _id: "$userReads.userId",
                count: {
                  $sum: 1
                }
              }
            }
          ]
        }
      },
      {
        "$project": {
          groupsNotificationsCount: {
            "$arrayToObject": {
              "$map": {
                "input": "$groupsNotificationsCount",
                "as": "g",
                "in": {
                  k: "$$g._id",
                  v: "$$g.count"
                }
              }
            }
          },
          usersNotificationsCount: {
            "$arrayToObject": {
              "$map": {
                "input": "$usersNotificationsCount",
                "as": "u",
                "in": {
                  k: {
                    $toString: "$$u._id"
                  },
                  v: "$$u.count"
                }
              }
            }
          }
        }
      }
    ])
    

    Here is the Mongo playground for your reference.