Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

storing upvotes/downvotes in mongodb

I have a collection of Posts and Users where users can upvote/downvote each post. What would be the best way to store this in a mongodb database to make sure that users cannot vote for a given document more than once?

The simplest nosql-ish solution I came up with is storing an array of user_ids who voted inside each Post document (or even array of (user_id, vote) where vote is +1 or -1 so that users will be able to change their vote). Is it a good idea from the performance standpoint given that each post can have thousands of votes?

What about really popular websites like Reddit where top posts can have hundreds of thousands of votes?

like image 924
ramirami Avatar asked Aug 27 '12 18:08

ramirami


3 Answers

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.

like image 102
ertemishakk Avatar answered Sep 20 '22 08:09

ertemishakk


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.

like image 24
Gilbert Le Blanc Avatar answered Sep 22 '22 08:09

Gilbert Le Blanc


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_ids in the Post document.

However, you could consider storing the votes in the User document instead (i.e. the post_ids 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).

like image 41
Thomas Avatar answered Sep 22 '22 08:09

Thomas