2

Very new to mongo and having a little trouble getting my head around this problem. I have collection looking like this

{ "_id" : "PN89dNYYkBBmab3uH", "card_id" : 1, "vote" : 1, "time" : 1437700845154 }
{ "_id" : "Ldz7N5syeW2SXtQzP", "card_id" : 1, "vote" : 1, "time" : 1437700846035 }
{ "_id" : "v3XWHHvFSHwYxxk6H", "card_id" : 1, "vote" : 2, "time" : 1437700849817 }
{ "_id" : "eehcDaCyTdz6Yd2a9", "card_id" : 2, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "efhcDaCyTdz6Yd2b9", "card_id" : 2, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "efhcDaCyTdz7Yd2b9", "card_id" : 3, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "w3XWgHvFSHwYxxk6H", "card_id" : 1, "vote" : 1, "time" : 1437700849817 }

I need to write a query that will group cards basically by votes (1 is like; 2 is dislike). So I need to get the total number of likes minus dislikes for each card and be able to rank them.

Result would be like:

card_id 1: 3 likes - 1 dislike = 2

card_id 3: 1 like - 0 dislikes = 1

card_id 2: 2 likes - 0 dislikes = 0

Any idea how to get the count of all the cards' 1 votes minus 2 votes and then order the results?

Blakes Seven
  • 49,422
  • 14
  • 129
  • 135
user2994560
  • 1,269
  • 4
  • 18
  • 30
  • @user2994560 on the server or on the client? I.e. do you have all of the data published already and you just need the aggregation in a template, or do you need the documents sorted before they get sent to the client? – David Weldon Jul 24 '15 at 06:34

2 Answers2

4

To do any sort of "grouping" with MongoDB queries then you want to be able to use the aggregation framework or mapReduce. The aggregation framework is generally preferred as it uses native coded operators rather than JavaScript translation, and is therefore typically faster.

Aggregation statements can only be run on the server API side, which does make sense because you would not want to do this on the client. But it can be done there and make the results available to the client.

With attribution to this answer for providing the methods to publish results:

Meteor.publish("cardLikesDislikes", function(args) {
    var sub = this;

    var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;

    var pipeline = [
        { "$group": {
            "_id": "$card_id",
            "likes": {
                "$sum": {
                    "$cond": [
                        { "$eq": [ "$vote", 1 ] },
                        1,
                        0
                    ]
                }
            },
            "dislikes": {
                "$sum": {
                    "$cond": [
                        { "$eq": [ "$vote", 2 ] },
                        1,
                        0
                    ]
                }
            },
            "total": {
                "$sum": {
                    "$cond": [
                        { "$eq": [ "$vote", 1 ] },
                        1,
                        -1
                    ]
                }
            }
        }},
        { "$sort": { "total": -1 } }
    ];

    db.collection("server_collection_name").aggregate(        
        pipeline,
        // Need to wrap the callback so it gets called in a Fiber.
        Meteor.bindEnvironment(
            function(err, result) {
                // Add each of the results to the subscription.
                _.each(result, function(e) {
                    // Generate a random disposable id for aggregated documents
                    sub.added("client_collection_name", Random.id(), {
                        card: e._id,                        
                        likes: e.likes,
                        dislikes: e.dislikes,
                        total: e.total
                    });
                });
                sub.ready();
            },
            function(error) {
                Meteor._debug( "Error doing aggregation: " + error);
            }
        )
    );

});

The general aggregation statement there is just a $group operation on the single key of "card_id". In order to get the "likes" and "dislikes" you use a "conditional expression" which is $cond.

This is a "ternary" operator which considers a logical test on the valueof "vote", and where it matches the expect type then a positive 1 is returned, otherwise it is 0.

Those values are then sent to the accumulator which is $sum to add them together, and produce the total counts for each "card_id" by either "like" or "dislike".

For the "total", the most efficient way is to attribute a "positive" value for "like" and a negative value for "dislike" at the same time as doing the grouping. There is an $add operator, but in this case it's usage would require another pipeline stage. So we just do it on a single stage instead.

At the end of this there is a $sort in "descending" order so the largest positive vote counts are on top. This is optional and you might just want to use dynamic sorting client side. But it is a good start for a default that removes the overhead of having to do that.

So that is doing a conditional aggregation and working with the results.


Test listing

This is what I tested with the a newly created meteor project, with no addins and just a single template and javascript file

console commands

meteor create cardtest
cd cardtest
meteor remove autopublish

Created the "cards" collection in the database with the documents posted in the question. And then edited the default files with the contents below:

cardtest.js

Cards = new Meteor.Collection("cardStore");

if (Meteor.isClient) {

  Meteor.subscribe("cards");

  Template.body.helpers({
    cards: function() {
      return Cards.find({});
    }
  });

}

if (Meteor.isServer) {

  Meteor.publish("cards",function(args) {
    var sub = this;

    var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;

    var pipeline = [
      { "$group": {
        "_id": "$card_id",
        "likes": { "$sum": { "$cond": [{ "$eq": [ "$vote", 1 ] },1,0] } },
        "dislikes": { "$sum": { "$cond": [{ "$eq": [ "$vote", 2 ] },1,0] } },
        "total": { "$sum": { "$cond": [{ "$eq": [ "$vote", 1 ] },1,-1] } }
      }},
      { "$sort": { "total": -1, "_id": 1 } }

    ];

    db.collection("cards").aggregate(
      pipeline,
      Meteor.bindEnvironment(
        function(err,result) {
          _.each(result,function(e) {
            e.card_id = e._id;
            delete e._id;

            sub.added("cardStore",Random.id(), e);
          });
          sub.ready();
        },
        function(error) {
          Meteor._debug( "error running: " + error);
        }
      )
    );

  });
}

cardtest.html

<head>
  <title>cardtest</title>
</head>

<body>
  <h1>Card aggregation</h1>

  <table border="1">
    <tr>
      <th>Card_id</th>
      <th>Likes</th>
      <th>Dislikes</th>
      <th>Total</th>
    </tr>
    {{#each cards}}
      {{> card }}
    {{/each}}
  </table>

</body>

<template name="card">
  <tr>
    <td>{{card_id}}</td>
    <td>{{likes}}</td>
    <td>{{dislikes}}</td>
    <td>{{total}}</td>
  </tr>
</template>

Final aggregated collection content:

[
   {
     "_id":"Z9cg2p2vQExmCRLoM",
     "likes":3,
     "dislikes":1,
     "total":2,
     "card_id":1
   },
   {
     "_id":"KQWCS8pHHYEbiwzBA",
      "likes":2,
      "dislikes":0,
      "total":2,
      "card_id":2
   },
   {
      "_id":"KbGnfh3Lqcmjow3WN",
      "likes":1,
      "dislikes":0,
      "total":1,
      "card_id":3
   }
]
Community
  • 1
  • 1
Blakes Seven
  • 49,422
  • 14
  • 129
  • 135
  • thanks. are "server_collection_name" and "client_collection_name" the same name of the existing collection? is this creating a new collection called "cardLikesDislikes" to the db? how can I access the results – user2994560 Jul 24 '15 at 17:38
  • @user2994560 That's just a "placehoder" for whatever your actual "collection" name is because I have no idea what it actually is from the context of your question. It basically means *"use the real collection name"* since this is the lower level driver and not the model access of meteor collections. – Blakes Seven Jul 24 '15 at 17:47
  • this seems to almost work, except when I run Scores.find().fetch() in the console, it's only returning an object with one result. like this _id: "4TbK37Yr2MoEJeJJy" card: null likes: 5 total: 4 dislikes: 1... this is the correct total of all the cards but not grouping the votes by ID – user2994560 Jul 24 '15 at 18:19
  • its totalling all of them together – user2994560 Jul 24 '15 at 20:29
  • @user2994560 Sorry for the delay. If you are having problems then it will be with how you are implementing. I have included a very basic test that I have used and just works correctly for me. – Blakes Seven Jul 25 '15 at 05:28
  • Thanks for the response. Worked perfectly! While I've got you here, can I ask do you know how to tally votes based on a time period relating to the timestamp? So if I wanted to get the votes that were recorded in the past day, week, month, etc? – user2994560 Jul 27 '15 at 20:18
  • @user2994560 You are meant to post new questions if you have other questions to the one that you aske, but you are also meant to search for answers first. The "top search result" for that is [already on this site ](http://stackoverflow.com/questions/26814427/group-result-by-15-minutes-time-interval-in-mongodb) if you have not found it already. Do not forget to mark the answers you get as useful by [accepting the answer](http://stackoverflow.com/help/accepted-answer) and hopefully processing an upvote if you have not done so already. – Blakes Seven Jul 27 '15 at 22:53
  • http://stackoverflow.com/questions/41074339/how-can-i-calculate-price-positive-and-negative-price-using-mongodb-or-robomongo @Blakes Seven – its me Dec 10 '16 at 10:37
1

use meteorhacks:aggregate package.

Cards = new Mongo.Collection('cards');

var groupedCards = Cards.aggregate({ $group: { _id: { card_id: '$card_id' }}});

groupedCards.forEach(function(card) {
  console.log(card);
});


{ "_id" : "v3XWHHvFSHwYxxk6H", "card_id" : 1, "vote" : 2, "time" : 1437700849817 }
{ "_id" : "eehcDaCyTdz6Yd2a9", "card_id" : 2, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "efhcDaCyTdz7Yd2b9", "card_id" : 3, "vote" : 1, "time" : 1437700850666 }
yasaricli
  • 2,433
  • 21
  • 30
  • http://stackoverflow.com/questions/41074339/how-can-i-calculate-price-positive-and-negative-price-using-mongodb-or-robomongo – its me Dec 10 '16 at 10:37