Registro aleatorio de MongoDB

Estoy buscando obtener un registro aleatorio de un mongodb enorme (100 millones de registros).

¿Cuál es la forma más rápida y eficiente de hacerlo? Los datos ya están allí y no hay ningún campo en el que pueda generar un número aleatorio y obtener una fila aleatoria.

¿Alguna sugerencia?

Comenzando con la versión 3.2 de MongoDB, puede obtener N documentos aleatorios de una colección usando el operador de canalización de agregación $sample :

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

Haga un recuento de todos los registros, genere un número aleatorio entre 0 y el recuento, y luego haga:

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

Actualización para MongoDB 3.2

3.2 introdujo $ muestra en la canalización de agregación.

También hay una buena publicación de blog sobre cómo ponerla en práctica.

Para versiones anteriores (respuesta anterior)

Esta fue en realidad una solicitud de función: http://jira.mongodb.org/browse/SERVER-533 pero se archivó en “No se solucionará”.

El libro de cocina tiene una muy buena receta para seleccionar un documento aleatorio de una colección: http://cookbook.mongodb.org/patterns/random-attribute/

Parafraseando la receta, asigna números aleatorios a sus documentos:

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

Luego selecciona un documento aleatorio:

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

Se necesita consultar con $gte y $lte para encontrar el documento con un número aleatorio más cercano al rand .

Y, por supuesto, querrá indexar en el campo aleatorio:

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

Si ya está consultando un índice, simplemente suéltelo, agregue random: 1 a él y agréguelo nuevamente.

También puede usar la función de indexación geoespacial de MongoDB para seleccionar los documentos ‘más cercanos’ a un número aleatorio.

Primero, habilite la indexación geoespacial en una colección:

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

Para crear un grupo de documentos con puntos aleatorios en el eje X:

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

Entonces puede obtener un documento al azar de la colección como este:

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

O puede recuperar varios documentos más cercanos a un punto aleatorio:

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

Esto requiere solo una consulta y ninguna comprobación nula, además el código es limpio, simple y flexible. Incluso podría usar el eje Y del punto geográfico para agregar una segunda dimensión de aleatoriedad a su consulta.

La siguiente receta es un poco más lenta que la solución de libro de cocina de mongo (agregue una clave aleatoria en cada documento), pero devuelve documentos aleatorios distribuidos de manera más uniforme. Está un poco menos distribuido de forma uniforme que la solución skip( random ) , pero es mucho más rápido y más seguro ante fallas en caso de que se eliminen los documentos.

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

También requiere que agregue un campo aleatorio “aleatorio” a sus documentos, así que no olvide agregar esto cuando los cree: es posible que deba inicializar su colección como lo muestra Geoffrey.

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

Resultados de referencia

Este método es mucho más rápido que el método skip() (de ceejayoz) y genera documentos más uniformemente aleatorios que el método de “libro de cocina” informado por Michael:

Para una colección con 1,000,000 de elementos:

  • Este método lleva menos de un milisegundo en mi máquina

  • El método skip() tarda 180 ms en promedio

El método del libro de recetas hará que nunca se recoja una gran cantidad de documentos porque su número aleatorio no los favorece.

  • Este método seleccionará todos los elementos de manera uniforme a lo largo del tiempo.

  • En mi punto de referencia, fue solo un 30% más lento que el método del libro de cocina.

  • la aleatoriedad no es 100% perfecta, pero es muy buena (y puede mejorarse si es necesario)

Esta receta no es perfecta: la solución perfecta sería una característica incorporada, como otros han notado.
Sin embargo, debería ser un buen compromiso para muchos propósitos.

Aquí hay una manera de usar los valores predeterminados de ObjectId para _id y un poco de matemática y lógica.

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

Esa es la lógica general en la representación de shell y fácilmente adaptable.

Entonces en puntos:

  • Encuentre los valores de la clave primaria mínima y máxima en la colección

  • Genere un número aleatorio que se encuentre entre las marcas de tiempo de esos documentos.

  • Agregue el número aleatorio al valor mínimo y busque el primer documento que sea mayor o igual que ese valor.

Esto usa “relleno” del valor de la marca de tiempo en “hex” para formar un valor válido de ObjectId ya que eso es lo que estamos buscando. El uso de enteros como el valor _id es esencialmente más simple pero la misma idea básica en los puntos.

En Python usando pymongo:

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

es difícil si no hay datos allí para desactivarlos. ¿Cuáles son el campo _id? ¿Son id de objeto mongodb? Si es así, puedes obtener los valores más altos y más bajos:

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

luego si asumes que los id están distribuidos uniformemente (pero no lo son, pero al menos es un comienzo):

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

Puede elegir una marca de tiempo aleatoria y buscar el primer objeto que se creó después. Solo escaneará un solo documento, aunque no necesariamente le dará una distribución 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) }; }(); 

Ahora puedes usar el agregado. Ejemplo:

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

Ver el documento .

Mi solución en 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; } 

Para obtener un número determinado de documentos aleatorios sin duplicados:

  1. primero obtener todos los identificadores
  2. obtener el tamaño de los documentos
  3. bucle que genera índice aleatorio y salto duplicado

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

Sugeriría agregar un campo entero al azar a cada objeto. Entonces puedes hacer un

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

para elegir un documento al azar Solo asegúrate de asegurar el índice ({random_field: 1})

Sugeriría utilizar map / reduce, donde usa la función de mapa para emitir solo cuando un valor aleatorio está por encima de una probabilidad dada.

 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 función reducef anterior funciona porque solo se emite una tecla ('1') de la función del mapa.

El valor de la "probabilidad" se define en el "scope", al invocar mapRreduce (...)

Usar mapReduce de esta manera también se puede usar en un db fragmentado.

Si desea seleccionar exactamente n de m documentos de la base de datos, puede hacerlo así:

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

Donde "countTotal" (m) es el número de documentos en el db, y "countSubset" (n) es la cantidad de documentos que se recuperarán.

Este enfoque podría dar algunos problemas en las bases de datos fragmentadas.

Puede seleccionar _id aleatorio y devolver el objeto correspondiente:

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

Aquí no necesita gastar espacio en almacenar números aleatorios en la colección.

Usando Python (pymongo), la función agregada también funciona.

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

Este enfoque es mucho más rápido que ejecutar una consulta para un número aleatorio (por ejemplo, collection.find ([random_int]). Este es especialmente el caso para colecciones grandes.

Cuando me encontré con una solución similar, volví atrás y descubrí que la solicitud comercial era en realidad para crear algún tipo de rotación del inventario que se presentaba. En ese caso, hay opciones mucho mejores, que tienen respuestas de motores de búsqueda como Solr, no de tiendas de datos como MongoDB.

En resumen, con el requisito de “girar de forma inteligente” el contenido, lo que deberíamos hacer en lugar de un número aleatorio en todos los documentos es incluir un modificador personal de puntuación q. Para implementarlo usted mismo, suponiendo una pequeña población de usuarios, puede almacenar un documento por usuario que tenga la IdProducto, el recuento de impresiones, el recuento de clics, la última fecha de visualización y cualquier otro factor que la empresa considere significativo para calcular la puntuación de ac. modificador Cuando recupera el conjunto para mostrar, normalmente solicita más documentos del almacén de datos que los solicitados por el usuario final, luego aplica el modificador de puntaje q, toma el número de registros solicitados por el usuario final y luego aleatoriza la página de resultados, un pequeño establecer, simplemente ordena los documentos en la capa de aplicación (en la memoria).

Si el universo de usuarios es demasiado grande, puede categorizar a los usuarios en grupos de comportamiento e indexar por grupo de comportamiento en lugar de por usuario.

Si el universo de productos es lo suficientemente pequeño, puede crear un índice por usuario.

He encontrado que esta técnica es mucho más eficiente, pero más importante, más efectiva en la creación de una experiencia relevante y valiosa para usar la solución de software.

ninguna de las soluciones funcionó bien para mí. especialmente cuando hay muchas lagunas y el conjunto es pequeño. esto funcionó muy bien para mí (en php):

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

Si tiene una clave de identificación simple, puede almacenar todas las identificaciones en una matriz y luego elegir una identificación aleatoria. (Respuesta de Ruby):

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

Usando Map / Reduce, ciertamente puede obtener un registro aleatorio, aunque no necesariamente de manera muy eficiente, dependiendo del tamaño de la colección filtrada resultante con la que termine trabajando.

Probé este método con 50,000 documentos (el filtro lo reduce a unos 30,000) y se ejecuta en aproximadamente 400ms en un Intel i3 con 16GB de ram y un disco duro 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 función de mapa simplemente crea una matriz de id de todos los documentos que coinciden con la consulta. En mi caso, probé esto con aproximadamente 30,000 de los 50,000 documentos posibles.

La función Reducir simplemente selecciona un entero aleatorio entre 0 y la cantidad de elementos (-1) en la matriz, y luego devuelve ese _id de la matriz.

400ms suena como un largo tiempo, y realmente lo es, si tiene cincuenta millones de registros en lugar de cincuenta mil, esto puede boost la sobrecarga hasta el punto en que se vuelve inutilizable en situaciones de múltiples usuarios.

Hay un problema abierto para que MongoDB incluya esta característica en el núcleo … https://jira.mongodb.org/browse/SERVER-533

Si esta selección “aleatoria” se construyó en una búsqueda de índice en lugar de recostackr identificadores en una matriz y luego seleccionar uno, esto ayudaría increíblemente. (¡vótelo!)

Esto funciona bien, es rápido, funciona con múltiples documentos y no requiere llenar un campo rand , que eventualmente se rellenará a sí mismo:

  1. agregue índice al campo .rand en su colección
  2. use buscar y actualizar, algo como:
 // 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) } }) 

PD. Cómo encontrar registros aleatorios en la pregunta de mongodb se marca como duplicado de esta pregunta. La diferencia es que esta pregunta pregunta explícitamente sobre el registro único como el otro explícitamente sobre la obtención de documentos aleatorios.

Si está usando mongoose, puede usar mongoose-azarosa mongoose-al azar

Si usa mongoid, el contenedor de documento a objeto, puede hacer lo siguiente en Ruby. (Suponiendo que su modelo es Usuario)

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

En mi .irbrc, tengo

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

entonces en la consola de Rails, puedo hacer, por ejemplo,

 rando User rando Article 

para obtener documentos al azar de cualquier colección.

Lo que funciona de manera eficiente y confiable es esto:

Agregue un campo llamado “aleatorio” a cada documento y asígnele un valor aleatorio, agregue un índice para el campo aleatorio y proceda de la siguiente manera:

Supongamos que tenemos una colección de enlaces web llamados “enlaces” y queremos un enlace aleatorio a partir de ella:

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

Para asegurarse de que el mismo enlace no aparezca una segunda vez, actualice su campo aleatorio con un nuevo número aleatorio:

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