1

Right now, this expressJS call serves comma-separated values to a mongoose .count that leads to a .find, but returns the count and the values separately..

How would I construct the results to be an array with each searched tag + the count of how many stories have that tag, in a way that would look something like this (to be looped over with a templating engine):

tags : [ { tag: tag1, count: 4 }, { tag: tag2, count: 2 } ]

Here's the express/mongoose:

router.route('/tag/:tags')

    // get the list of tags for userGenerated tags
    // accessed by GET at http://localhost:4200/api/v1/tag/tag1,tag2
    .get(function(req, res) {

        var tags = req.params.tags.split(',');

        console.log(tags); // ['tag1', 'tag2']

        Story.count( { tags: { $in: tags } }, function (err, count) {

            if (err)
                res.send(err);

            console.log('there is/are %d story(ies) with this/these tag(s)', count);

            if (count >= 1) {

                Story.find( { tags: { $in: tags } }, 'tags', function(err, stories) {

                    if (err)
                        res.send(err);

                    res.json(storytags);

                }); // find and return tags of stories

            } // if count >= 1

            if (count < 1) {

                res.json({ message: count + ' results' });

            } // if count < 1 

        }); // .count

    }); // get list of tags
3
  • What do you mean by "respective count"? Could you explain more what is in your expected output tags : [ { element: element1, count: count1 }, { element: element2, count: count2 } ] (Are the stories the elements? And are the counts the number of times the tags appear in the stories?) Commented Jun 9, 2014 at 16:39
  • @dylants a more contextual json for the output would be tags : [ { tag: tag1, count: 4 }, { tag: tag2, count: 2 } ] (stories are the db docs that have an array of tags) where this would be returning an array that consists of each queried tag and that tag's count (of stories wherein that tag appears) Commented Jun 9, 2014 at 17:50
  • Then to me the first Story.count isn't necessary since it's counting all stories that contain those tags. What you need is the second query, the Story.find that you have above, and then perhaps iterate over those stories collecting the stats you need to output in the tags block you've described. Commented Jun 9, 2014 at 19:29

1 Answer 1

3

Not very clear here how "tags" actually appears in your document, where it is either just a single value in a field or in an array. At any rate if you intending to count tags then you are best off using the aggregation framework rather than "post processing" your .find() results in code.

First with "tags" as a single field value:

    Story.aggregate(
        [
            // Match the documents with tags
            { "$match": { "tags": { "$in": tags } }},

            // Group the tags with counts
            { "$group": {
                "_id": "$tags",
                "count": { "$sum": 1 }
            }},

            // Group to single array with total count
            { "$group": {
                "_id": null,
                "tags": { 
                    "$push": {
                        "tag": "$_id",
                        "count": "$count"
                    }
                },
                "totalCount": { "$sum": "$count" }
            }},

            // Project to remove the _id
            { "$project": {
                "_id": 0,
                "tags": 1,
                "totalCount": 0
            }}
        ],
        function(err,result) {

            if (err)
                res.send(err);

            res.json( result[0] );

        }
    );

That pretty much encapsulates your entire listing so there is no need for that additional .count() operation, but also not sure if you really need it.

Where the "tags" is actually an array then you need to do things a little differently, as you need to extract your matches from the array to get the counts:

    Story.aggregate(
        [
            // Match the documents with tags
            { "$match": { "tags": { "$in": tags } }},

            // Unwind the array
            { "$unwind": "$tags" },

            // Actually match the tags within the now unwound array
            { "$match": { "tags": { "$in": tags } }},

            // Group by each tag
            { "$group": {
                "_id": "$tags",
                "count": { "$sum": 1 }
            }},

            // Rename the _id for your output
            { "$project": {
               "_id": 0,
               "tag": "$_id",
               "count": 1
            }}

        ],
        function(err,result) {

            if (err)
                res.send(err);

            res.json({ "totalCount": count, "tags": result });

        }
    );

Or with MongoDB 2.6 or greater you can streamline a little possibly by filtering the array before you $unwind the array using the $map operator and others.

I'll just expand the tags value here for clarity

    Story.aggregate(
        [
            // Match the documents with tags
            { "$match": { "tags": { "$in": ["tag1","tag2" } }},


            // Filter the array for matches
            { "$project": {
                "tags": {
                   "$setDifference": [
                       { 
                           "$map": {
                               "input": "$tags",
                               "as": "el",
                               "in": {
                                   "$cond": [
                                       { "$or": [
                                           { "$eq": ["$$el", "tag1" ] },
                                           { "$eq": ["$$el", "tag2" ] },
                                       ]},
                                       "$$el",
                                       false
                                   ]
                               }
                           }
                       },
                       [false]
                   ]
                }
            }},

            // Unwind the array already filtered
            { "$unwind": "$tags" },

            // Group by each tag
            { "$group": {
                "_id": "$tags",
                "count": { "$sum": 1 }
            }},

            // Rename the _id for your output
            { "$project": {
               "_id": 0,
               "tag": "$_id",
               "count": 1
            }}

        ],
        function(err,result) {

            if (err)
                res.send(err);

            res.json({ "totalCount": count, "tags": result });

        }
    );

Which has the advantage of removing the "tags" you do not want from the array before calling $unwind which can speed up the matching depending on how many array elements there are.

Of course while the above examples differ from the first in obtaining the document count, you can still include this with a little extra grouping if you prefer the combined result:

    Story.aggregate(
        [
            // Match the documents with tags
            { "$match": { "tags": { "$in": ["tag1","tag2" } }},


            // Filter the array for matches
            { "$project": {
                "tags": {
                   "$setDifference": [
                       { 
                           "$map": {
                               "input": "$tags",
                               "as": "el",
                               "in": {
                                   "$cond": [
                                       { "$or": [
                                           { "$eq": ["$$el", "tag1" ] },
                                           { "$eq": ["$$el", "tag2" ] },
                                       ]},
                                       "$$el",
                                       false
                                   ]
                               }
                           }
                       },
                       [false]
                   ]
                }
            }},

            // Unwind the array already filtered
            { "$unwind": "$tags" },

            // Group by each tag
            { "$group": {
                "_id": "$tags",
                "count": { "$sum": 1 },
                "docs": { "$addToSet": "$_id" }
            }},

            // Group to a single array response
            { "$group": {
                "_id": null,
                "tags": {
                    "$push": {
                        "tag": "$_id",
                        "count": "$count"
                    }
                },
                "totalCount": { "$sum": { "$size": "$docs" } }
            }

            // Rename the fields for your output
            { "$project": {
               "_id": 0,
               "tags": 1,
               "count": 1,
               "totalCount"
            }}

        ],
        function(err,result) {

            if (err)
                res.send(err);

            res.json( result[0] );

        }
    );

Again, obtaining that "totalCount" in a singular result can be done without the help of operators such as $size and indeed without the $addToSet by grouping differently.

But the overall direction here is this is where you basically want to go if you are counting "tags", as you can let the server do the work for you.

Sign up to request clarification or add additional context in comments.

1 Comment

many thanks for this.. really grateful for the in depth detail.. will be integrating this very soon, and am looking forward to accepting the answer then.. thanks again

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.