34

This is my object:

{ "_id" : ObjectId("53fdcb6796cb9b9aa86f05b9"), "list" : [ "a", "b" ], "complist" : [ { "a" : "a", "b" : "b" }, { "a" : "c", "b" : "d" } ] }

And this is what I want to accomplish: check if "list" contains a certain element and get only the field "a" from the objects on "complist" while reading the document regardless of any of these values. I'm building a forum system, this is the query that will return the details of a forum. I need to read the forum information while knowing if the user is in the forum's white list.

With a find I can use the query

db.itens.find({},{list:{$elemMatch:{$in:["a"]}}})

to get only the first element that matches a certain value. This way I can just check if the returned array is not empty and I know if "list" contains the value I'm looking for. I can't do it on the query because I want the document regardless of it containing the value I'm looking for in the "list" value. I need the document AND know if "list" has a certain value.

With an aggregate I can use the query

db.itens.aggregate({$project:{"complist.a":1}})

to read only the field "a" of the objects contained in complist. This is going to get the forum's threads basic information, I don't want all the information of the threads, just a couple of things.

But when I try to use the query

db.itens.aggregate({$project:{"complist.b":1,list:{$elemMatch:{$in:["a"]}}}})

to try and do both, it throws me an error saying the operator $elemMatch is not valid.

Am I doing something wrong here with the $elemMatch in aggregate? Is there a better way to accomplish this?

Stephen Lynx
  • 1,077
  • 2
  • 14
  • 29

8 Answers8

30

Quite on old question but literally none of the proposed answers are good.


TLDR:

You can't use $elemMatch in a $project stage. but you can achieve the same result using other aggregation operators like $filter.

db.itens.aggregate([
    {
        $project: {
            compList: {
               $filter: {
                input: "$complist",
                as: "item",
                cond: {$eq: ["$$item.a", 1]}
               }
            }
        }
    }
])

And if you want just the first item from the array that matches the condition similarly to what $elemMatch does you can incorporate $arrayElemAt


In Depth Explanation:

First let's understand $elemMatch:

$elemMatch is a query expressions while also this projection version of it exists this refers to a query projection and not $project aggregation stage.

So what? what does this have to do with anything? well a $project stage has certain input structure it can have while the one we want to use is:

<field>: <expression>

What is a valid expression?

Expressions can include field paths, literals, system variables, expression objects, and expression operators. Expressions can be nested.

So we want to use an expression operator, but as you can see from the doc's $elemMatch is not part of it. hence it's not a valid expression to be used in an aggregation $project stage.

Tom Slabbaert
  • 21,288
  • 10
  • 30
  • 43
17

For some reason $elemMatch doesn't work in aggregations. You need to use the new $filter operator in Mongo 3.2. See https://docs.mongodb.org/manual/reference/operator/aggregation/filter/

Graeme P
  • 2,202
  • 2
  • 25
  • 27
15

The answer to this question maybe help.

db.collection_name.aggregate({
    "$match": {
        "complist": {
            "$elemMatch": {
                "a": "a"
            }
        }
    }
});
Hevelson Rosario
  • 420
  • 6
  • 11
  • 1
    are you using mongoosejs to access mongo db? – Hevelson Rosario Jul 16 '20 at 14:02
  • This works, it wasn't working the first time because if there is a path like "complist.sub.path" should drop the "complist" so that it's there is only "sub.path" below. – Shadoweb Sep 11 '21 at 14:38
  • I didn't find a way to get the index of the array in this method, so if you want to do extra manipulations later in the Aggregate, then you better go for the full $unwind way, or use $project like the accepted answer. – Shadoweb Sep 11 '21 at 15:07
12

Actually, the simplest solution is to just $unwind your array, then $match the appropriate documents. You can wind-up the appropriate documents again using $group and $push.

Graeme P
  • 2,202
  • 2
  • 25
  • 27
2

Although the question is old, here is my contribution for November 2017.

I had similar problem and doing two consecutive match operations worked for me. The code below is a subset of my whole code and I changed elements names, so it's not tested. Anyway this should point you in the right direction.

db.collection.aggregate([
    {
        "$match": {
            "_id": "ID1"
        }
    },
    {
        "$unwind": "$sub_collection"
    },
    {
        "$match": {
            "sub_collection.field_I_want_to_match": "value"
        }
    }
])
Paulo Pedroso
  • 3,555
  • 2
  • 29
  • 34
0

For aggregations simply use $expr:

db.items.aggregate([
    {
        "$match": {
           "$expr": {"$in": ["a", "$list"]}
        }
    },
])
Fusion
  • 5,046
  • 5
  • 42
  • 51
0

You can simply unwind the document and then group it again by id

{ $unwind: "$yourarray" },
{
  $match: {
    "field": { $gt: 1 }
  },
},
{
  $group: {
    _id: "$_id"
  }
},
Sehrish Waheed
  • 1,230
  • 14
  • 17
-19

Well, it happens you can use "array.field" on a find's projection block.

 db.itens.find({},{"complist.b":1,list:{$elemMatch:{$in:["a"]}}})

did what I needed.

Stephen Lynx
  • 1,077
  • 2
  • 14
  • 29