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.
Collections are Meteor's way of storing persistent data. The special thing about collections in Meteor is that they can be accessed from both the server and the client, making it easy to write view logic without having to write a lot of server code.
Find() Method. In MongoDB, find() method is used to select documents in a collection and return a cursor to the selected documents. Cursor means a pointer that points to a document, when we use find() method it returns a pointer on the selected documents and returns one by one.
Using underscore, the below worked for me:
function(){
var random = _.sample(Collection.find().fetch());
return Collection.find({_id: random && random._id});
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With