Je cherche à obtenir un enregistrement aléatoire d'un é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à présentes et il n’existe aucun champ dans lequel je puisse générer un nombre aléatoire et obtenir une ligne aléatoire.
Aucune suggestion?
À partir de la version 3.2 de MongoDB, vous pouvez obtenir N documents aléatoires d’une collection à l’aide de l’opérateur $sample
agrégation:
// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])
Comptez tous les enregistrements, générez un nombre aléatoire compris entre 0 et le nombre, puis procédez comme suit:
db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
3.2 introduit $ sample dans le pipeline d'agrégation.
Il y a aussi un bon blog post sur sa mise en pratique.
Il s’agissait en fait d’une demande de fonctionnalité: http://jira.mongodb.org/browse/SERVER-533 , mais elle a été classée sous "Ne résout pas le problème"
Le livre de cuisine 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 affectez des numéros aléatoires à vos documents:
db.docs.save( { key : 1, ..., random : Math.random() } )
Puis sélectionnez 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 } } )
}
Une interrogation avec $gte
et $lte
est nécessaire pour trouver le document avec un nombre aléatoire plus proche de Rand
.
Et bien sûr, vous voudrez indexer sur le champ aléatoire:
db.docs.ensureIndex( { key : 1, random :1 } )
Si vous interrogez déjà un index, supprimez-le simplement, ajoutez-lui random: 1
et ajoutez-le à nouveau.
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 tas de documents avec des points aléatoires sur l'axe des abscisses:
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] } } )
Ou vous pouvez récupérer plusieurs documents proches d'un point quelconque:
db.docs.find( { random_point : { $near : [Math.random(), 0] } } ).limit( 4 )
Cela ne nécessite qu'une seule requête et pas de contrôle null, plus le code est propre, simple et flexible. Vous pouvez même utiliser l'axe Y du géopoint pour ajouter une deuxième dimension aléatoire à votre requête.
La recette suivante est un peu plus lente que la solution mongo cookbook (ajoutez une clé aléatoire à chaque document), mais renvoie des documents aléatoires distribués de manière plus uniforme. Elle est un peu moins uniformément distribuée que la solution skip( random )
, mais elle est beaucoup plus rapide et plus sûre si les 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;
}
Vous devez également ajouter un champ "aléatoire" aléatoire à vos documents. N'oubliez pas de l'ajouter lorsque vous les créez: vous devrez peut-être initialiser votre collection, comme le montre Geoffrey.
function addRandom(collection) {
collection.find().forEach(function (obj) {
obj.random = Math.random();
collection.save(obj);
});
}
db.eval(addRandom, db.things);
Résultats de référence
Cette méthode est beaucoup plus rapide que la méthode skip()
(de ceejayoz) et génère des documents de manière plus uniformément aléatoire 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
la méthode skip()
prend 180 ms en moyenne
La méthode du livre de recettes fera en sorte qu'un grand nombre de documents ne soit jamais sélectionné car leur nombre aléatoire ne les favorise pas.
Cette méthode sélectionne tous les éléments de manière uniforme dans le temps.
Dans mon cas, il était seulement 30% plus lent 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 peut être amélioré si nécessaire)
Cette recette n'est pas parfaite - la solution parfaite serait une fonctionnalité intégrée, comme d'autres l'ont déjà noté.
Cependant, ce devrait être un bon compromis pour plusieurs raisons.
Voici un moyen d'utiliser les valeurs par défaut ObjectId
pour _id
et un peu de math 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 dans la représentation de Shell et facilement adaptable.
Donc en points:
Trouver les valeurs de clé primaire min et max dans la collection
Générez un nombre aléatoire compris entre les horodatages de ces documents.
Ajoutez le nombre aléatoire à la valeur minimale et recherchez le premier document supérieur ou égal à cette valeur.
Ceci utilise "padding" à partir de la valeur timestamp en "hex" pour former une valeur ObjectId
valide puisque c'est ce que nous recherchons. L'utilisation d'entiers comme valeur _id
est essentiellement plus simple, mais correspond à la même idée de base dans les points.
En Python utilisant Pymongo:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
c’est difficile s’il n’ya pas de données à saisir. Quels sont les champs _id? sont-ils les identifiants d'objet mongodb? Si tel 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 identifiants sont distribués uniformément (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);
Vous pouvez choisir un horodatage aléatoire et rechercher le premier objet créé par la suite . Il ne numérisera qu'un seul document, bien que cela 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)
};
}();
Maintenant, vous pouvez utiliser l'agrégat . Exemple:
db.users.aggregate(
[ { $sample: { size: 3 } } ]
)
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;
}
Pour obtenir un nombre déterminé de documents aléatoires sans doublons:
loop geting random index and skip duplicated
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)
});
});
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 la place pour stocker des nombres aléatoires dans la collection.
En utilisant Python (pymongo), la fonction d'agrégat fonctionne également.
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.find ([random_int]). C'est particulièrement le cas pour les grandes collections.
Je suggère d'ajouter un champ int aléatoire à chaque objet. Ensuite, vous pouvez juste faire un
findOne({random_field: {$gte: Rand()}})
choisir un document au hasard. Assurez-vous simplement d’assurer Index ({random_field: 1})
Je suggérerais d'utiliser map/reduction, où vous utilisez la fonction map pour n'émettre que lorsqu'une valeur aléatoire est supérieure à 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 de réduction ci-dessus fonctionne car une seule touche ('1') est émise par la fonction carte.
La valeur de la "probabilité" est définie dans la "portée" lors de l'appel de mapRreduce (...)
Utiliser mapReduce comme ceci devrait également être utilisable sur une base de données fragmentée.
Si vous voulez sélectionner exactement n sur m documents 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 la base de données, et "countSubset" (n) est le nombre de documents à récupérer.
Cette approche peut donner des problèmes sur les bases de données fragmentées.
Si vous utilisez de la mangouste, vous pouvez utiliser mangouste-aléatoire mongoose-random
aucune des solutions n'a bien fonctionné pour moi. surtout quand il y a beaucoup de lacunes et que le jeu 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();
Face à une solution similaire, j’ai fait marche arrière et découvert que la demande commerciale visait en fait à créer une forme de rotation de l’inventaire présenté. Dans ce cas, il existe de bien meilleures options, qui répondent aux réponses des moteurs de recherche tels que Solr, mais pas aux magasins de données tels que MongoDB.
En bref, avec l'exigence de "faire pivoter intelligemment" le contenu, ce que nous devrions faire à la place d'un nombre aléatoire dans tous les documents est d'inclure un modificateur de score q personnel. Pour implémenter cela vous-même, en supposant une petite population d'utilisateurs, vous pouvez stocker un document par utilisateur avec l'identificateur de produit, le nombre d'impressions, le nombre de clics, la date de la dernière consultation et tout autre facteur jugé utile par l'entreprise pour calculer l'aq. modificateur. Lors de la récupération de l'ensemble à afficher, vous demandez généralement plus de documents dans le magasin de données que demandé par l'utilisateur final, puis appliquez le modificateur de score q, prenez le nombre d'enregistrements demandés par l'utilisateur final, puis randomisez la page de résultats, une infime donc, triez simplement les documents dans la couche application (en mémoire).
Si l'univers des utilisateurs est trop volumineux, vous pouvez classer les utilisateurs en groupes de comportement et les indexer par groupe de comportement plutôt que par utilisateur.
Si l'univers des produits est suffisamment petit, vous pouvez créer un index par utilisateur.
J'ai trouvé cette technique beaucoup plus efficace, mais surtout plus efficace pour créer une expérience pertinente et intéressante d'utilisation de la solution logicielle.
Si vous avez une clé simple, vous pouvez stocker tous les identifiants dans un tableau, puis choisir un identifiant aléatoire. (Réponse Ruby):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
vous pouvez également utiliser shuffle-array après avoir exécuté votre requête
var shuffle = require ('shuffle-array');
Accounts.find (qry, function (err, results_array) {newIndexArr = shuffle (results_array);
Cela fonctionne bien, c'est rapide, fonctionne avec plusieurs documents et ne nécessite pas de renseigner le champ Rand
, qui sera éventuellement renseigné tout seul:
// 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 dans mongodb la question est marquée comme une copie de cette question. La différence est que cette question demande explicitement d'enregistrer un enregistrement comme l'autre explicitement d'obtenir un document aléatoires.
En utilisant Carte/Réduire, vous pouvez certainement obtenir un enregistrement aléatoire, mais pas nécessairement de manière très efficace, en fonction de la taille de la collection filtrée obtenue avec laquelle vous vous retrouvez.
J'ai testé cette méthode avec 50 000 documents (le filtre la réduit à environ 30 000) et elle s'exécute en environ 400 ms sur un processeur Intel i3 avec 16 Go de RAM et un disque dur 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 Carte crée simplement un tableau des identifiants de tous les documents correspondant à la requête. Dans mon cas, j'ai testé cela avec environ 30 000 des 50 000 documents possibles.
La fonction Réduction sélectionne simplement un entier aléatoire compris entre 0 et le nombre d'éléments (-1) dans le tableau, puis renvoie le _id du tableau.
400 ms semble être une longue période, et si vous aviez cinquante millions d’enregistrements au lieu de cinquante mille, cela pourrait augmenter les frais généraux au point de devenir inutilisable dans des situations multi-utilisateurs.
MongoDB a un problème en suspens à inclure cette fonctionnalité dans le noyau ... https://jira.mongodb.org/browse/SERVER-533
Si cette sélection "aléatoire" était intégrée à une recherche d'index au lieu de collecter les identifiants dans un tableau, puis de les sélectionner, cela aiderait incroyablement. (allez voter!)
Mon tri PHP/MongoDB/ordre par solution RANDOM. J'espère que cela aide quelqu'un.
Remarque: ma collection MongoDB contient des identifiants numériques qui font référence à un enregistrement de base de données MySQL.
Je crée d'abord un tableau avec 10 nombres générés aléatoirement
$randomNumbers = [];
for($i = 0; $i < 10; $i++){
$randomNumbers[] = Rand(0,1000);
}
Dans mon agrégation, j'utilise l'opérateur de pipeline $ addField associé à $ arrayElemAt et $ mod (modulus). L'opérateur de module me donnera un nombre compris entre 0 et 9, que j'utilise ensuite pour choisir un nombre dans le tableau avec des nombres générés aléatoirement.
$aggregate[] = [
'$addFields' => [
'random_sort' => [ '$arrayElemAt' => [ $randomNumbers, [ '$mod' => [ '$my_numeric_mysql_id', 10 ] ] ] ],
],
];
Après cela, vous pouvez utiliser le tri Pipeline.
$aggregate[] = [
'$sort' => [
'random_sort' => 1
]
];