我希望从一个巨大的记录(一亿个记录)中获得一个随机记录mongodb
。
最快,最有效的方法是什么?数据已经存在,并且没有可以生成随机数并获得随机行的字段。
有什么建议么?
我希望从一个巨大的记录(一亿个记录)中获得一个随机记录mongodb
。
最快,最有效的方法是什么?数据已经存在,并且没有可以生成随机数并获得随机行的字段。
有什么建议么?
Answers:
从MongoDB 3.2版本开始,您可以使用$sample
聚合管道运算符从集合中获取N个随机文档:
// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])
如果要从集合的过滤子集中选择随机文档$match
,请在管道之前添加一个阶段:
// Get one random document matching {a: 10} from the mycoll collection.
db.mycoll.aggregate([
{ $match: { a: 10 } },
{ $sample: { size: 1 } }
])
如评论中所述,当size
大于1时,返回的文档样本中可能有重复项。
对所有记录进行计数,生成介于0和该计数之间的随机数,然后执行以下操作:
db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
3.2 在聚合管道中引入了$ sample。
关于如何将其付诸实践也有一篇不错的博客文章。
这实际上是一个功能请求:http : //jira.mongodb.org/browse/SERVER-533,但是它属于“无法修复”。
该食谱有一个很好的秘诀,可以从集合中选择一个随机文档:http : //cookbook.mongodb.org/patterns/random-attribute/
要解释该配方,请为文档分配随机数:
db.docs.save( { key : 1, ..., random : Math.random() } )
然后选择一个随机文档:
rand = Math.random()
result = db.docs.findOne( { key : 2, random : { $gte : rand } } )
if ( result == null ) {
result = db.docs.findOne( { key : 2, random : { $lte : rand } } )
}
同时查询和$gte
,$lte
以查找随机数最接近的文档rand
。
当然,您需要在随机字段上建立索引:
db.docs.ensureIndex( { key : 1, random :1 } )
如果您已经在查询索引,只需将其删除,追加random: 1
到索引,然后再次添加即可。
$gte
。在这种情况下,替代解决方案stackoverflow.com/a/9499484/79201会更好。
您还可以使用MongoDB的地理空间索引功能将文档“最近”选择为一个随机数。
首先,对集合启用地理空间索引:
db.docs.ensureIndex( { random_point: '2d' } )
要创建一堆在X轴上具有随机点的文档:
for ( i = 0; i < 10; ++i ) {
db.docs.insert( { key: i, random_point: [Math.random(), 0] } );
}
然后,您可以像这样从集合中获取随机文档:
db.docs.findOne( { random_point : { $near : [Math.random(), 0] } } )
或者,您可以检索距离随机点最近的几个文档:
db.docs.find( { random_point : { $near : [Math.random(), 0] } } ).limit( 4 )
这只需要一个查询,没有空检查,加上代码干净,简单和灵活。您甚至可以使用地理位置的Y轴为查询添加第二个随机维度。
以下食谱比mongo cookbook解决方案要慢一些(在每个文档上添加一个随机密钥),但是返回的是分布更均匀的随机文档。与skip( random )
解决方案相比,它的分布不均,但是在删除文档的情况下,它更快,更安全。
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;
}
它还需要您在文档中添加一个随机的“随机”字段,因此创建它们时请不要忘记添加此字段:您可能需要初始化集合,如Geoffrey所示
function addRandom(collection) {
collection.find().forEach(function (obj) {
obj.random = Math.random();
collection.save(obj);
});
}
db.eval(addRandom, db.things);
基准结果
此方法比skip()
(ceejayoz)方法快得多,并且比Michael报告的“ cookbook”方法生成更均匀的随机文档:
对于具有1,000,000个元素的集合:
这种方法在我的机器上花费不到一毫秒
该skip()
方法平均需要180毫秒
该Cookbook方法将导致大量文档永远不会被拾取,因为它们的随机数不利于它们。
该方法将随着时间的推移均匀地选择所有元素。
在我的基准测试中,它仅比Cookbook方法慢30%。
随机性不是100%完美,但非常好(如有必要,可以改善)
这个食谱并不完美-完美的解决方案将是其他人所指出的内置功能。
但是,对于许多目的,这应该是一个很好的折衷方案。
这是使用的默认ObjectId
值_id
以及一些数学和逻辑的方法。
// 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];
这是外壳表示中的通用逻辑,并且易于适应。
所以要点:
在集合中找到最小和最大主键值
生成一个随机数,介于这些文档的时间戳之间。
将随机数加到最小值,然后找到大于或等于该值的第一个文档。
这将使用“十六进制”中时间戳值中的“填充”来形成有效值,ObjectId
因为这正是我们要寻找的。使用整数作为_id
值本质上比较简单,但要点相同。
在使用pymongo的Python中:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
count()
与estimated_document_count()
如count()
在Mongdo V4.2已经过时了。
如果没有数据可以取消,这将非常困难。_id字段是什么?他们是mongodb对象ID的吗?如果是这样,您可以获得最高和最低值:
lowest = db.coll.find().sort({_id:1}).limit(1).next()._id;
highest = db.coll.find().sort({_id:-1}).limit(1).next()._id;
那么,如果您假设ID是均匀分布的(但不是,但至少是一个开始):
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);
您可以选择随机时间戳记,然后搜索之后创建的第一个对象。尽管它不一定能为您提供统一的分发,但它只会扫描单个文档。
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)
};
}();
我在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;
}
为了获得确定数量的无重复文档,请执行以下操作:
循环获取随机索引并跳过重复
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)
});
});
我建议使用map / reduce,在其中使用map函数仅在随机值高于给定概率时才发出。
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);
上述reducef函数之所以有效,是因为map函数仅发出一个键('1')。
调用mapRreduce(...)时,在“范围”中定义“概率”的值
像这样使用mapReduce也可以在分片数据库上使用。
如果要从数据库中准确选择n个文档,则可以这样操作:
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);
其中“ countTotal”(m)是数据库中的文档数,“ countSubset”(n)是要检索的文档数。
这种方法可能会对分片数据库产生一些问题。
您可以选择随机的_id并返回相应的对象:
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)
})
})
})
在这里,您不需要花时间在集合中存储随机数。
我建议向每个对象添加一个随机的int字段。然后你可以做一个
findOne({random_field: {$gte: rand()}})
选择一个随机文档。只要确保您确保Index({random_field:1})
当我遇到类似的解决方案时,我回溯并发现业务请求实际上是为了对要显示的库存进行某种形式的轮换。在这种情况下,有更好的选择,可以从诸如Solr之类的搜索引擎获得答案,而不能从诸如MongoDB之类的数据存储中获得答案。
简而言之,由于要求“智能旋转”内容,我们应该做的是代替个人q得分修饰符,而不是对所有文档使用随机数。为了自己实现这一点,假设用户数量很少,您可以为每个用户存储一个文档,该文档具有productId,展示次数,点击次数,上次查看日期以及企业发现对计算aq分数有意义的任何其他因素修饰符。检索要显示的集合时,通常您从数据存储中请求的文档要比最终用户请求的要多,然后应用q得分修饰符,获取最终用户请求的记录数,然后将结果页面随机化设置,因此只需对应用程序层(在内存中)中的文档进行排序。
如果用户范围太大,则可以将用户分类为行为组,然后按行为组而不是用户进行索引。
如果产品范围足够小,则可以为每个用户创建一个索引。
我发现该技术效率更高,但更重要的是,在创建使用软件解决方案的相关有价值的经验方面,效率更高。
没有一种解决方案对我来说效果很好。尤其是当间隙很多且设置很小时。这对我来说很好(在php中):
$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
find
+ skip
很不好,您返回所有文档只是为了选择一个:S。
我的PHP / MongoDB按RANDOM解决方案排序/排序。希望这对任何人有帮助。
注意:我的MongoDB集合中有数字ID,用于引用MySQL数据库记录。
首先,我创建一个包含10个随机生成数字的数组
$randomNumbers = [];
for($i = 0; $i < 10; $i++){
$randomNumbers[] = rand(0,1000);
}
在我的聚合中,我将$ addField管道运算符与$ arrayElemAt和$ mod(模数)结合使用。模运算符会给我一个0到9之间的数字,然后我用它从具有随机生成数字的数组中选择一个数字。
$aggregate[] = [
'$addFields' => [
'random_sort' => [ '$arrayElemAt' => [ $randomNumbers, [ '$mod' => [ '$my_numeric_mysql_id', 10 ] ] ] ],
],
];
之后,您可以使用排序管道。
$aggregate[] = [
'$sort' => [
'random_sort' => 1
]
];
如果您有一个简单的ID密钥,则可以将所有ID存储在一个数组中,然后选择一个随机ID。(Ruby答案):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
使用Map / Reduce,您当然可以得到一个随机记录,但不一定非常有效,这取决于最终使用的结果过滤后的集合的大小。
我已经用50,000个文档测试了此方法(过滤器将其减少到大约30,000个),并且在具有16GB内存和SATA3 HDD的Intel i3上,它可以在大约400毫秒内执行...
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" }
}
);
Map函数只是创建一个与查询匹配的所有文档的ID的数组。就我而言,我用50,000个可能的文档中的大约30,000个进行了测试。
Reduce函数仅选择一个介于0和数组中项数(-1)之间的随机整数,然后从数组中返回该_id。
400ms听起来很长一段时间,实际上,如果您有五千万条记录而不是五万条记录,这可能会将开销增加到在多用户情况下变得无法使用的程度。
MongoDB在核心中包含此功能存在一个未解决的问题... https://jira.mongodb.org/browse/SERVER-533
如果将此“随机”选择内置到索引查找中,而不是将id收集到一个数组中然后选择一个,那么这将非常有用。(去投票吧!)
这样效果很好,速度很快,可以处理多个文档,并且不需要填充rand
字段,字段最终将自己填充:
// 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。如何在mongodb问题中查找随机记录被标记为该问题的重复项。所不同的是,这个问题是问关于明确单个记录作为另一个明确有关获取随机文件小号。
如果使用的是文档到对象包装器mongoid,则可以在Ruby中执行以下操作。(假设您的模型是用户)
User.all.to_a[rand(User.count)]
在我的.irbrc中,我有
def rando klass
klass.all.to_a[rand(klass.count)]
end
因此在Rails控制台中,我可以执行例如
rando User
rando Article
从任何集合中随机获取文档。
有效而可靠地工作的是:
向每个文档添加一个名为“ random”的字段,并为其分配一个随机值,为该随机字段添加一个索引,然后按以下步骤操作:
假设我们有一个称为“ links”的Web链接集合,并且我们想要一个随机链接:
link = db.links.find().sort({random: 1}).limit(1)[0]
为确保同一链接不会再次弹出,请使用新的随机数更新其随机字段:
db.links.update({random: Math.random()}, link)