Wuff
Wuff

Reputation: 267

Project specific fields of embedded documents

I am trying to get just one field of an specific embedded documents.

The embedded document should be filtered based on two condiditions and then just return one field of that embedded document.

db.usertest.insertMany(
    [{
    "username" : "userA",
    "objects" : [
        {
        "type" : "fruit",
        "origin" : "ABC",
        "name" : "banana",
        },
        {
        "type" : "fruit",
        "origin" : "XYZ",
        "name" : "pineapple",
        },

        {
        "type" : "vegetable",
        "origin" : "XYZ",
        "name" : "carrot"
        }
    ]
    },
     {
     "username" : "userB",
     "objects" : []
     }
]
)

I want to find the field "username" and the field "name" of the embedded document.

First I thought of $elemMatch, but I am stuck at the $project part and it just returns the field "name" of all embedded documents when $elemMatch is true.

db.usertest.aggregate(
    [{$match:{"objects": {"$elemMatch":{
         "type": "fruit",
         "origin": "XYZ"}}}},
     {$project: {
      "username":1,
      "objects.name":1
      }}]
).pretty()

Then I thought of using $filter, but it obviously just returns the whole embedded document instead of just the field "name".

db.usertest.aggregate(
    [{$project: {
      "username":1,
      "name":{
          $filter: {
             input: "$objects",
             cond: { $and: [
                     { $eq: [ "$$this.type", "fruit" ] },
                  { $eq: [ "$$this.origin", "XYZ" ] }
                 ]}
          }}
      }}]
).pretty()

Any ideas?

Upvotes: 1

Views: 1124

Answers (2)

s7vr
s7vr

Reputation: 75994

There is no $elemMatch operator in $project stage. So to use mimic similar functionality you can create $filter with $arrayElemAt with index 0 and $let to hold the intermediate document in a variable and output the name.

Keep the $match stage so you only look at documents where there is at least one matching object.

Something like

db.usertest.aggregate(
[{"$match":{"objects":{"$elemMatch":{"type":"fruit","origin":"XYZ"}}}},
{"$project":{
  "name":{"$let":{
    "vars":{"object":{
      "$arrayElemAt":[
        {"$filter":{
          "input":"$objects",
          "as":"o",
          "cond":{
            "$and":[
              {"$eq":["$$o.type","fruit"]},
              {"$eq":["$$o.origin","XYZ"]}
            ]
          }
        }},
        0
      ]
    }},
    "in":"$$object.name"
  }}
}}])

Upvotes: 1

mickl
mickl

Reputation: 49995

You can combine multiple aggregation operators in following order:

$filter -> $map (to get only name property) -> $arrayElemAt (to take first element)

db.usertest.aggregate(
  [{$project: {
    "username":1,
    "name": {
      $arrayElemAt: [
        {
          $map: {
            input: {
              $filter: {
                 input: "$objects",
                 cond: { $and: [
                         { $eq: [ "$$this.type", "fruit" ] },
                      { $eq: [ "$$this.origin", "XYZ" ] }
                     ]}
              }},
              as: "item",
              in: "$$item.name"
          }
        },0
      ]
    }
    }}]
).pretty()

Upvotes: 1

Related Questions