Enregistrement aléatoire de MongoDB

je suis à la recherche pour obtenir un enregistrement aléatoire à partir d'une énorme (100 millions d'enregistrements) mongodb .

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 aucun champ dans lequel je peux générer un nombre aléatoire et obtenir une rangée aléatoire.

des suggestions?

265
demandé sur Hassaan 2010-05-13 06:43:16

24 réponses

à partir de la version 3.2 de MongoDB, vous pouvez obtenir N docs aléatoires d'une collection en utilisant le $sample opérateur de pipeline d'agrégation:

// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])
165
répondu JohnnyHK 2018-10-04 15:55:43

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

db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
114
répondu ceejayoz 2014-02-16 02:25:10

mise à jour pour MongoDB 3.2

3.2 introduit $ échantillon dans le pipeline d'agrégation.

Il ya aussi un bon blog post sur la mise en pratique.

pour les versions plus anciennes (réponse précédente)

il s'agissait en fait d'une requête de caractéristique: http://jira.mongodb.org/browse/SERVER-533 mais il a été classé sous " ne sera pas fixer."

le livre de recettes a 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 assignez des nombres aléatoires à vos documents:

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

puis sélectionner 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 } } )
}

avec à la fois $gte et $lte est nécessaire pour trouver le document avec un nombre 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 posez déjà des questions sur un indice, il suffit de le laisser tomber, d'y ajouter random: 1 et de l'ajouter à nouveau.

82
répondu Michael 2016-10-02 16:15:19

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.

d'abord, activer 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] } );
}

alors 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 le plus proche d'un point aléatoire:

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

cela nécessite une seule requête et pas de vérifications null, plus le code est propre, simple et flexible. Vous pouvez même utiliser l'axe des Y du géopoint pour ajouter une deuxième dimension d'aléatoire à votre requête.

54
répondu Nico de Poel 2012-02-29 12:50:05

la recette suivante est un peu plus lente que la Mongo cookbook solution (ajouter une touche aléatoire sur chaque document), mais renvoie des documents aléatoires distribués plus uniformément. Il est un peu moins uniformément distribué que la solution skip( random ) , mais beaucoup plus rapide et plus sûr en cas de documents sont supprimés.

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 aussi d'ajouter un champ aléatoire "aléatoire" à vos documents donc n'oubliez pas d'ajouter ceci 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);

les résultats d'un Benchmark

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

pour une collection de 1.000.000 éléments:

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

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

la méthode du livre de cuisine provoquera un grand nombre de documents pour ne jamais être choisi parce que leur nombre aléatoire ne les favorise pas.

  • cette méthode permet de choisir tous les éléments de façon uniforme dans le temps.

  • dans mon benchmark, il était seulement 30% plus lent que la méthode du livre de cuisine.

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

Cette recette n'est pas parfait - la solution parfaite serait une fonctionnalité intégrée comme d'autres l'ont noté.

Toutefois, il devrait s'agir d'un bon compromis à bien des égards.

19
répondu spam_eggs 2015-06-11 16:14:08

voici un moyen d'utiliser les valeurs par défaut ObjectId pour _id et un peu de mathématiques 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 string 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 string to a valid ObjectId
var _id = new ObjectId(((min + random)/1000).toString(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 shell et facilement adaptable.

Donc, dans les points:

  • Trouvez les valeurs clés primaires min et max dans la collection

  • Générer un nombre aléatoire qui tombe entre les horodatages de ces documents.

  • Ajouter le nombre aléatoire à la valeur minimale et de trouver le premier document qui est supérieur ou égal à cette valeur.

cela utilise le "rembourrage" de la valeur horodatée dans "hex" pour former une valeur valide ObjectId puisque c'est ce que nous recherchons. Utiliser des entiers comme la valeur _id est essentiellement plus simple, mais la même idée de base dans le point.

7
répondu Blakes Seven 2015-06-26 11:06:04

en Python en utilisant pymongo:

import random

def get_random_doc():
    count = collection.count()
    return collection.find()[random.randrange(count)]
6
répondu Jabba 2015-01-24 14:38:26

c'est difficile si il n'y a pas de données de clés. quelles sont les champ _id? ce sont des objets d'identité mongodb? Si c'est le cas, 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 id sont uniformément répartis (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);
5
répondu dm. 2010-05-13 13:48:41

vous pouvez choisir une date aléatoire et rechercher le premier objet qui a été créé par la suite. Il ne numérisera qu'un seul document, bien qu'il ne vous donne pas nécessairement une distribution 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).toString(16);
        var id = ObjectId(hexSeconds + "0000000000000000");
        query._id = {$gte: id}
        return coll.find(query).limit(1)
    };
}();
5
répondu Martin Nowak 2014-12-04 23:53:26

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

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

Voir doc .

5
répondu dbam 2017-02-06 17:00:45

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;
}
3
répondu code_turist 2014-12-23 17:29:22

afin d'obtenir un nombre déterminé de docs aléatoires sans duplicata:

  1. de la première à obtenir tous les id
  2. d'obtenir la taille des documents
  3. boucle de geting aléatoire index et passez 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)
                });
            });
    
3
répondu Fabio Guerra 2016-03-02 19:09:42

je suggère d'ajouter un champ int aléatoire à chaque objet. Alors vous pouvez juste faire un

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

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

2
répondu mstearn 2012-05-11 21:39:33

je suggère d'utiliser map/reduce, où vous utilisez la fonction map pour émettre seulement quand une valeur aléatoire est au-dessus d'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 parce qu'une seule clé ('1') est émise par la fonction map.

la valeur de la" probabilité "est définie dans la" portée", en invoquant mapRreduce(...)

en utilisant mapReduce comme ceci devrait également être utilisable sur un db partagé.

si vous voulez sélectionner exactement n de m documents à partir de la base de données, 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 le db, et" countSubset " (n) est le nombre de documents à récupérer.

cette approche pourrait poser des problèmes sur les bases de données partagées.

2
répondu torbenl 2014-02-06 11:01:49

vous pouvez choisir random _id et retourner l'objet 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 sur le stockage des nombres aléatoires dans la collecte.

2
répondu Vijay13 2015-04-30 04:24:13

en utilisant Python (pymongo), la fonction aggregate fonctionne aussi.

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.trouver([random_int]). C'est particulièrement le cas pour les grandes collections.

2
répondu Daniel 2018-04-17 14:37:24

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

en bref, avec l'exigence de "tourner intelligemment" le contenu, ce que nous devrions faire au lieu d'un nombre aléatoire à travers tous les documents est d'inclure un modificateur du score q personnel. Pour mettre en œuvre cela vous-même, en supposant une petite population d'utilisateurs, vous pouvez stocker un document par utilisateur qui a le productId, impression count, click-through count, last seen date, et tous les autres facteurs que l'entreprise trouve comme étant significatif de calculer un Q score modificateur. Lors de la récupération de l'ensemble à afficher, généralement vous demandez plus de documents à partir de la mémoire de données que demandé par l'utilisateur final, puis appliquer le modificateur de score q, prendre le nombre d'enregistrements demandé par l'utilisateur final, puis de randomiser la page des résultats, un ensemble minuscule, donc simplement trier les documents dans la couche application (en mémoire).

si l'univers des utilisateurs est trop grand, vous pouvez catégoriser les utilisateurs en groupes de comportement et l'index par groupe de comportement plutôt que l'utilisateur.

Si l'univers de produits est assez petit, vous pouvez créer un index par l'utilisateur.

j'ai trouvé cette technique pour être beaucoup plus efficace, mais plus surtout plus efficace dans la création d'une expérience pertinente et valable de l'utilisation de la solution logicielle.

1
répondu paegun 2013-09-11 16:32:43

aucune des solutions n'a bien fonctionné pour moi. surtout quand il y a beaucoup de lacunes et que l'ensemble 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();
1
répondu Mantas Karanauskas 2014-01-21 18:07:44

si vous avez une clé d'identification simple, vous pouvez stocker tous les id dans un tableau, puis choisir un id aléatoire. (Réponse de Ruby):

ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
0
répondu Mr. Demetrius Michael 2013-03-19 14:10:47

en utilisant Map / Reduce, vous pouvez certainement obtenir un enregistrement aléatoire, juste pas nécessairement très efficacement en fonction de la taille de la collection filtrée résultant que vous finissez par travailler avec.

j'ai testé cette méthode avec 50 000 documents (le filtre la réduit à environ 30 000), et elle s'exécute dans environ 400ms sur un Intel i3 avec 16 Go de ram et un HDD 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 de l'identité de tous les documents qui correspondent à la requête. Dans mon cas, j'ai testé ceci avec environ 30.000 sur les 50.000 documents possibles.

la fonction Reduce choisit simplement un entier aléatoire entre 0 et le nombre d'articles (-1) dans le tableau, puis renvoie que _id du tableau.

400ms semble comme un long moment, et il est vraiment, si vous aviez cinquante millions de disques au lieu de cinquante mille, ce mai augmentez les frais généraux jusqu'à ce qu'ils deviennent inutilisables dans les situations où il y a plusieurs utilisateurs.

il y a une question ouverte pour MongoDB pour inclure cette caractéristique dans le noyau... https://jira.mongodb.org/browse/SERVER-533

si cette sélection" aléatoire " était intégrée dans un index-lookup au lieu de collecter des ID dans un tableau et d'en sélectionner un, cela serait incroyablement utile. (allez voter!)

0
répondu doublehelix 2014-01-29 23:26:46

cela fonctionne bien, c'est rapide, Fonctionne avec plusieurs documents et ne nécessite pas de remplir le champ rand , qui finira par se remplir lui-même:

  1. ajouter l'index à .rand champ 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 en mongodb la question est marquée comme une copie de cette question. Le la différence est que cette question pose explicitement au sujet de l'enregistrement simple comme l'autre explicitement au sujet d'obtenir le document aléatoire s .

0
répondu Mirek Rusin 2017-05-23 12:26:38

si vous utilisez Mangoose, vous pouvez utiliser Mangoose-random Mangoose-random

0
répondu codersaif 2016-05-30 07:18:08

Si vous utilisez des mongoid, le document-objet wrapper, vous pouvez effectuer les opérations suivantes Rubis. (En supposant que votre modèle est 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 rail, je peux faire, par exemple,

rando User
rando Article

pour obtenir des documents au hasard dans n'importe quelle collection.

-2
répondu Zack Xu 2013-12-06 12:31:39

ce qui fonctionne de manière efficace et fiable est ceci:

Ajouter un champ appelé "aléatoire" de chaque document et d'attribuer une valeur aléatoire, ajouter un index pour le champ aléatoire et procédez comme suit:

supposons que nous ayons une collection de Liens web appelés "links" et que nous en voulons un lien aléatoire:

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

pour s'assurer que le même lien n'apparaîtra pas une deuxième fois, mettez à jour son champ aléatoire avec un nouveau nombre aléatoire:

db.links.update({random: Math.random()}, link)
-7
répondu trainwreck 2011-03-31 13:15:12