2

I have collection in mongodb (3.0):

{
    _id: 1, 
    m: [{_id:11, _t: 'type1'},
        {_id:12, _t: 'type2'}, 
        {_id:13, _t: 'type3'}]
},    
{
    _id: 2, 
    m: [{_id:21, _t: 'type1'},
        {_id:22, _t: 'type21'}, 
        {_id:23, _t: 'type3'}]
}

I want to find documents with m attributes where m._t containing ['type1', 'type2'].

Like this:

{
    _id: 1, 
    m: [{_id:11, _t: 'type1'},
        {_id:12, _t: 'type2'}]
},    
{
    _id: 2, 
    m: [{_id:21, _t: 'type1'}]
}

I tried to use $ and $elemMatch, but couldn't get required result. How to do it, using find()? Help me, please! Thanks!

2 Answers 2

1

Because the $elemMatch operator limits the contents of the m array field from the query results to contain only the first element matching the $elemMatch condition, the following will only return the an array with the first matching elements

{
     "_id" : 11,
    "_t" : "type1"
}

and

{
    "_id" : 21,
    "_t" : "type1"
}

Query using $elemMatch projection:

db.collection.find(
    { 
        "m._t": {
            "$in": ["type1", "type2"]
        }
    },
    {
        "m": {
            "$elemMatch": {
                "_t": {
                    "$in": ["type1", "type2"]
                }
            }
        }
    }
)

Result:

/* 0 */
{
    "_id" : 1,
    "m" : [ 
        {
            "_id" : 11,
            "_t" : "type1"
        }
    ]
}

/* 1 */
{
    "_id" : 2,
    "m" : [ 
        {
            "_id" : 21,
            "_t" : "type1"
        }
    ]
}

One approach you can take is the aggregation framework, where your pipeline would consist of a $match operator, similar to the find query above to filter the initial stream of documents. The next pipeline step would be the crucial $unwind operator that "splits" the array elements to be further streamlined with another $match operator and then the final $group pipeline to restore the original data structure by using the accumulator operator $push.

The following illustrates this path:

db.collection.aggregate([
    {
        "$match": {
            "m._t": {
                "$in": ["type1", "type2"]
            }
        }
    },
    {
        "$unwind": "$m"
    },
    {
        "$match": {
            "m._t": {
                "$in": ["type1", "type2"]
            }
        }
    },
    {
        "$group": {
            "_id": "$_id",
            "m": {
                "$push": "$m"
            }
        }
    }
])

Sample Output:

/* 0 */
{
    "result" : [ 
        {
            "_id" : 2,
            "m" : [ 
                {
                    "_id" : 21,
                    "_t" : "type1"
                }
            ]
        }, 
        {
            "_id" : 1,
            "m" : [ 
                {
                    "_id" : 11,
                    "_t" : "type1"
                }, 
                {
                    "_id" : 12,
                    "_t" : "type2"
                }
            ]
        }
    ],
    "ok" : 1
}
Sign up to request clarification or add additional context in comments.

Comments

1

To get your "filtered" result, the $redact with the aggregation pipeline is the fastest way:

db.junk.aggregate([
  { "$match": { "m._t": { "$in": ["type1", "type2"] } } },
  { "$redact": {
    "$cond": {
      "if": { 
        "$or": [
          { "$eq": [ { "$ifNull": ["$_t", "type1"] }, "type1" ] },
          { "$eq": [ { "$ifNull": ["$_t", "type2"] }, "type2" ] }
        ],
      },
      "then": "$$DESCEND",
      "else": "$$PRUNE"
    }
  }}
])

The $redact operator sets up a logical filter for the document that can also traverse into the array levels. Note that this is matching on _t at all levels of the document, so make sure there are no other elements sharing this name.

The query uses $in for selection just as the logical filter uses $or. Anything that does not match, gets "pruned".

{
    "_id" : 1,
    "m" : [
            {
                    "_id" : 11,
                    "_t" : "type1"
            },
            {
                    "_id" : 12,
                    "_t" : "type2"
            }
    ]
}
{ 
    "_id" : 2, 
    "m" : [ { "_id" : 21, "_t" : "type1" } ]
}

Short and sweet and simple.

A bit more cumbersome, but a reasonably safer is to use this construct with $map and $setDifference to filter results:

db.junk.aggregate([
  { "$match": { "m._t": { "$in": ["type1", "type2"] } } },
  { "$project": {
    "m": {
      "$setDifference": [
        { "$map": {
          "input": "$m",
          "as": "el",
          "in": {
            "$cond": {
              "if": {
                "$or": [
                  { "$eq": [ "$$el._t", "type1" ] },
                  { "$eq": [ "$$el._t", "type2" ] }
                ]
              },
              "then": "$$el",
              "else": false
            }
          }
        }},
        [false]
      ]
    }
  }}      
])

The $map evaluates the conditions against each element and the $setDifference removes any of those condtions that returned false rather than the array content. Very similar to the $cond in redact above, but it is just working specifically with the one array and not the whole document.

In future MongoDB releases ( currently available in development releases ) there will be the $filter operator, which is very simple to follow:

db.junk.aggregate([
  { "$match": { "m._t": { "$in": ["type1", "type2"] } } },
  { "$project": {
    "m": {
      "$filter": {
        "input": "$m",
        "as": "el",
        "cond": {
          "$or": [
            { "$eq": [ "$$el._t", "type1" ] },
            { "$eq": [ "$$el._t", "type2" ] }
          ]
        }
      }
    }
  }}
])

And that will simply remove any array element that does not match the specified conditions.

If you want to filter array content on the server, the aggregation framework is the way to do it.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.