storing upvotes/downvotes in mongodb
What about really popular websites like Reddit where top posts can have hundreds of thousands of votes?
What about them? Using your idea in a relational database, you have an integer for the user id pointer, an integer for the post pointer, and a byte for the vote. 9 bytes total for each vote.
Sure, there's some index overhead. Make it 15 bytes total per vote. 6 million votes would take up 90 megabytes of disk space.
Reddit locks posts after a period of time, so they can't be edited or voted on. So Reddit doesn't have to store individual votes forever. Just vote totals.
MongoDB documents are currently restricted to a maximum of 16MB, so assuming Gilbert's calculations are accurate, you wouldn't be able to store all 6 Million user_id
s in the Post
document.
However, you could consider storing the votes in the User
document instead (i.e. the post_id
s that the particular user voted for). It is much less likely that a user votes on 6 million different posts, so this way you won't reach the size limit as quickly.
Another way to handle this: If you expect that many votes for a particular post, you may want to store the votes outside the Post
documents in a separate collection and do an additional query, similar to a many-to-many JOIN table in SQL fashion:
user_votes { user_id: ObjectId(...), post_id: ObjectId(...), vote:-1 }
and create a compound index on (user_id, post_id).
Alright, I know its been a while since this question has been asked, however I wasnt satisfied with the above answers or any other answers that was posted on similar questions. So finally I came up with another answer.
First of all, as mentioned above, MongoDB documents are currently restricted to a maximum of 16MB. Therefore logical way of implementing this would be, having 2 different collections, one for upvotes and one for downvotes which would store postid and array of users who have upvoted or downvoted. This would help us seperate the collections and have more space to work with. To make it clear here is the schemas I have used :
for upvotes:
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const UpvoteModel = new Schema({
questionid: {
type: Schema.Types.ObjectId,
ref: 'questionsModel'
}
,
votes: [{
user: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}]
})
module.exports = Upvote = mongoose.model('upvotes', UpvoteModel)
for downvotes:
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const DownvoteModel = new Schema({
questionid: {
type: Schema.Types.ObjectId,
ref: 'questionsModel'
}
,
votes: [{
user: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}]
})
module.exports = Downvote = mongoose.model('downvotes', DownvoteModel)
post collection ( or questions in this case):
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const QuestionsSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
title: {
type: String,
required: true
},
description: {
type: String
},
voteCount: {
type: Number
},
votes: [{
user: { type: Schema.Types.ObjectId }
}],
views: [{
user: { type: Schema.Types.ObjectId }
}],
answers: [{
user: { type: Schema.Types.ObjectId },
answer: {
type: String
},
date: {
type: Date,
default: Date.now
}
}],
date: {
type: Date,
default: Date.now
}
})
module.exports = Question = mongoose.model('questionsModel', QuestionsSchema)
Everytime a post/question is created, post/question id is stored in upvote and downvote table, then if a user clicks on upvote or downvote you would need to add that user in that specific table then go and update the voteCount in post/question table.
post/question route :
router.post('/upvote', (req, res) => {
Upvote.findOne({ questionid: req.body.params.questionid })
.then(oneVote => {
if (oneVote.votes.filter(user => req.body.params.userid).length === 1) {
Question.updateOne({ _id: req.body.params.questionid },
{ $inc: { voteCount: -1 } })
.then(() => {
Upvote.updateOne({
questionid: req.body.params.questionid,
},
{
$pull: {
votes: { user: ObjectId(req.body.params.userid) }
}
})
.then(() => console.log('decrement by -1'))
}
)
.catch(err => console.log(err))
}
else if (oneVote.votes.filter(user => req.body.params.userid).length === 0) {
Upvote.findOneAndUpdate({
questionid: req.body.params.questionid,
'votes.user': { $ne: ObjectId(req.body.params.userid) }
},
{
$push: {
votes: { user: ObjectId(req.body.params.userid) }
}
},
{ useFindAndModify: false }
)
.then(oldupvote => {
Downvote.findOne({ questionid: req.body.params.questionid })
.then(downvote => {
if (downvote.votes.filter(user => req.body.params.userid).length > 0) {
Downvote.updateOne({
questionid: req.body.params.questionid,
},
{
$pull: {
votes: { user: ObjectId(req.body.params.userid) }
}
})
.then(() => {
Question.updateOne({ _id: req.body.params.questionid },
{ $inc: { voteCount: 2 } })
.then(() => console.log('increment by 2')
)
.catch(err => console.log(err))
})
.catch(err => console.log(err))
}
else {
Question.updateOne({ _id: req.body.params.questionid },
{ $inc: { voteCount: 1 } })
.then(() => console.log('increment by 1')
)
.catch(err => console.log(err))
}
})
.catch(err => console.log(err))
})
}
})
.catch(err => console.log(err))
})
downvote route:
router.post('/downvote', (req, res) => {
Downvote.findOne({ questionid: req.body.params.questionid })
.then(oneVote => {
if (oneVote.votes.filter(user => req.body.params.userid).length === 1) {
Question.updateOne({ _id: req.body.params.questionid },
{ $inc: { voteCount: 1 } })
.then(() => {
Downvote.updateOne({
questionid: req.body.params.questionid,
},
{
$pull: {
votes: { user: ObjectId(req.body.params.userid) }
}
})
.then(() => console.log('increment by 1'))
.catch(err => console.log(err))
}
)
.catch(err => console.log(err))
}
else if (oneVote.votes.filter(user => req.body.params.userid).length === 0) {
Downvote.findOneAndUpdate({
questionid: req.body.params.questionid,
'votes.user': { $ne: ObjectId(req.body.params.userid) }
},
{
$push: {
votes: { user: ObjectId(req.body.params.userid) }
}
},
{ useFindAndModify: false }
)
.then(oldownvote => {
Upvote.findOne({ questionid: req.body.params.questionid })
.then(upvote => {
if (upvote.votes.filter(user => req.body.params.userid).length > 0) {
Upvote.updateOne({
questionid: req.body.params.questionid,
},
{
$pull: {
votes: { user: ObjectId(req.body.params.userid) }
}
})
.then(() => {
Question.updateOne({ _id: req.body.params.questionid },
{ $inc: { voteCount: -2 } })
.then(() => console.log('decrement by -2')
)
})
.catch(err => console.log(err))
}
else {
Question.updateOne({ _id: req.body.params.questionid },
{ $inc: { voteCount: -1 } })
.then(() => console.log('decrement by -1')
)
.catch(err => console.log(err))
}
})
.catch(err => console.log(err))
})
// .then(() => {
// Upvote.findOne({ questionid: req.body.params.questionid })
// .then(updatedupvote => console.log(updatedupvote))
// })
.catch(err => console.log(err))
}
})
.catch(err => console.log(err))
})
This answer is posted for future reference if anyone is looking for a similar answere, here it is.