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
}
]