0

I'm trying to figure out how I can query nested objects within a document. Suppose I have a mongoose Schema as follows:

var posts = mongoose.Schema({
    title : String,
    post : String,
    comments : [{
        status : Number,
        title : String,
        comment : String
    }]
});

I want to search for all comments in a particular post, where the status of the comment equals 1. This is what I have tried, but it doesn't work:

Post.find(
    {_id: req.params.id, comments: { status: 1 }},
    null,
    {},
    function (err, data) {
        if (err) return console.error(err);
        return res.json(data);
    }
);

What am I doing wrong?

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Chris Paterson
  • 1,129
  • 5
  • 21
  • 31

1 Answers1

3

Use "dot notation":

Post.find(
    {
        _id: req.params.id, 
        "commments": { "$exists": true }, 
        "comments.status": 1 
    },
    function (err, data) {
        if (err) return console.error(err);
        return res.json(data);
    }
);

Without that you are asking for comments to be "exactly" in the form you are querying and only contain "status" as a field.

If all you want to return are just the matching comments of an array you basically have two options.Either you use the positional $ operator in order to return only the first matching element of an array, which is it's current restriction. Or you use the aggregation framework in order to return all of the items:

Post.aggregate(
    [
        // Initial match is on only the documents that contain
        { "$match": {
            "_id": req.params.id,
            "comments": { "$exists": true },
            "comments.status": 1
        }},

        // Unwind the array to "de-normalize" as documents
        { "$unwind": "$comments" },

        // Match the "unwound" documents
        { "$match": { 
            "comments.status": 1
        }},

        // Push back as an array
        { "$group": {
            "_id": "$_id",
            "title": { "$first": "$title },
            "post": { "$first": "$post" },
            "comments": { "$push": "$comments" }
        }}
    ],
    function(err,data) {

        // work in here
})

Or with MongoDB 2.6 and upwards you can do that "in-place" using $map instead:

Post.aggregate(
    [
        // Initial match is on only the documents that contain
        { "$match": {
            "_id": req.params.id,
            "comments": { "$exists": true },
            "comments.status": 1
        }},

        // Project matching items only
        { "$project": {
           "title": 1,
           "post": 1,
           "comments": {
               "$setDifference": [
                   { 
                       "$map": {
                           "input": "$comments",
                           "as": "el",
                           "in": {
                               "$cond": [
                                   { "$eq": [ "$$el.status", 1 ] },
                                   "$$el",
                                   false
                               ]
                           }
                       }
                   },
                   [false]
               ]
           }
        }}
    ],
    function(err,data) {

        // work in here
})

So the difference here is matching the "document" that contains the conditions you specify in the query and "filtering" the array members that only match the specification of your query.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • I tried that, but it's still returning all comments, not just those with status = 1. – Chris Paterson Jun 05 '14 at 02:54
  • @ChrisPaterson The query form with find only matches "documents" which contain the whole array. Applying a "filter" to the array contents of the document is a different thing. See the additional information. – Neil Lunn Jun 05 '14 at 03:12
  • I'm unable to get any of the above to work. I get this message in the console: "TypeError: Cannot read property 'comments' of undefined". Also, I'm using mongo 2.4. – Chris Paterson Jun 06 '14 at 00:25
  • @ChrisPaterson That means you have documents that do not contain a comments field. I have altered the examples to include the `$exists` operator in the query that requires this field in the document otherwise it will not attempt to match – Neil Lunn Jun 06 '14 at 00:36