451 votes

Enregistrement aléatoire de MongoDB

Je cherche à obtenir un enregistrement aléatoire à partir d'une énorme collection (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 numéro aléatoire et obtenir une ligne aléatoire.

2 votes

Voir aussi SO question intitulée "Ordonner un ensemble de résultats de façon aléatoire dans mongo". . La réflexion sur le classement aléatoire d'un ensemble de résultats est une version plus générale de cette question, plus puissante et plus utile.

14 votes

Cette question revient sans cesse. Les dernières informations peuvent probablement être trouvées sur le site demande de fonctionnalité pour obtenir des éléments aléatoires d'une collection dans le ticket tracker de MongoDB. Si elle était implémentée en mode natif, ce serait probablement l'option la plus efficace. (Si vous voulez cette fonctionnalité, allez voter pour elle).

0 votes

Est-ce une collection sharded ?

125voto

ceejayoz Points 85962

Effectuer un comptage de tous les enregistrements, générer un nombre aléatoire entre 0 et le comptage, puis faire :

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

167 votes

Malheureusement, la fonction skip() est plutôt inefficace puisqu'elle doit analyser autant de documents. De plus, il y a une condition de course si des lignes sont supprimées entre l'obtention du compte et l'exécution de la requête.

8 votes

Notez que le nombre aléatoire doit être compris entre 0 et le nombre (exclusif). Par exemple, si vous avez 10 éléments, le nombre aléatoire doit être compris entre 0 et 9. Sinon, le curseur pourrait essayer de sauter le dernier élément, et rien ne serait renvoyé.

4 votes

Merci, cela a parfaitement fonctionné pour mes besoins. @mstearn, vos commentaires sur l'efficacité et les conditions de course sont valables, mais pour les collections où aucun des deux n'a d'importance (extraction unique par lot côté serveur dans une collection où les enregistrements ne sont pas supprimés), cette solution est largement supérieure à la solution bidon (IMO) du Mongo Cookbook.

94voto

Michael Points 445

Mise à jour pour MongoDB 3.2

3.2 introduit $sample au pipeline d'agrégation.

Il y a aussi un bon article de blog sur sa mise en pratique.

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

Il s'agissait en fait d'une demande de fonctionnalité : http://jira.mongodb.org/browse/SERVER-533 mais c'était classé dans la catégorie "Ne pas réparer".

Le livre de cuisine propose une très bonne recette pour sélectionner un document au hasard dans une collection : http://cookbook.mongodb.org/patterns/random-attribute/

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

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

Sélectionnez ensuite un document au hasard :

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

Interrogation avec les deux $gte y $lte est nécessaire pour trouver le document avec un numéro aléatoire le plus proche. rand .

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

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

Si vous effectuez déjà des requêtes dans un index, il suffit de l'abandonner, d'ajouter random: 1 et l'ajouter à nouveau.

7 votes

Et voici un moyen simple d'ajouter le champ aléatoire à chaque document de la collection. function setRandom() { db.topics.find().forEach(function (obj) {obj.random = Math.random();db.topics.save(obj);}) ; } db.eval(setRandom) ;

0 votes

La demande de fonctionnalité a été rouverte, mais n'est pas encore programmée.

9 votes

Cette méthode sélectionne un document au hasard, mais si vous le faites plusieurs fois, les recherches ne sont pas indépendantes. Vous avez plus de chances d'obtenir le même document deux fois de suite que le hasard ne le voudrait.

57voto

Nico de Poel Points 404

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 ensemble 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] } } )

Vous pouvez également 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 de nullité, et le code est propre, simple et flexible. Vous pourriez même utiliser l'axe Y du géopoint pour ajouter une deuxième dimension aléatoire à votre requête.

8 votes

J'aime cette réponse, c'est la plus efficace que j'ai vue qui ne nécessite pas un tas de manipulations côté serveur.

4 votes

Elle est également biaisée par les documents qui ont peu de points dans leur voisinage.

6 votes

C'est vrai, et il y a aussi d'autres problèmes : les documents sont fortement corrélés sur leurs clés aléatoires, donc il est très prévisible quels documents seront retournés en tant que groupe si vous sélectionnez plusieurs documents. En outre, les documents proches des limites (0 et 1) ont moins de chances d'être choisis. Ce dernier point pourrait être résolu en utilisant un géocartage sphérique, qui s'enroule autour des bords. Cependant, vous devez considérer cette réponse comme une version améliorée de la recette du livre de cuisine, et non comme un mécanisme de sélection aléatoire parfait. Il est suffisamment aléatoire pour la plupart des besoins.

21voto

spam_eggs Points 847

La recette suivante est un peu plus lente que la solution mongo cookbook (ajouter une clé aléatoire sur chaque document), mais renvoie des documents aléatoires plus uniformément distribués. La distribution est un peu moins régulière que celle de la solution 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 demande également d'ajouter un champ "aléatoire" à vos documents, n'oubliez pas de l'ajouter 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 l'évaluation comparative

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" rapportée par Michael :

Pour une collection de 1.000.000 d'éléments :

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

  • le site skip() La méthode prend 180 ms en moyenne

Avec la méthode du livre de cuisine, un grand nombre de documents ne seront jamais sélectionnés parce que leur numéro aléatoire ne les favorise pas.

  • Cette méthode permet de prélever tous les éléments de manière uniforme au fil du temps.

  • Dans mon évaluation, elle n'était que 30 % plus lente que la méthode du livre de recettes.

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

Cette recette n'est pas parfaite - la solution idéale serait une fonction intégrée, comme d'autres l'ont fait remarquer.
Cependant, il devrait constituer un bon compromis pour de nombreux usages.

1voto

mstearn Points 2295

Je suggère d'ajouter un champ int aléatoire à chaque objet. Ensuite, vous pouvez simplement faire un

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

pour choisir un document au hasard. Assurez-vous juste que vous assurezIndex({champ_aléatoire:1})

2 votes

Si le premier enregistrement de votre collection a une valeur de random_field relativement élevée, ne sera-t-il pas retourné presque tout le temps ?

2 votes

Lehaitus est correct, il sera il n'est pas approprié pour n'importe quel but

7 votes

Cette solution est complètement fausse, l'ajout d'un nombre aléatoire (imaginons entre 0 et 2^32-1) ne garantit pas une bonne distribution et l'utilisation de $gte rend les choses encore pires, car votre sélection aléatoire ne sera même pas proche d'un nombre pseudo-aléatoire. Je suggère de ne jamais utiliser ce concept.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X