Enregistrement aléatoire de MongoDB

Je cherche à obtenir un enregistrement aléatoire d’un énorme mongodb (100 millions d’enregistrements).

Quel est le moyen le plus rapide et le plus efficace de le faire? Les données sont déjà là et il n’y a pas de champ dans lequel je peux générer un nombre aléatoire et obtenir une ligne aléatoire.

Aucune suggestion?

À partir de la version 3.2 de MongoDB, vous pouvez obtenir N docs aléatoires à partir d’une collection à l’aide de l’opérateur de pipeline d’agrégation $sample :

 // Get one random document from the mycoll collection. db.mycoll.aggregate( { $sample: { size: 1 } } ) 

Effectuez un décompte de tous les enregistrements, générez un nombre aléatoire compris entre 0 et le nombre, puis procédez comme suit:

 db.yourCollection.find().limit(-1).skip(yourRandomNumber).next() 

Mise à jour pour MongoDB 3.2

3.2 introduit $ sample dans le pipeline d’agrégation.

Il y a aussi un bon article sur la mise en pratique.

Pour les anciennes versions (réponse précédente)

C’était en fait une demande de fonctionnalité: http://jira.mongodb.org/browse/SERVER-533 mais il a été classé sous “Won’t fix”.

Le livre de recettes a une très bonne recette pour sélectionner un document aléatoire dans une collection: http://cookbook.mongodb.org/patterns/random-atsortingbute/

Pour paraphraser la recette, vous atsortingbuez des numéros aléatoires à vos documents:

 db.docs.save( { key : 1, ..., random : Math.random() } ) 

Ensuite, sélectionnez un document aléatoire:

 rand = Math.random() result = db.docs.findOne( { key : 2, random : { $gte : rand } } ) if ( result == null ) { result = db.docs.findOne( { key : 2, random : { $lte : rand } } ) } 

Interroger à la fois avec $gte et $lte est nécessaire pour trouver le document avec un nombre aléatoire le plus proche de rand .

Et bien sûr, vous voudrez indexer sur le champ aléatoire:

 db.docs.ensureIndex( { key : 1, random :1 } ) 

Si vous interrogez déjà un index, déposez-le, ajoutez-y random: 1 et ajoutez-le à nouveau.

Vous pouvez également utiliser la fonction d’indexation géospatiale de MongoDB pour sélectionner les documents les plus proches d’un nombre aléatoire.

Tout d’abord, activez l’indexation géospatiale sur une collection:

 db.docs.ensureIndex( { random_point: '2d' } ) 

Pour créer un tas de documents avec des points aléatoires sur l’axe des X:

 for ( i = 0; i < 10; ++i ) { db.docs.insert( { key: i, random_point: [Math.random(), 0] } ); } 

Ensuite, vous pouvez obtenir un document aléatoire de la collection comme ceci:

 db.docs.findOne( { random_point : { $near : [Math.random(), 0] } } ) 

Ou vous pouvez récupérer plusieurs documents les plus proches d'un point aléatoire:

 db.docs.find( { random_point : { $near : [Math.random(), 0] } } ).limit( 4 ) 

Cela ne nécessite qu'une seule requête et aucune vérification NULL, et le code est propre, simple et flexible. Vous pouvez même utiliser l'axe Y du géopoint pour append une deuxième dimension aléatoire à votre requête.

La recette suivante est un peu plus lente que la solution mongo cookbook (append une clé aléatoire sur chaque document), mais retourne des documents aléatoires dissortingbués de manière plus uniforme. C’est une dissortingbution un peu moins uniforme que la solution de skip( random ) , mais beaucoup plus rapide et plus sûre en cas de suppression de documents.

 function draw(collection, query) { // query: mongodb query object (optional) var query = query || { }; query['random'] = { $lte: Math.random() }; var cur = collection.find(query).sort({ rand: -1 }); if (! cur.hasNext()) { delete query.random; cur = collection.find(query).sort({ rand: -1 }); } var doc = cur.next(); doc.random = Math.random(); collection.update({ _id: doc._id }, doc); return doc; } 

Il vous faut également append un champ “aléatoire” aléatoire à vos documents. N’oubliez donc pas de l’append lorsque vous les créez: vous devrez peut-être initialiser votre collection comme indiqué par Geoffrey.

 function addRandom(collection) { collection.find().forEach(function (obj) { obj.random = Math.random(); collection.save(obj); }); } db.eval(addRandom, db.things); 

Résultats de référence

Cette méthode est beaucoup plus rapide que la méthode skip() (de ceejayoz) et génère des documents plus uniformément aléatoires que la méthode “cookbook” rapscope par Michael:

Pour une collection avec 1 000 000 d’éléments:

  • Cette méthode prend moins d’une milliseconde sur ma machine

  • la méthode skip() prend en moyenne 180 ms

La méthode du livre de recettes empêchera de sélectionner un grand nombre de documents car leur nombre aléatoire ne les favorise pas.

  • Cette méthode permet de sélectionner tous les éléments de manière uniforme dans le temps.

  • Dans mon benchmark, il était seulement 30% plus lent que la méthode des livres de recettes.

  • le caractère aléatoire n’est pas parfait à 100% mais il est très bon (et peut être amélioré si nécessaire)

Cette recette n’est pas parfaite – la solution parfaite serait une fonctionnalité intégrée, comme d’autres l’ont noté.
Cependant, cela devrait être un bon compromis à plusieurs fins.

Voici un moyen d’utiliser les valeurs ObjectId par défaut pour _id et un peu de math et de logique.

 // Get the "min" and "max" timestamp values from the _id in the collection and the // diff between. // 4-bytes from a hex ssortingng is 8 characters var min = parseInt(db.collection.find() .sort({ "_id": 1 }).limit(1).toArray()[0]._id.str.substr(0,8),16)*1000, max = parseInt(db.collection.find() .sort({ "_id": -1 })limit(1).toArray()[0]._id.str.substr(0,8),16)*1000, diff = max - min; // Get a random value from diff and divide/multiply be 1000 for The "_id" precision: var random = Math.floor(Math.floor(Math.random(diff)*diff)/1000)*1000; // Use "random" in the range and pad the hex ssortingng to a valid ObjectId var _id = new ObjectId(((min + random)/1000).toSsortingng(16) + "0000000000000000") // Then query for the single document: var randomDoc = db.collection.find({ "_id": { "$gte": _id } }) .sort({ "_id": 1 }).limit(1).toArray()[0]; 

C’est la logique générale de la représentation de la shell et facilement adaptable.

Donc en points:

  • Recherchez les valeurs de clé primaire min et max dans la collection

  • Générez un nombre aléatoire compris entre les horodatages de ces documents.

  • Ajoutez le nombre aléatoire à la valeur minimale et recherchez le premier document supérieur ou égal à cette valeur.

Cela utilise “padding” de la valeur d’horodatage dans “hex” pour former une valeur ObjectId valide car c’est ce que nous recherchons. Utiliser des entiers comme valeur d’id est plus simple mais la même idée de base dans les points.

En Python en utilisant pymongo:

 import random def get_random_doc(): count = collection.count() return collection.find()[random.randrange(count)] 

il est difficile s’il n’y a pas de données à clé. quel est le champ _id? sont-ils des identifiants d’object mongodb? Si oui, vous pouvez obtenir les valeurs les plus élevées et les plus basses:

 lowest = db.coll.find().sort({_id:1}).limit(1).next()._id; highest = db.coll.find().sort({_id:-1}).limit(1).next()._id; 

alors si vous supposez que les identifiants sont uniformément dissortingbués (mais ils ne le sont pas, mais au moins c’est un début):

 unsigned long long L = first_8_bytes_of(lowest) unsigned long long H = first_8_bytes_of(highest) V = (H - L) * random_from_0_to_1(); N = L + V; oid = N concat random_4_bytes(); randomobj = db.coll.find({_id:{$gte:oid}}).limit(1); 

Vous pouvez choisir un horodatage aléatoire et rechercher le premier object créé par la suite. Il ne numérisera qu’un seul document, même si cela ne vous donnera pas nécessairement une dissortingbution uniforme.

 var randRec = function() { // replace with your collection var coll = db.collection // get unixtime of first and last record var min = coll.find().sort({_id: 1}).limit(1)[0]._id.getTimestamp() - 0; var max = coll.find().sort({_id: -1}).limit(1)[0]._id.getTimestamp() - 0; // allow to pass additional query params return function(query) { if (typeof query === 'undefined') query = {} var randTime = Math.round(Math.random() * (max - min)) + min; var hexSeconds = Math.floor(randTime / 1000).toSsortingng(16); var id = ObjectId(hexSeconds + "0000000000000000"); query._id = {$gte: id} return coll.find(query).limit(1) }; }(); 

Maintenant, vous pouvez utiliser l’agrégat. Exemple:

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

Voir le doc .

Ma solution sur php:

 /** * Get random docs from Mongo * @param $collection * @param $where * @param $fields * @param $limit * @author happy-code * @url happy-code.com */ private function _mongodb_get_random (MongoCollection $collection, $where = array(), $fields = array(), $limit = false) { // Total docs $count = $collection->find($where, $fields)->count(); if (!$limit) { // Get all docs $limit = $count; } $data = array(); for( $i = 0; $i < $limit; $i++ ) { // Skip documents $skip = rand(0, ($count-1) ); if ($skip !== 0) { $doc = $collection->find($where, $fields)->skip($skip)->limit(1)->getNext(); } else { $doc = $collection->find($where, $fields)->limit(1)->getNext(); } if (is_array($doc)) { // Catch document $data[ $doc['_id']->{'$id'} ] = $doc; // Ignore current document when making the next iteration $where['_id']['$nin'][] = $doc['_id']; } // Every iteration catch document and decrease in the total number of document $count--; } return $data; } 

Pour obtenir un nombre déterminé de documents aléatoires sans doublons:

  1. d’abord obtenir tous les identifiants
  2. obtenir la taille des documents
  3. loop geting random index et sauter dupliqué

     number_of_docs=7 db.collection('preguntas').find({},{_id:1}).toArray(function(err, arr) { count=arr.length idsram=[] rans=[] while(number_of_docs!=0){ var R = Math.floor(Math.random() * count); if (rans.indexOf(R) > -1) { continue } else { ans.push(R) idsram.push(arr[R]._id) number_of_docs-- } } db.collection('preguntas').find({}).toArray(function(err1, doc1) { if (err1) { console.log(err1); return; } res.send(doc1) }); }); 

Je suggère d’append un champ int aléatoire à chaque object. Alors vous pouvez juste faire un

 findOne({random_field: {$gte: rand()}}) 

pour choisir un document au hasard. Assurez-vous juste que vous indiquez Index ({random_field: 1})

Je suggérerais d’utiliser Map / Reduce, où vous utilisez la fonction de carte pour n’émettre que lorsqu’une valeur aléatoire est supérieure à une probabilité donnée.

 function mapf() { if(Math.random() <= probability) { emit(1, this); } } function reducef(key,values) { return {"documents": values}; } res = db.questions.mapReduce(mapf, reducef, {"out": {"inline": 1}, "scope": { "probability": 0.5}}); printjson(res.results); 

La fonction Reducef ci-dessus fonctionne car une seule clé («1») est émise par la fonction de carte.

La valeur de la "probabilité" est définie dans la "scope" lors de l'appel de mapRreduce (...)

Utiliser mapReduce comme cela devrait également être utilisable sur une firebase database fragmentée.

Si vous voulez sélectionner exactement n des m documents de la firebase database, vous pouvez le faire comme ceci:

 function mapf() { if(countSubset == 0) return; var prob = countSubset / countTotal; if(Math.random() <= prob) { emit(1, {"documents": [this]}); countSubset--; } countTotal--; } function reducef(key,values) { var newArray = new Array(); for(var i=0; i < values.length; i++) { newArray = newArray.concat(values[i].documents); } return {"documents": newArray}; } res = db.questions.mapReduce(mapf, reducef, {"out": {"inline": 1}, "scope": {"countTotal": 4, "countSubset": 2}}) printjson(res.results); 

Où "countTotal" (m) est le nombre de documents dans la firebase database, et "countSubset" (n) est le nombre de documents à récupérer.

Cette approche pourrait poser des problèmes sur les bases de données fragmentées.

Vous pouvez choisir random _id et renvoyer l’object correspondant:

  db.collection.count( function(err, count){ db.collection.distinct( "_id" , function( err, result) { if (err) res.send(err) var randomId = result[Math.floor(Math.random() * (count-1))] db.collection.findOne( { _id: randomId } , function( err, result) { if (err) res.send(err) console.log(result) }) }) }) 

Ici, vous n’avez pas besoin de dépenser de l’espace pour stocker des nombres aléatoires dans la collection.

En utilisant Python (pymongo), la fonction d’agrégat fonctionne également.

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

Cette approche est beaucoup plus rapide que l’exécution d’une requête pour un nombre aléatoire (par exemple, collection.find ([random_int]). Ceci est particulièrement le cas pour les grandes collections.

Lorsque j’ai été confronté à une solution similaire, j’ai fait marche arrière et j’ai constaté que la demande professionnelle consistait en fait à créer une forme de rotation de l’inventaire présenté. Dans ce cas, il y a de bien meilleures options, qui ont des réponses de moteurs de recherche comme Solr, pas des magasins de données comme MongoDB.

En résumé, avec l’exigence de “faire pivoter intelligemment” le contenu, nous devrions plutôt utiliser un modificateur personnel de score q au lieu d’un nombre aléatoire sur tous les documents. Pour l’implémenter vous-même, en supposant une petite population d’utilisateurs, vous pouvez stocker un document par utilisateur ayant le productId, le nombre d’impressions, le nombre de clics, la date de la dernière consultation et tous les autres facteurs jugés significatifs pour calculer le score aq. modificateur. Lors de la récupération de l’ensemble à afficher, vous demandez généralement plus de documents du magasin de données que demandé par l’utilisateur final, puis appliquez le modificateur de score q, prenez le nombre d’enregistrements demandés par l’utilisateur final, puis randomisez la page de résultats définir, il suffit donc de sortinger les documents dans la couche application (en mémoire).

Si l’univers des utilisateurs est trop important, vous pouvez classer les utilisateurs en groupes de comportements et indexer par groupe de comportements plutôt que par utilisateur.

Si l’univers des produits est suffisamment petit, vous pouvez créer un index par utilisateur.

J’ai trouvé cette technique beaucoup plus efficace, mais surtout plus efficace pour créer une expérience pertinente et utile d’utilisation de la solution logicielle.

non des solutions a bien fonctionné pour moi. surtout quand il y a beaucoup de lacunes et que le set est petit. cela a très bien fonctionné pour moi (en php):

 $count = $collection->count($search); $skip = mt_rand(0, $count - 1); $result = $collection->find($search)->skip($skip)->limit(1)->getNext(); 

Si vous avez une clé d’identification simple, vous pouvez stocker tous les identifiants dans un tableau, puis choisir un identifiant aléatoire. (Réponse Ruby):

 ids = @coll.find({},fields:{_id:1}).to_a @coll.find(ids.sample).first 

En utilisant Map / Reduce, vous pouvez certainement obtenir un enregistrement aléatoire, mais pas nécessairement très efficacement en fonction de la taille de la collection filtrée résultante avec laquelle vous travaillez.

J’ai testé cette méthode avec 50 000 documents (le filtre le réduit à environ 30 000) et il s’exécute en 400 ms environ sur un Intel i3 avec 16 Go de mémoire vive et un disque dur SATA3 …

 db.toc_content.mapReduce( /* map function */ function() { emit( 1, this._id ); }, /* reduce function */ function(k,v) { var r = Math.floor((Math.random()*v.length)); return v[r]; }, /* options */ { out: { inline: 1 }, /* Filter the collection to "A"ctive documents */ query: { status: "A" } } ); 

La fonction Map crée simplement un tableau des identifiants de tous les documents correspondant à la requête. Dans mon cas, j’ai testé avec environ 30 000 des 50 000 documents possibles.

La fonction Reduce sélectionne simplement un nombre entier aléatoire compris entre 0 et le nombre d’éléments (-1) dans le tableau, puis renvoie celui-ci dans le tableau.

400ms semble être une longue période, et si vous aviez cinquante millions d’enregistrements au lieu de cinquante mille, cela pourrait augmenter les coûts au sharepoint devenir inutilisables dans des situations multi-utilisateurs.

MongoDB a un problème ouvert pour inclure cette fonctionnalité dans le kernel … https://jira.mongodb.org/browse/SERVER-533

Si cette sélection “aléatoire” était intégrée à une recherche d’index au lieu de collecter des identifiants dans un tableau et d’en sélectionner un, cela aiderait incroyablement. (allez le voter!)

Cela fonctionne bien, c’est rapide, fonctionne avec plusieurs documents et ne nécessite pas de rand champ rand , qui finira par se remplir:

  1. append un index au champ .rand de votre collection
  2. utilisez find et refresh, quelque chose comme:
 // Install packages: // npm install mongodb async // Add index in mongo: // db.ensureIndex('mycollection', { rand: 1 }) var mongodb = require('mongodb') var async = require('async') // Find n random documents by using "rand" field. function findAndRefreshRand (collection, n, fields, done) { var result = [] var rand = Math.random() // Append documents to the result based on criteria and options, if options.limit is 0 skip the call. var appender = function (criteria, options, done) { return function (done) { if (options.limit > 0) { collection.find(criteria, fields, options).toArray( function (err, docs) { if (!err && Array.isArray(docs)) { Array.prototype.push.apply(result, docs) } done(err) } ) } else { async.nextTick(done) } } } async.series([ // Fetch docs with unitialized .rand. // NOTE: You can comment out this step if all docs have initialized .rand = Math.random() appender({ rand: { $exists: false } }, { limit: n - result.length }), // Fetch on one side of random number. appender({ rand: { $gte: rand } }, { sort: { rand: 1 }, limit: n - result.length }), // Continue fetch on the other side. appender({ rand: { $lt: rand } }, { sort: { rand: -1 }, limit: n - result.length }), // Refresh fetched docs, if any. function (done) { if (result.length > 0) { var batch = collection.initializeUnorderedBulkOp({ w: 0 }) for (var i = 0; i < result.length; ++i) { batch.find({ _id: result[i]._id }).updateOne({ rand: Math.random() }) } batch.execute(done) } else { async.nextTick(done) } } ], function (err) { done(err, result) }) } // Example usage mongodb.MongoClient.connect('mongodb://localhost:27017/core-development', function (err, db) { if (!err) { findAndRefreshRand(db.collection('profiles'), 1024, { _id: true, rand: true }, function (err, result) { if (!err) { console.log(result) } else { console.error(err) } db.close() }) } else { console.error(err) } }) 

ps. Comment trouver des enregistrements aléatoires dans la question mongodb est marqué comme duplicata de cette question. La différence réside dans le fait que cette question pose explicitement la question d’un enregistrement unique comme l’autre explicitement concernant l’obtention de documents aléatoires.

Si vous utilisez de la mongoose, vous pouvez utiliser de la mongoose-aléatoire

Si vous utilisez mongoid, le wrapper de document à object, vous pouvez effectuer les opérations suivantes dans Ruby. (En supposant que votre modèle est un utilisateur)

 User.all.to_a[rand(User.count)] 

Dans mon .irbrc, j’ai

 def rando klass klass.all.to_a[rand(klass.count)] end 

donc dans la console de rails, je peux faire, par exemple,

 rando User rando Article 

pour obtenir des documents au hasard à partir de n’importe quelle collection.

Ce qui fonctionne de manière efficace et fiable est le suivant:

Ajoutez un champ appelé “random” à chaque document et atsortingbuez-lui une valeur aléatoire, ajoutez un index pour le champ aléatoire et procédez comme suit:

Supposons que nous ayons une collection de liens Web appelés “liens” et que nous voulons un lien aléatoire:

 link = db.links.find().sort({random: 1}).limit(1)[0] 

Pour que le même lien ne s’affiche pas une seconde fois, mettez à jour son champ aléatoire avec un nouveau nombre aléatoire:

 db.links.update({random: Math.random()}, link)