4

I have a mongoose Schema that looks likes this :

var AnswerSchema = new Schema({
  author: {type: Schema.Types.ObjectId, ref: 'User'},
  likes: [{type: Schema.Types.ObjectId, ref: 'User'}]
  text: String,
  ....
});

and I have an API endpoint that allow to get answers posted by a specific user (which exclude the likes array). What I want to do is add a field (with "true/false" value for example) to the answer(s) returned by the mongoose query, when a specific user_id is (or is not) in the likes array of an answer. This way, I can display to the user requesting the answers if he already liked an answer or not.

How could I achieve this in an optimised way ? I would like to avoid fetching the likes array, then look into it myself in my Javascript code to check if specified userId is present in it, then remove it before sending it back to the client... because it sounds wrong to fetch all this data from mongoDB to my node app for nothing. I'm sure there is a better way by using aggregation but I never used it and am a bit confused on how to do it right. The database might grow very large so it must be quick and optimised.

Hexalyse
  • 369
  • 1
  • 4
  • 14
  • So you want to see if array A contains any values in array B, where both arrays are stored in the DB, but you don't want to fetch array A or B? – David says Reinstate Monica Nov 20 '15 at 16:28
  • Not exactly. I just want to retrieves some "Answers", and in the JSON data of each answers returned, I want to have a field that is "true" if the "likes" array contains a specific value, and "false" if it doesn't. – Hexalyse Nov 21 '15 at 17:36

1 Answers1

2

One approach you could take is via the aggregation framework which allows you to add/modify fields via the $project pipeline, applying a host of logical operators that work in cohort to achieve the desired end result. For instance, in your above case this would translate to:

Answer.aggregate()
    .project({
        "author": 1,            
        "matched": {
            "$eq": [ 
                { 
                    "$size": { 
                        "$ifNull": [
                            { "$setIntersection": [ "$likes", [userId] ] }, 
                            []
                        ] 
                    } 
                },
                1
            ]
        }
    })
    .exec(function (err, docs){
        console.log(docs);
    })

As an example to test in mongo shell, let's insert some few test documents to the test collection:

db.test.insert([
    {
        "likes": [1, 2, 3]
    },
    {
        "likes": [3, 2]
    },
    {
        "likes": null
    },
    {
        "another": "foo"
    }
])

Running the above aggregation pipeline on the test collection to get the boolean field for userId = 2:

var userId = 2;
db.test.aggregate([
    { 
        "$project": {               
            "matched": {
                "$eq": [ 
                    { 
                        "$size": { 
                            "$ifNull": [
                                { "$setIntersection": [ "$likes", [userId] ] }, 
                                []
                            ] 
                        } 
                    },
                    1
                ]
            }
        }
    }
])

gives the following output:

{
    "result" : [ 
        {
            "_id" : ObjectId("564f487c7d3c273d063cd21e"),
            "matched" : true
        }, 
        {
            "_id" : ObjectId("564f487c7d3c273d063cd21f"),
            "matched" : true
        }, 
        {
            "_id" : ObjectId("564f487c7d3c273d063cd220"),
            "matched" : false
        }, 
        {
            "_id" : ObjectId("564f487c7d3c273d063cd221"),
            "matched" : false
        }
    ],
    "ok" : 1
}
chridam
  • 100,957
  • 23
  • 236
  • 235
  • 1
    I'll have to take a look at the aggregation framework because it seems really verbose and complex (I don't get all these $let, vars, and $$ and the size, ifnull etc. at first glance). But it seems to be able to do what I want. The only thing I need to do differently than your answer is remove the "likes" field from the returned output - I guess I just have to remove in from the $project. – Hexalyse Nov 21 '15 at 17:40
  • @Hexalyse I've updated my answer; the `likes` field is removed from the projection as per requirements. – chridam Nov 24 '15 at 10:18
  • Thanks. This solution seemed very complex to me (going through an intersection then looking at size), considering the simple need I had. But I'm reading the aggregation doc from mongodb website and... well, it becomes clearer now. – Hexalyse Nov 24 '15 at 10:30
  • @Hexalyse No worries. On the surface, that may look daunting but once you grasp the aggregation framework fundamentals it becomes much clearer as you've noticed. There are some fantastic operators that simplify the above operations but only available in the future release, have a look at the upcoming [**New Aggregation Array Operators**](https://docs.mongodb.org/manual/release-notes/3.2/#new-aggregation-array-operators). – chridam Nov 24 '15 at 10:34
  • Isn't the $let/$vars part useless ? As far as I understand the aggregation pipeline, I could directly do the $eq part in the "matched" projection, and use "$likes" instead of "$$names" for the $setIntersection. I tried it in mongoshell and it works. Or maybe I'm missing something ? – Hexalyse Nov 25 '15 at 10:57
  • @Hexalyse I think you are right, might have overcomplicated things a little bit as I didn't start with the simple approach you mentioned, so my apologies. There may be some significant improvement in performance using the simple approach on large collections, haven't yet had enough time to test this but just inserting using the `$eq` operator directly should be the best and simple approach :-) Thanks! – chridam Nov 25 '15 at 11:01
  • If I have a request doing a .find().sort().limit().populate(), can I chain the aggregation/project to this request ? – Hexalyse Nov 25 '15 at 12:56
  • @Hexalyse That would qualify as a [**different question**](http://meta.stackexchange.com/questions/43478/exit-strategies-for-chameleon-questions) altogether according to the SO rules, consider posting that as a new question. – chridam Nov 25 '15 at 12:58
  • Thanks for the advice. Still learning the SO workflow and guidelines. – Hexalyse Nov 25 '15 at 13:55