mongodbaggregation-framework

How to filter a nested array at the projection stage?


I'm struggling to filter a nested array at the projection stage.

The actual match condition is more complicated, but this will do:

db.collection.insertMany([
  { "id": 0,
    "list_L1": [
      { "obj_L1": {
          "list_L2": [
            {id: "000", go: "Y"},
            {id: "001", go: "N"},
            {id: "002", go: "N"} ]}}]},
  { "id": 1,
    "list_L1": [
      { "obj_L1": {
          "list_L2": [
            {id: "100", go: "Y"},
            {id: "101", go: "Y"},
            {id: "102", go: "N"} ]}}]},
  {
    "id": 2,
    "list_L1": [
      { "obj_L1": {
          "list_L2": [
            {id: "200",go: "N"},
            {id: "201",go: "N"},
            {id: "202",go: "N"} ]}}]}
]);

db.collection.aggregate([
  {$match: { "list_L1.obj_L1.list_L2.go": "Y"}}
]);

(Apologies if the format is weird - I'm trying to save vertical space)

This filters documents alright, but I've been asked to also keep only the matching elements of list_L2, producing something like this:

[
  { "_id": ObjectId("5a934e000102030405000000"),
    "id": 0,
    "list_L1": [
      { "obj_L1": {
          "list_L2": [
            {id: "000", go: "Y"} ]}}},
  { "_id": ObjectId("5a934e000102030405000001"),
    "id": 1,
    "list_L1": [
      { "obj_L1": {
          "list_L2": [
            {id: "100", go: "Y"},
            {id: "101", go: "Y"}} ]}}}
]

If the document only had list_L1 it wouldn't be hard, but I can't "see" the values two levels deep; this returns empty arrays:

db.collection.aggregate([
  {$match: { "list_L1.obj_L1.list_L2.go": "Y"}},
  {$addFields: { "list_L1.obj_L1.list_L2": {
    $filter: {
      input: "$list_L1.obj_L1.list_L2",
      cond: { $eq: ["$$this.go","Y"] }}}}}
]);

[
  {
    "_id": ObjectId("5a934e000102030405000000"),
    "list_L1": [
      { "obj_L1": {
          "list_L2": [] }}]},
  {
    "_id": ObjectId("5a934e000102030405000001"),
    "list_L1": [
      {
        "obj_L1": {
          "list_L2": [] }}]}
]

So how do I filter the inner array after matching what I want? Alternative, Icould remove the non-matching elements of all documents and then filter for non-empty, but I'm too new to this.


Solution

  • Refactor your schema if possible. Store the innermost array elements as individual documents will help you gain query simplicity and potential performance boost through indexing.

    Nevertheless, for your current scenario, you can use $map to iterate through list_L1 array. Chain up $mergeObjects and $filter to return only matched elements in list_L2

    db.collection.aggregate([
      {
        "$match": {
          "list_L1.obj_L1.list_L2.go": "Y"
        }
      },
      {
        "$set": {
          "list_L1": {
            "$map": {
              "input": "$list_L1",
              "as": "ll1",
              "in": {
                "$mergeObjects": [
                  "$$ll1",
                  {
                    "obj_L1": {
                      "list_L2": {
                        "$filter": {
                          "input": "$$ll1.obj_L1.list_L2",
                          "as": "ll2",
                          "cond": {
                            "$eq": [
                              "Y",
                              "$$ll2.go"
                            ]
                          }
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      }
    ])
    

    Mongo Playground