Record casuale da MongoDB

Sto cercando di ottenere un record casuale da un enorme mongodb (100 milioni di dischi).

Qual è il modo più veloce ed efficace per farlo? I dati sono già lì e non ci sono campi in cui posso generare un numero casuale e ottenere una riga casuale.

Eventuali suggerimenti?

A partire dalla versione 3.2 di MongoDB, puoi ottenere N documenti casuali da una raccolta utilizzando l’operatore di pipeline di aggregazione $sample :

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

Esegui il conteggio di tutti i record, genera un numero casuale compreso tra 0 e il conteggio, quindi esegui:

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

Aggiornamento per MongoDB 3.2

3.2 ha introdotto $ sample alla pipeline di aggregazione.

C’è anche un buon post sul blog per metterlo in pratica.

Per versioni precedenti (risposta precedente)

Questa era in realtà una richiesta di funzionalità: http://jira.mongodb.org/browse/SERVER-533 ma è stata archiviata in “Non risolverà”.

Il ricettario ha un’ottima ricetta per selezionare un documento casuale da una raccolta: http://cookbook.mongodb.org/patterns/random-attribute/

Per parafrasare la ricetta, assegni numeri casuali ai tuoi documenti:

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

Quindi seleziona un documento casuale:

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

La ricerca con $gte e $lte è necessaria per trovare il documento con un numero casuale più vicino a rand .

E ovviamente vorrai indicizzare sul campo casuale:

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

Se stai già eseguendo una query su un indice, semplicemente trascinalo, aggiungi random: 1 a esso e aggiungilo di nuovo.

È inoltre ansible utilizzare la funzione di indicizzazione geospaziale di MongoDB per selezionare i documenti “più vicini” a un numero casuale.

Innanzitutto, abilita l’indicizzazione geospaziale su una raccolta:

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

Per creare un gruppo di documenti con punti casuali sull’asse X:

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

Quindi puoi ottenere un documento casuale dalla collezione in questo modo:

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

Oppure puoi recuperare diversi documenti più vicini a un punto casuale:

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

Ciò richiede solo una query e nessun controllo nullo, inoltre il codice è pulito, semplice e flessibile. È anche ansible utilizzare l'asse Y del geopoint per aggiungere una seconda dimensione di casualità alla query.

La seguente ricetta è un po ‘più lenta della soluzione del libro di ricette di mongo (aggiungi una chiave casuale su ogni documento), ma restituisce documenti casuali distribuiti in modo più uniforms. È un po ‘meno uniformsmente distribuito rispetto alla soluzione skip( random ) , ma molto più veloce e più sicuro nel caso in cui i documenti vengano rimossi.

 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; } 

Richiede inoltre di aggiungere un campo casuale “casuale” ai tuoi documenti, quindi non dimenticare di aggiungere questo quando li crei: potresti dover inizializzare la tua raccolta come mostrato da Geoffrey

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

Risultati del benchmark

Questo metodo è molto più veloce del metodo skip() (di ceejayoz) e genera documenti randomizzati più uniformi rispetto al metodo “cookbook” riportato da Michael:

Per una collezione con 1.000.000 di elementi:

  • Questo metodo richiede meno di un millisecondo sulla mia macchina

  • il metodo skip() richiede in media 180 ms

Il metodo del libro di ricette causerà un numero elevato di documenti che non verranno mai prelevati perché il loro numero casuale non li favorisce.

  • Questo metodo sceglierà tutti gli elementi in modo uniforms nel tempo.

  • Nel mio benchmark era solo il 30% più lento del metodo dei libri di cucina.

  • la casualità non è perfetta al 100% ma è molto buona (e può essere migliorata se necessario)

Questa ricetta non è perfetta – la soluzione perfetta sarebbe una funzionalità integrata come altri hanno notato.
Tuttavia dovrebbe essere un buon compromesso per molti scopi.

Ecco un modo utilizzando i valori ObjectId predefiniti per _id e un po ‘di matematica e logica.

 // 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]; 

Questa è la logica generale nella rappresentazione della shell e facilmente adattabile.

Quindi in punti:

  • Trova i valori min e max della chiave primaria nella raccolta

  • Genera un numero casuale che cade tra i timestamp di tali documenti.

  • Aggiungi il numero casuale al valore minimo e trova il primo documento che è maggiore o uguale a quel valore.

Questo utilizza “padding” dal valore di timestamp in “hex” per formare un valore ObjectId valido poiché questo è ciò che stiamo cercando. Usare gli interi come valore _id è essenzialmente più semplice ma la stessa idea di base nei punti.

In Python usando pymongo:

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

è difficile se non ci sono dati da cui chiudere. quali sono i campi _id? sono id di mongodb? In tal caso, potresti ottenere i valori più alti e più bassi:

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

quindi se si assume che gli ID siano distribuiti uniformsmente (ma non lo sono, ma almeno è un inizio):

 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); 

Puoi scegliere un timestamp casuale e cercare il primo object che è stato creato in seguito. Analizzerà solo un singolo documento, sebbene non fornisca necessariamente una distribuzione uniforms.

 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) }; }(); 

Ora puoi usare l’aggregato. Esempio:

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

Vedi il documento

La mia soluzione su 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; } 

Per ottenere un numero determinato di documenti casuali senza duplicati:

  1. prima prendi tutti gli id
  2. ottenere dimensioni dei documenti
  3. loop ottenendo un indice casuale e saltato duplicato

     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) }); }); 

Suggerirei di aggiungere un campo int casuale a ciascun object. Quindi puoi semplicemente fare a

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

scegliere un documento a caso. Assicurati solo di assicurarsiIndex ({random_field: 1})

Suggerirei di usare map / reduce, dove si usa la funzione map per emettere solo quando un valore casuale è superiore a una determinata probabilità.

 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 funzione reduce di sopra funziona perché solo una chiave ('1') viene emessa dalla funzione mappa.

Il valore della "probabilità" è definito nello "scope", quando si richiama mapRreduce (...)

Utilizzare mapReduce come questo dovrebbe essere utilizzabile anche su un db sharded.

Se vuoi selezionare esattamente n di m documenti dal db, puoi farlo in questo modo:

 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); 

Dove "countTotal" (m) è il numero di documenti nel db e "countSubset" (n) è il numero di documenti da recuperare.

Questo approccio potrebbe dare alcuni problemi sui database più complessi.

Puoi scegliere _id casuale e restituire l’object corrispondente:

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

Qui non è necessario spendere spazio per la memorizzazione di numeri casuali nella raccolta.

Utilizzando Python (pymongo), funziona anche la funzione di aggregazione.

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

Questo approccio è molto più rapido rispetto all’esecuzione di una query per un numero casuale (ad es. Collection.find ([random_int]). Ciò vale in particolare per le raccolte di grandi dimensioni.

Quando mi trovavo di fronte a una soluzione simile, tornai indietro e scoprii che la richiesta commerciale era in realtà per creare una qualche forma di rotazione dell’inventario presentato. In tal caso, ci sono opzioni molto migliori, che hanno risposte dai motori di ricerca come Solr, non da archivi dati come MongoDB.

In breve, con l’obbligo di “ruotare in modo intelligente” il contenuto, ciò che dovremmo fare invece di un numero casuale su tutti i documenti è includere un modificatore personale del punteggio q. Per implementare questo da soli, supponendo una piccola popolazione di utenti, è ansible memorizzare un documento per utente che ha l’ID prodotto, il conteggio delle impressioni, il conteggio click-through, l’ultima data vista e qualsiasi altro fattore che l’azienda ritiene significativo per calcolare il punteggio aq modificatore. Quando si recupera il set da visualizzare, in genere si richiedono più documenti dall’archivio dati rispetto a quelli richiesti dall’utente finale, quindi si applica il modificatore del punteggio q, si prende il numero di record richiesti dall’utente finale, quindi si randomizza la pagina dei risultati, un piccolo impostare, in modo semplice, ordinare i documenti nel livello applicazione (in memoria).

Se l’universo degli utenti è troppo grande, è ansible classificare gli utenti in gruppi di comportamento e indicizzare in base al gruppo di comportamento anziché all’utente.

Se l’universo dei prodotti è abbastanza piccolo, è ansible creare un indice per utente.

Ho trovato questa tecnica molto più efficiente, ma soprattutto più efficace nel creare un’esperienza pertinente e valida di utilizzo della soluzione software.

nessuna delle soluzioni ha funzionato bene per me. specialmente quando ci sono molte lacune e il set è piccolo. questo ha funzionato molto bene per me (in PHP):

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

Se si dispone di una semplice chiave ID, è ansible memorizzare tutti gli ID in una matrice, quindi selezionare un ID casuale. (Risposta ruby):

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

Usando Map / Reduce, puoi certamente ottenere un record casuale, ma non necessariamente in modo molto efficiente a seconda delle dimensioni della collezione filtrata risultante con cui lavori.

Ho testato questo metodo con 50.000 documenti (il filtro lo riduce a circa 30.000), e viene eseguito in circa 400ms su un Intel i3 con RAM da 16 GB e un disco rigido 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 funzione Mappa crea semplicemente una serie di ID di tutti i documenti che corrispondono alla query. Nel mio caso l’ho testato con circa 30.000 dei 50.000 documenti possibili.

La funzione Riduci semplicemente seleziona un numero intero casuale compreso tra 0 e il numero di elementi (-1) nell’array, quindi restituisce _id dall’array.

400 ms suona come un tempo lungo, e lo è davvero, se hai cinquanta milioni di record invece di cinquantamila, questo può aumentare il sovraccarico fino al punto in cui diventa inutilizzabile in situazioni multiutente.

C’è un problema aperto per MongoDB per includere questa funzionalità nel core … https://jira.mongodb.org/browse/SERVER-533

Se questa selezione “casuale” è stata incorporata in una ricerca di indice invece di raccogliere id in un array e quindi selezionarne uno, ciò sarebbe di grande aiuto. (vai a votare!)

Funziona bene, è veloce, funziona con più documenti e non richiede la rand campo rand , che alla fine si popolerà da solo:

  1. aggiungi l’indice al campo .rand della tua raccolta
  2. usa trova e aggiorna, qualcosa come:
 // 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. Come trovare i record casuali nella domanda di mongodb è contrassegnato come duplicato di questa domanda. La differenza è che questa domanda chiede esplicitamente sul singolo record come l'altro esplicitamente sull'ottenere documenti casuali.

Se si utilizza mangusta, è ansible utilizzare mangusta-casuale mangusta-casuale

Se stai usando mongoid, il wrapper document-to-object, puoi fare quanto segue in Ruby. (Supponendo che il tuo modello sia Utente)

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

Nel mio .irbrc, ho

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

così nella console di rails, posso fare, per esempio,

 rando User rando Article 

per ottenere documenti in modo casuale da qualsiasi raccolta.

Ciò che funziona in modo efficiente e affidabile è questo:

Aggiungi un campo chiamato “random” a ciascun documento e assegna un valore casuale ad esso, aggiungi un indice per il campo casuale e procedi come segue:

Supponiamo di avere una raccolta di collegamenti Web denominata “link” e vogliamo un collegamento casuale da esso:

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

Per garantire che lo stesso collegamento non venga visualizzato una seconda volta, aggiorna il suo campo casuale con un nuovo numero casuale:

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