Is there an easy way to pass a document property as argument list to operators like $setEquals?

Viewed 52

The $setEquals aggregation pipeline operator has the following interface:

{ $setEquals: [ <expression1>, <expression2>, ... ] }

Now after a few $group stages I have a document like this:

{ myLists: [[...], [...], [...]] }

And would like to check if all those sub-arrays contain the same values, so pass it to $setEquals:

{ $setEquals: '$myLists' }

but mongo complains: $setEquals needs at least two arguments had: 1.

If I know the number of elements, I can just pass them explicitly:

{ $setEquals: [{ $arrayElemAt: ['$myLists', 0] }, { $arrayElemAt: ['$myLists', 1] }] }

If I don't know the number, I can use $reduce to do it sort-of element-wise, but this requires the empty set as an indicator for "false":

{
  $reduce: {
    input: '$myList',
    initialValue: { $arrayElemAt: ['$myList', 0] },
    in: {
      $cond: {
        if: { $setEquals: ['$$value', '$$this'] },
        then: '$$value',
        else: [],
      },
    },
  },
}
    

Is there a better way?

2 Answers

Basically, $setEquals operator will accept physical input of set, we can not pass an array of sets by reference field, If you really need this you can try a custom logic,

  • $map to iterate loop of myLists array
  • $filter to iterate loop of myLists array and check current set is in filters,
  • $size to get total filtered sets in the above filter operation
  • $gt to check size return by the above operation is greater than 1 then return true otherwise false
  • $allElementsTrue to check above return array of booleans are true then return true otherwise false
db.collection.aggregate([
  {
    $project: {
      myLists: {
        $allElementsTrue: {
          $map: {
            input: "$myLists",
            in: {
              $gt: [
                {
                  $size: {
                    $filter: {
                      input: "$myLists",
                      as: "m",
                      cond: { $setEquals: ["$$m", "$$this"] }
                    }
                  }
                },
                1
              ]
            }
          }
        }
      }
    }
  }
])

Playground

As you've noticed the syntax does not allow using a "stored array" as the value for these operators, (can read more about it here).

So there is no "better" way than your workaround, but you do need to consider the "edge" cases:

  1. myLists is empty.
  2. myLists has 1 list in it.

like so:

db.collection.aggregate([
  {
    $project: {
      isUnique: {
        $cond: [
          {
            $in: [
              {
                $size: "$myList"
              },
              [
                0,
                1
              ]
            ]
          },
          true,
          {
            $ne: [
              {
                $reduce: {
                  input: "$myList",
                  initialValue: {
                    $arrayElemAt: [
                      "$myList",
                      0
                    ]
                  },
                  in: {
                    $cond: {
                      if: {
                        $setEquals: [
                          "$$value",
                          "$$this"
                        ]
                      },
                      then: "$$value",
                      else: []
                    }
                  }
                }
              },
              []
            ]
          }
        ]
      }
    }
  }
])

Mongo Playground

Related