Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I add properties to subdocument array in aggregation/make $map like $addFields?

> db.things.find()
{ "_id" : 1, "a" : [ ] }
{ "_id" : 2, "a" : [ { "x" : 2, "y" : 1 }, { "x" : 0, "y" : 1 } ] }
{ "_id" : 3, "a" : [ { "x" : 2, "y" : 5, "abitraryPropertyWhichShouldBeKept" : true } ] }

I could like to add a calculated property, say magnitude, to each subdocument at a. I don’t want to have to specify x and y manually, and there might be other properties I need to keep. My attempt so far (where I have to manually specify x and y which I shouldn’t have to do):

db.things.aggregate([
  {
    $addFields: {
     a: {
       $map: {
         input: '$a',
         as: 'item',
         'in': {
           x: '$$item.x',
           y: '$$item.y',
           magnitude: {
             $sqrt: {
               $add: [
                 { $pow: ['$$item.x', 2], },
                 { $pow: ['$$item.y', 2] }],
               },
             },
           },
         },
       },
     },
  },
])

results which drop the extra key I wanted to keep:

{ "_id" : 1, "a" : [ ] }
{ "_id" : 2, "a" : [ {
  "x" : 2, "y" : 1, "magnitude" : 2.23606797749979 }, {
  "x" : 0, "y" : 1, "magnitude" : 1 } ] }
{ "_id" : 3, "a" : [ { "x" : 2, "y" : 5, "magnitude" : 5.385164807134504 } ] }

The only other way I could think of is chaining $unwind, $addFields, and $group. But to prevent $unwind from removing document 1, I’d have to add a dummy entry to the end of each a array and then discard it when grouping them together again.

Is there something like $map but that behaves like $addFields so that I can just add a property to the nested document? Do I really have to specify all possible keys of the subdocument just to use $map? If I do not know the keys in the nested subdocuments ahead of time, is my only choice really the $unwind with all the extra pomp and circumstance necessary to use it?

Current Workaround

Here’s my $unwind$group try. But, as you can see, I need a ton of stages that wouldn’t be necessary if I could just get $addFields-like behavior inside of $map or some expression operator which would merge objects together like JavaScript’s Object.assign():

db.things.aggregate([
  { // Add dummy to preserve documents during imminent $unwind
    $addFields: {
      a: {
        $concatArrays: [ '$a', [ null, ], ],
      },
    },
  },
  { $unwind: '$a', },
  { // Do actual calculation on single array element
    $addFields: {
      'a.magnitude': {
        $sqrt: {
          $add: [
            { $pow: ['$a.x', 2] },
            { $pow: ['$a.y', 2] },
          ],
        },
      },
    },
  },
  {
    $group: {
      _id: '$_id',
      a: { $push: '$a', },
      item: { $first: '$$ROOT', },
    },
  },
  { // Remove dummy element
    $addFields: {
      'item.a': {
        $slice: [ '$a', { $add: [ { $size: '$a', }, -1] } ],
      },
    },
  },
  { $replaceRoot: { newRoot: '$item', }, },
])

results which properly preserve any unanticipated keys in the original data:

{ "_id" : 3, "a" : [ {
  "x" : 2, "y" : 5, "abitraryPropertyWhichShouldBeKept" : true,
  "magnitude" : 5.385164807134504 } ] }
{ "_id" : 2, "a" : [ {
  "x" : 2, "y" : 1, "magnitude" : 2.23606797749979 }, {
  "x" : 0, "y" : 1, "magnitude" : 1 } ] }
{ "_id" : 1, "a" : [ ] }
like image 522
binki Avatar asked Sep 06 '25 00:09

binki


1 Answers

You can use mergeObjects aggregation operator is available in the 3.5.6 development release which will be rolled into upcoming 3.6 release.

db.things.aggregate([
  {
    "$addFields": {
      "a": {
        "$map": {
          "input": "$a",
          "as": "item",
          "in": {
            "$mergeObjects": [
              "$$item",
              {
                "magnitude": {
                  "$sqrt": {
                    "$add": [
                      {
                        "$pow": [
                          "$$item.x",
                          2
                        ]
                      },
                      {
                        "$pow": [
                          "$$item.y",
                          2
                        ]
                      }
                    ]
                  }
                }
              }
            ]
          }
        }
      }
    }
  }
])

Using 3.4.4 & higher production release.

Use $arrayToObject and $objectToArray in a $map to keep the existing key value and $concatArrays to merge the computed key value array.

db.things.aggregate([
  {
    "$addFields": {
      "a": {
        "$map": {
          "input": "$a",
          "as": "item",
          "in": {
            "$arrayToObject": {
              "$concatArrays": [
                {
                  "$objectToArray": "$$item"
                },
                [
                  {
                    "k": "magnitude",
                    "v": {
                      "$sqrt": {
                        "$add": [
                          {
                            "$pow": [
                              "$$item.x",
                              2
                            ]
                          },
                          {
                            "$pow": [
                              "$$item.y",
                              2
                            ]
                          }
                        ]
                      }
                    }
                  }
                ]
              ]
            }
          }
        }
      }
    }
  }
])
like image 101
s7vr Avatar answered Sep 10 '25 11:09

s7vr