Search code examples
mongodbmongodb-queryaggregation-framework

Use array values as key in $match stage mongodb


Given a list of documents like:

[
  {
    _id: 1,
    field1: "test",
    field2: "value2",
    fields: [
      "field1",
      "field2"
    ]
  },
  {
    _id: 2,
    field1: "value1",
    field2: "test",
    fields: [
      "field1",
      "field2"
    ]
  },
  {
    _id: 3,
    field1: "test",
    field2: "value2",
    fields: [
      "field2"
    ]
  },
  {
    _id: 4,
    field1: "value1",
    field2: "value2",
    fields: [
      "field1"
    ]
  },
  {
    _id: 5,
    field1: "test",
    field2: "test",
    fields: []
  }
]

I want to retrieve all documents that have any of the specified fields in the array, with a value “test”. For instance, in document with _id: 1, field2 is a value of fields array, and field2 has test as value. In contrast, document with _id: 5 has keys field1 and field2 with test value, but none of this fields is present in fields array. So, given the previous list of documents, only documents with _id: 1 and _id: 2 should be returned.


Solution

  • a possible approach as steps

    1. Create a temporary key-value pair array field from the field1,field2.. so that it can be filtered/searched
    2. Do the comparison using a match.
    3. Remove the temporary field added in the first stage.

    An approach with $anyElementTrue in the match stage - demo

    db.collection.aggregate([
      {
        $addFields: {
          keyValuePairArray: {
            $filter: {
              input: { $objectToArray: "$$ROOT" },
              cond: { $not: { $in: [ "$$this.k", ["_id", "fields"] ] } }
            }
          }
        }
      },
      {
        $match: {
          $expr: {
            $anyElementTrue: {
              $map: {
                input: "$keyValuePairArray",
                in: {
                  $and: [
                    { $in: [ "$$this.k", "$fields" ] },
                    { $eq: [ "$$this.v", "test" ] }
                  ]
                }
              }
            }
          }
        }
      },
      { $unset: "keyValuePairArray" }
    ])
    

    Another approach with $filter in the match stage by checking its size - demo

    db.collection.aggregate([
      {
        $addFields: {
          keyValuePairArray: {
            $filter: {
              input: { $objectToArray: "$$ROOT" },
              cond: { $not: { $in: [ "$$this.k", ["_id", "fields"] ] } }
            }
          }
        }
      },
      {
        $match: {
          $expr: {
            $gt: [
              {
                $size: {
                  $filter: {
                    input: "$keyValuePairArray",
                    cond: {
                      $and: [
                        { $in: [ "$$this.k", "$fields" ] },
                        { $eq: [ "$$this.v", "test" ] }
                      ]
                    }
                  }
                }
              },
              0
            ]
          }
        }
      },
      { $unset: "keyValuePairArray" }
    ])
    

    EDIT - you might not even need the temporary field by removing the _id and fields since they will not affect in the $match comparison anyway - check demo

    db.collection.aggregate([
      {
        $match: {
          $expr: {
            $anyElementTrue: {
              $map: {
                input: { $objectToArray: "$$ROOT" },
                in: {
                  $and: [
                    { $in: [ "$$this.k", "$fields" ] },
                    { $eq: [ "$$this.v", "test" ] }
                  ]
                }
              }
            }
          }
        }
      }
    ])