29

I want to create a Schema.statics.random function that gets me a random element from the collection. I know there is an example for the native MongoDB driver, but I can't get it working in Mongoose.

user1680104
  • 8,437
  • 4
  • 22
  • 27

10 Answers10

41

I found this Mongoose Schema static function in a GitHub Gist, which should achieve what you are after. It counts number of documents in the collection and then returns one document after skipping a random amount.

QuoteSchema.statics.random = function(callback) {
  this.count(function(err, count) {
    if (err) {
      return callback(err);
    }
    var rand = Math.floor(Math.random() * count);
    this.findOne().skip(rand).exec(callback);
  }.bind(this));
};

Source: https://gist.github.com/3453567

NB I modified the code a bit to make it more readable.

matthewtole
  • 3,207
  • 22
  • 19
  • Cool. You know how this could be transalted to CoffeeScript? – user1680104 Feb 01 '13 at 11:13
  • Okay, it didn't work, so I wasn't sure (that was the problem in first case), but since I still face the problem with inline Javscript. It's rather strange. The stuff inside function(callback) { } is never called. Maybe some problem with the DB-Connection. I'll try to figure it out. – user1680104 Feb 01 '13 at 12:59
  • Hmm, it's strange. The callback never gets called. – user1680104 Feb 01 '13 at 13:42
  • I've tested it and it works fine. Could you post your entire schema and I'll see if there's something I can see? – matthewtole Feb 01 '13 at 13:45
  • Well, it is working. Just somehow connecting to a remote server doesn't work. – user1680104 Feb 01 '13 at 15:41
  • It all came down to my development machine. So if anyone ever has the same problem: Set up another system. ;) – user1680104 Feb 01 '13 at 18:43
  • 1. Ewww coffee script, 2. they have js to coffee script online converters, just a google away – jemiloii Oct 14 '16 at 23:16
23

If you are not wanting to add "test like" code into your schema, this uses Mongoose queries.

Model.count().exec(function(err, count){

  var random = Math.floor(Math.random() * count);

  Model.findOne().skip(random).exec(
    function (err, result) {

      // result is random 

  });

});
Eat at Joes
  • 4,937
  • 1
  • 40
  • 40
  • I found this really easy to implement – chris31389 Sep 11 '15 at 08:27
  • How about multiple documents in one query. Is there an easier solution other than querying multiple times? – codersaif May 30 '16 at 07:23
  • @codersaif This is not what you'd want to use for that. The skip function is directed at pagination and is a cursor, [mongodb cursor](https://docs.mongodb.com/manual/reference/method/cursor.skip/). This will move the cursor to a specific position and is meant to read forward. The code in my answer is basically pagination, `Model.findOne().skip(100).limit(1).exec()` but is fetching a single document. – Eat at Joes Nov 08 '16 at 11:48
  • 3
    @PrathameshMore 1)`Model.count()` retrieves how many documents are actually in the collection. 2) `Math....` gets a random whole number within the range of documents counted. 3) Finally we hit mongo one more time with the `findOne()` using the `skip` function to get a single document indexed to the random number in range. – Eat at Joes Sep 23 '19 at 20:49
  • What if I want to fetch 10 documents from the database? How can I implement this? – Prathamesh More Apr 21 '20 at 16:34
17

A shorter and maybe more performant solution
(we don't iterate through the collection once to count and a second time to skip elements, but mongoose might do that behind the scenes):

Use aggregate and $sample:

Model.aggregate([{ $sample: { size: 1 } }])
Ioanna
  • 1,311
  • 2
  • 23
  • 36
14

You can use aggregate:

User.aggregate([
    {$match: {gender: "male"}},
    {$sample: {size: 10}}
], function(err, docs) {
    console.log(docs);
});

Or you can use npm package https://www.npmjs.com/package/mongoose-simple-random

User.findRandom({gender: "male"}, {}, {limit: 10}, function(err, results) { 
    console.log(results); // 10 elements
});
Deepak
  • 3,134
  • 2
  • 24
  • 24
6

I've implemented a plugin for mongoose that does this in a very efficient way using a $near query on two randomly generated coordinates using a 2dsphere index. Check it out here: https://github.com/matomesc/mongoose-random.

Mihai Tomescu
  • 1,525
  • 16
  • 21
3

For people looking at this in times of async/await, promises etc.:

MySchema.statics.random = async function() {
  const count = await this.count();
  const rand = Math.floor(Math.random() * count);
  const randomDoc = await this.findOne().skip(rand);
  return randomDoc;
};
embiem
  • 113
  • 5
1

Nowadays Model.estimatedDocumentCount() is recommended. For the Model called Item an outline of a function would be:

const Item = require('../your/path/to/Model/Item')

async function getRandomItem() {
   const numItems = await Item.estimatedDocumentCount()
   const rand = Math.floor(Math.random() * numItems)
   const randomItem = await Item.findOne().skip(rand)
   return randomItem
}
Jakub Siwiec
  • 428
  • 4
  • 13
0
// Getting estimated document count.
yourModel.estimatedDocumentCount().then((count) => {
  //Random number between 0 and count.
  const rand = Math.floor(Math.random() * count);

  // Finding ONE random document.
  yourModel
    .findOne()
    .skip(rand)
    .then((randomDocument) => {
      console.log(randomDocument);
    });
});

You can also use countDocuments(), but estimatedDocumentCount() is recommended for performance.

I prefer this method due to getting one document instead of an array.

FBaez51
  • 482
  • 1
  • 7
  • 12
0
  1. Get the total number of documents in collection.
  2. Define a random number between 1 and the total number of documents.
  3. Get a document and skip that random number.
const total = await model.countDocuments();
const skip = Math.floor(Math.random() * total) + 1;
const randomDoc = await model.findOne({}).skip(skip).exec();
Michael Lynch
  • 2,682
  • 3
  • 31
  • 59
0

command which gives 3 random documents:

db.collection.aggregate([{$sample:{size: 3}}])

express line to get 4 random documents. i think it works in the latest versions of mongo.

db.aggregate([{$sample:{size: 4}}])

see this link for further info

Community
  • 1
  • 1