skip to Main Content

i have a mongodb collection that I sort by the amount of points each item has, and it shows a rank according to it’s place in the collection :

db.collection('websites').find({}).sort({  "points": -1 }).forEach(doc => {
    
  rank++;
  doc.rank = rank;
  delete doc._id;
  console.log(doc)

enter image description here

Si I thought to myself : Ok, I’m gonna update the rank in the collection, so I added this :

 db.collection('websites').updateMany({},
    { $set: { rank: doc.rank } }
)

But I was too good to be true, and it updates every single item with the same rank, which changes at each refresh, what exactly is going on, here ?

enter image description here
enter image description here

EDIT : I managed to do it by doing this :

rank = 0;

db.collection('websites').find({}).sort({  "points": -1 }).forEach(doc => {

  rank++;
  doc.rank = rank;
  //delete doc._id;
  console.log(doc._id);

  db.collection('websites').updateMany({_id : doc._id},
    { $set: { rank: doc.rank } },
    { upsert: true }
)

})

5

Answers


  1. Chosen as BEST ANSWER

    I managed to do it by doing:

    rank = 0;
    
    db.collection('websites').find({}).sort({  "points": -1 }).forEach(doc => {
      rank++;
      doc.rank = rank;
      //delete doc._id;
      console.log(doc._id);
    
      db.collection('websites').updateMany({_id : doc._id},
        { $set: { rank: doc.rank } },
        { upsert: true }
      )
    })
    

    Thank you everyone !


  2. Try this:

    db.collection('websites')
        .updateOne( //update only one
        {rank: doc.rank}, //update the one where rank is the sent in parameter doc.rank
        { $set: { rank: doc.rank } } // if multiple docs have the same rank you should send in more parameters
    )
    
    Login or Signup to reply.
  3. db.collection('websites').updateMany({/*All docs match*/},
        { $set: { rank: doc.rank } }
    )
    

    Reason it updates same rank because you have no filter which means it matches all docs in the collection and you have updateMany

    You need to set a filter to restrict docs to be updated.

    db.collection('websites').updateMany({id: "someID"},
            { $set: { rank: doc.rank } }
        )
    
    Login or Signup to reply.
  4. The problem here is that mongo is using the same doc.rank value to update all the records that match the filter criteria (all records in your case). Now you have two options to resolve the issue –

    1. Works but is less efficient) – Idea here is that you need to calculate the rank for each website that you want to update. loop throuh all the document and run below query which will update every document with it’s calculated rank. You could probably think that this is inefficient and you would be right. We are making large number of network calls to update the records. Worse part is that the slowness is unbounded and will get slower as number of records increases.
    db.collection('websites')
      .updateOne( 
        { id: 'docIdThatNeedsToBeUpdated'}, 
        { $set: { rank: 'calculatedRankOfTheWebsite' } } 
      )
    
    1. Efficient option – Use the same technique to calculate the rank for each website and loop through it to generate the update statement as above. But this time you would not make the update calls separately for all the websites. Rather you would use Bulk update technique. You add all your update statement to a batch and execute them all at one go.
    //loop and use below two line to add the statements to a batch.
    var bulk = db.websites.initializeUnorderedBulkOp();
    bulk.find({ id: 'docIdThatNeedsToBeUpdated' })
      .updateOne({
        $set: {
          rank: 'calculatedRankOfTheWebsite'
        }
      });
    
    //execute all of the statement at one go outside of the loop
    bulk.execute();
    
    Login or Signup to reply.
  5. The OP states we want to sort all the docs by points, then "rerank" them from 1 to n in that order and update the DB. Here is an example of where "aggregate is the new update" thanks to the power of $merge onto the same collection as the input:

    db.foo.aggregate([
        // Get everything in descending order...                                                     
        {$sort: {'points':-1}}
    
        // ... and turn it into a big array:                                              
        ,{$group: {_id:null, X:{$push: '$$ROOT'}}}
    
        // Walk the array and incrementally set rank.  The input arg
        // is $X and we set $X so we are overwriting the old X:                                    
        ,{$addFields: {X: {$function: {
            body: function(items) {
                for(var i = 0; i < items.length; i++) {
                    items[i]['rank'] = (i+1);
                }
                return items;
            },
            args: [ '$X' ],
            lang: "js"
            }}
        }}
    
        // Get us back to regular docs, not an array:                                     
        ,{$unwind: '$X'}
        ,{$replaceRoot: {newRoot: '$X'}}
    
        // ... and update everything:                                                     
        ,{$merge: {
            into: "foo",
            on: [ "_id" ],
            whenMatched: "merge",
            whenNotMatched: "fail"
        }}
    ]);
    

    If using $function spooks you, you can use a somewhat more obtuse approach with $reduce as a stateful for loop substitute. To better understand what is happening, block comment with /* */ the stages below $group and one by one uncomment each successive stage to see how that operator is affecting the pipeline.

    db.foo.aggregate([
        // Get everything in descending order...                                                     
        {$sort: {'points':-1}}
    
        // ... and turn it into a big array:                                              
        ,{$group: {_id:null, X:{$push: '$$ROOT'}}}
    
        // Use $reduce as a for loop with state.                                          
        ,{$addFields: {X: {$reduce: {
            input: '$X',
    
            // The value (stateful) part of the loop will contain a                       
            // counter n and the array newX which we will rebuild with                    
            // the incremental rank:                                                      
            initialValue: {
                n:0,
                newX:[]
            },
            in: {$let: {
               vars: {qq:{$add:['$$value.n',1]}}, // n = n + 1                       
               in: {
                   n: '$$qq',
                   newX: {$concatArrays: [
                            '$$value.newX',
                            // A little weird but this means "take the 
                            // current item in the array ($$this) and 
                            // set $$this.rank = $qq by merging it into the 
                            // item.  This results in a new object but 
                            // $concatArrays needs an array so wrap it 
                            // with [ ]":
                            [ {$mergeObjects: ['$$this',{rank:'$$qq'}]} ]
                          ]}
                        }
                }}
            }}
        }}
    
        ,{$unwind: '$X.newX'}
        ,{$replaceRoot: {newRoot: '$X.newX'}}
    
        ,{$merge: {
            into: "foo",
            on: [ "_id" ],
            whenMatched: "merge",
            whenNotMatched: "fail"
        }}
    ]);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search