简体   繁体   中英

Get random document from a Meteor collection

What would be the most efficient algorithm for fetching a random document from a Meteor collection, given that there is no numeric index?

( There is another question which deals with doing so in MongoDB using the skip method, but this doesn't seem to be supported in Meteor).

The inefficient way I came up with was selecting all the records and iterating up to a random number, but that is clearly getting expensive and cumbersome as the collection size grows.

Had the same problem, but I need to get a random element from the results of a query. I found a solution, thanks to this question mentioning fetch(): Meteor: Finding an object from a collection by _id

You can convert the query to an array with this method. So converting the query results to an array would be Collection.find().fetch() . You can then simply get the length of this array and use it to generate a random number, and select that element of the array.

var array = Collection.find().fetch();
var randomIndex = Math.floor( Math.random() * array.length );
var element = array[randomIndex];

NOTE: this works in Meteor, not in plain MongoDB! For MongoDB, see the other answer or linked questions that use skip().

Currently the MongoDB query language has no random-operator (although there is an open feature-request ticket for that ).

Update Version 3.2: You can now use the $sample aggregation operator to obtain a random sample.

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

If you can't or don't want to use it, there are some workarounds, but they aren't pretty.

One is to use db.collection.count() to get the number of documents in the collection. Then you can select the n -th document with db.collection.find().skip(n).limit(1) . But when the collection is large this can take a while, because the whole collection needs to be iterated with a cursor.

Another is to add a field rand with a random floating-point number between 0.0 and 1.0 to each document when you insert them. Then you can generate another random number r and do db.collection.find({rand:{$gt:r}}).sort({rand:1}).limit(1) to get the next greater one. When you have an index on the rand field, this will be very fast. But the randomness won't be uniformly distributed, because those documents which happen to have a larger gap between them and their predecessor will be picked more frequently. Also, when r happens to be larger than the largest in the collection, no result will be returned. In that case you should try again with the same number but this time with rand:{$lte:r} and sort({rand:-1}) . When this doesn't return a document either, the collection is empty (or at least has no documents with a rand field).

The only corner-case where you can quickly and fairly select a random document is when your collection doesn't change (or at least doesn't change frequently). In that case you can number all the documents with consecutive integers starting with 0, index that field, and find() for a random number between 0 and your already known number of documents.

Using underscore, the below worked for me:

function(){
    var random = _.sample(Collection.find().fetch());
    return Collection.find({_id: random && random._id});
}

Inspired from @dillygirl response. How to select N random users from the Meteor.users collection. I created the getRandomBots() method (testing twitter API ):

function getRandomBots(numberOfBots){
    var usersCollectionArray = Meteor.users.find().fetch();
    //if you want all the users just getRandomBots();
    if(typeof numberOfBots === "undefined"){
        return usersCollectionArray;
    }
    /***
     * RandomNumbers
     * @param numberOfBots
     * @param max
     * @returns {Array}
     */
    function randomNumbers(numberOfBots, max){
        var arr = []
        while(arr.length < numberOfBots){
            var randomnumber=Math.ceil(Math.random()*max);
            var found=false;
            for(var i=0;i<arr.length;i++){
                if(arr[i]==randomnumber){found=true;break}
            }
            if(!found)arr[arr.length]=randomnumber;
        }
        return arr;
    }
    //length of the users collection
    var count = Meteor.users.find().count();

    //random numbers between 0 and Max bots, selecting the number of bots required
    var numbers = randomNumbers(numberOfBots,count);

    //the bots we are gonna select
    var bots = [];

    //pushing users with using the random number as index.
    _.each(numbers,function(item){
        bots.push(usersCollectionArray[item]);
    });

    //testing on server console ...
    _.each(bots,function(item){
       console.log(item.services.twitter.screenName);
    });
}

//selecting 8 bots
getRandomBots(8);

You can use the random _id attribute of out collections to solve this, it will be random in the first call. It can also be in different order due "choice", but it is not completely random, altough solved my task:

collection.find({},{sort: _id:Random.choice([1,-1])}})

A "true" random would be:

var items = collection.find({}).fetch();
var random_items = _.shuffle(items);
var random_items_id = _.map(random_items,function(element){return element._id});
return collection.find({_id:{$in:random_items_id}});
import { Random } from 'meteor/random';

const getRandomDocuments = function(amount) {
    // finds the next _id >= from a random one
    const randomId = Random.id();
    const gteRandom = MyCollection.find({
        _id: { $gte: randomId }
    }, {
        fields: { _id: 1 },
        sort: [
            ['_id', 'asc']
        ],
        limit: amount
    });
    const remainingToGet = Math.max(0, amount - gteRandom.count());
    // if it didn't find enough looks for next _id < the random one
    const ltRandom = MyCollection.find({
        _id: { $lt: randomId }
    }, {
        fields: { _id: 1 },
        sort: [
            ['_id', 'asc']
        ],
        limit: remainingToGet
    });
    // combine the two queries
    let allIds = [];
    gteRandom.forEach(function(doc) {
        allIds.push(doc._id);
    });
    ltRandom.forEach(function(doc) {
        allIds.push(doc._id);
    });
    return MyCollection.find({
        _id: { $in: allIds }
    });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM