tutorial - Registro aleatorio de MongoDB
mongodb university (24)
Estoy buscando obtener un registro aleatorio de un enorme mongodb
(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?
Actualización para MongoDB 3.2
3.2 introdujo $sample a la tubería de agregación.
También hay una buena publicación en el blog para ponerlo en práctica.
Para versiones anteriores (respuesta anterior)
Esto 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 al azar 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 al azar:
rand = Math.random()
result = db.docs.findOne( { key : 2, random : { $gte : rand } } )
if ( result == null ) {
result = db.docs.findOne( { key : 2, random : { $lte : rand } } )
}
Es necesario consultar tanto con $gte
como con $lte
para encontrar el documento con un número aleatorio más cercano a rand
.
Y, por supuesto, querrá indexar en el campo aleatorio:
db.docs.ensureIndex( { key : 1, random :1 } )
Si ya está consultando contra un índice, simplemente suéltelo, agregue random: 1
a él y agréguelo nuevamente.
A partir de la versión 3.2 de MongoDB, puede obtener N documentos aleatorios de una colección utilizando el operador de la tubería de agregación de $sample
:
// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])
Ahora puedes usar el agregado. Ejemplo:
db.users.aggregate(
[ { $sample: { size: 3 } } ]
)
$sample .
Al usar Map / Reduce, ciertamente puede obtener un registro aleatorio, pero no necesariamente de manera muy eficiente, dependiendo del tamaño de la colección filtrada resultante con la que termine de trabajar.
He probado este método con 50,000 documentos (el filtro lo reduce a unos 30,000), y se ejecuta en aproximadamente 400 ms 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 Mapa simplemente crea una matriz de los 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 número entero aleatorio entre 0 y el número de elementos (-1) en la matriz, y luego devuelve ese _id de la matriz.
400 ms suena como mucho tiempo, y realmente lo es, si tenía cincuenta millones de registros en lugar de cincuenta mil, esto puede aumentar 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 función en el núcleo ... https://jira.mongodb.org/browse/SERVER-533
Si esta selección "aleatoria" se integrara en una búsqueda de índice en lugar de recopilar identificadores en una matriz y luego seleccionar una, esto ayudaría increíblemente. (¡Ve a votar!)
Aquí hay una manera de usar los valores de ObjectId
predeterminados 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.
Así que en puntos:
Encuentra los valores de clave primaria mínimo y máximo 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 encuentre el primer documento que sea mayor o igual a ese valor.
Esto utiliza el "relleno" del valor de marca de tiempo en "hex" para formar un valor de ObjectId
válido, ya que eso es lo que estamos buscando. Usar enteros como el valor _id
es esencialmente más simple pero la misma idea básica en los puntos.
Cuando me enfrenté a una solución similar, retrocedí y descubrí que la solicitud comercial era en realidad para crear alguna forma 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 almacenes de datos como MongoDB.
En resumen, con el requisito de "rotar inteligentemente" el contenido, lo que deberíamos hacer en lugar de un número aleatorio en todos los documentos es incluir un modificador de puntuación q personal. Para implementar esto usted mismo, suponiendo que una pequeña población de usuarios, puede almacenar un documento por usuario que tenga el Id. De producto, 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 calificación aq. modificador Cuando recupera el conjunto para mostrar, normalmente solicita más documentos del almacén de datos que lo solicitado por el usuario final, luego aplica el modificador de puntuación q, toma la cantidad de registros solicitados por el usuario final, y luego asigna al azar la página de resultados, una pequeña establecer, así que simplemente ordene los documentos en la capa de aplicación (en la memoria).
Si el universo de usuarios es demasiado grande, puede clasificar a los usuarios en grupos de comportamiento e indexarlos 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 aún es más efectiva para crear una experiencia relevante y valiosa de usar la solución de software.
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 para descifrar. ¿Qué es el campo _id? ¿Son mongodb id de objeto? Si es así, podría 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 están, 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);
Esto funciona bien, es rápido, funciona con varios documentos y no requiere rellenar el campo de rand
, que eventualmente se llenará a sí mismo:
- añadir índice al campo .rand en su colección
- Usa 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 mongodb está marcado como duplicado de esta pregunta. La diferencia es que esta pregunta pregunta explícitamente acerca de un solo registro, mientras que la otra pregunta explícitamente acerca de cómo obtener documentos aleatorios.
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()
La siguiente receta es un poco más lenta que la solución del libro de cocina de Mongo (agregar una clave aleatoria en cada documento), pero devuelve documentos aleatorios distribuidos de manera más uniforme. Es un poco menos distribuida que la solución de skip( random )
, pero mucho más rápida y más segura 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 necesite 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 aleatorios más uniformes que el método del "libro de cocina" informado por Michael:
Para una colección con 1.000.000 elementos:
Este método toma menos de un milisegundo en mi máquina
el método
skip()
toma 180 ms en promedio
El método del libro de cocina hará que una gran cantidad de documentos nunca sean seleccionados porque su número aleatorio no los favorece.
Este método recogerá todos los elementos uniformemente con el 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 función incorporada, como han señalado otros.
Sin embargo, debe ser un buen compromiso para muchos propósitos.
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:
link = db.links.find().sort({random: 1}).limit(1)[0]
Para garantizar que el mismo enlace no aparezca por segunda vez, actualice su campo aleatorio con un nuevo número aleatorio:
db.links.update({random: Math.random()}, link)
Ninguna de las soluciones funcionó bien para mí. Especialmente cuando hay muchos huecos 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();
Para obtener un número determinado de documentos aleatorios sin duplicados:
- primero consigue todas las identificaciones
- obtener el tamaño de los documentos
bucle obteniendo índice aleatorio y saltar 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) }); });
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 otorga 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)
};
}();
Puedes escoger _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 el almacenamiento de números aleatorios en la colección.
Si está usando mangosta, entonces puede usar mangosta al azar mongoose-random
Si está usando mongoid, la envoltura 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
Así que en la consola de rieles, puedo hacer, por ejemplo,
rando User
rando Article
para obtener documentos al azar de cualquier colección.
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 rubí):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
También puede utilizar 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 montón 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 puedes obtener un documento aleatorio 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 ningún cheque nulo, 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.
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 de las colecciones grandes.
Yo sugeriría agregar un campo int aleatorio a cada objeto. Entonces puedes hacer un
findOne({random_field: {$gte: rand()}})
para elegir un documento al azar. Solo asegúrate de asegurarte Indice ({random_field: 1})
Yo sugeriría usar map / reduce, donde usas la función map 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 de arriba funciona porque la función de mapa solo emite una tecla (''1'').
El valor de la "probabilidad" se define en el "alcance", al invocar mapRreduce (...)
El uso de mapReduce como este también se debe utilizar en una base de datos fragmentada.
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 la base de datos, y "countSubset" (n) es el número de documentos para recuperar.
Este enfoque podría dar algunos problemas en las bases de datos fragmentadas.
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;
}