mongodbaggregation-framework

Filter subdocuments by multiple criteria


I have a collection "fruits" like this:

db={
  "fruits": [
    {
      "name": "Banana",
      "variants": [
        {
          "color": "green",
          "props": [
            {
              "uid": 1,
              "grade": 3
            },
            {
              "uid": 3,
              "grade": 2
            }
          ]
        },
        {
          "color": "yellow",
          "props": [
            {
              "uid": 2,
              "grade": 1
            },
            {
              "uid": 1,
              "grade": 2
            }
          ]
        },
        {
          "color": "brown",
          "props": [
            {
              "uid": 1,
              "grade": 3
            },
            {
              "uid": 4,
              "grade": 2
            }
          ]
        }
      ]
    }
  ]
}

available_prop_uids = [1,4,5]

Now I want to filter the documents in an aggregation by multiple criteria in case the array variants.props is not empty:

a.) Filter array variants by max props.grade: variants should contain only subdocuments that contain at least one prop with the max prop value from all variants per document. In the example, the variants with color green and brown have a prop with grade 3, which is the highest grade among all variants for the document Banana. Therefore, the variants array is reduced to two subdocuments, with color green and brown.

b.) Filter array variants by available_prop_uids: Furthermore, variants should contain only subdocuments where the set of prop.uids is a subset of available_prop_uids. Variant green is now removed from the array since it contains a prop with uid 3 which is not in available_prop_uids.

Desired outcome:

    [{
      "name": "Banana",
      "variants": [
        {
          "color": "brown",
          "props": [
            {
              "uid": 1,
              "grade": 3
            },
            {
              "uid": 4,
              "grade": 2
            }
          ]
        }
      ]
    }]

Thanks for your help!


Solution

  • here are the steps i took.

    1. Find the maximum props.grade value and save it in a temporary field.
    2. Filter such that at least 1 in the props array has grade value equal to max
    3. Filter such that an array of props.grade values is a subset of the input list
    4. Remove the temp field created on step 1
    db.collection.aggregate([
      {
        $addFields: {
          max: {
            $max: {
              $map: {
                input: "$variants",
                as: "variant",
                in: {
                  $reduce: {
                    input: "$$variant.props",
                    initialValue: 0,
                    in: {
                      $cond: [ { $gt: [ "$$this.grade", "$$value" ] }, "$$this.grade", "$$value" ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      {
        $addFields: {
          variants: {
            $filter: {
              input: "$variants",
              as: "variant",
              cond: {
                $or: [
                  { $eq: [ { $size: "$$variant.props" }, 0 ] },
                  {
                    $anyElementTrue: {
                      $map: {
                        input: "$$variant.props",
                        as: "prop",
                        in: { $eq: [ "$$prop.grade", "$max" ] }
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      },
      {
        $addFields: {
          variants: {
            $filter: {
              input: "$variants",
              as: "variant",
              cond: {
                $or: [
                  { $eq: [ { $size: "$$variant.props" }, 0 ] },
                  {
                    $setIsSubset: [
                      {
                        $map: {
                          input: "$$variant.props",
                          as: "prop",
                          in: "$$prop.uid"
                        }
                      },
                      [ 1, 4, 5 ]
                    ]
                  }
                ]
              }
            }
          }
        }
      },
      { $unset: "max" }
    ])
    

    playground

    Steps 2 and 3 can be combined to 1 if needed. will remove an extra looping step https://mongoplayground.net/p/LdmTI45VYvJ