我希望从庞大的集合(1 亿条记录)中获取随机记录。
最快和最有效的方法是什么?
数据已经存在,并且没有我可以在其中生成随机数并获得随机行的字段。
我希望从庞大的集合(1 亿条记录)中获取随机记录。
最快和最有效的方法是什么?
数据已经存在,并且没有我可以在其中生成随机数并获得随机行的字段。
从 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
到它,然后再次添加它。
您还可以使用 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 方法将导致大量文档永远不会被选中,因为它们的随机数不利于它们。
此方法将随着时间的推移均匀地选取所有元素。
在我的基准测试中,它只比食谱方法慢 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];
这是 shell 表示的一般逻辑,并且很容易适应。
所以在点:
查找集合中的最小和最大主键值
生成一个介于这些文档的时间戳之间的随机数。
将随机数添加到最小值并找到大于或等于该值的第一个文档。
这使用“十六进制”中的时间戳值中的“填充”来形成有效值ObjectId
,因为这是我们正在寻找的。使用整数作为_id
值本质上更简单,但基本思想相同。
在 Python 中使用 pymongo:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
使用 Python (pymongo),聚合函数也可以工作。
collection.aggregate([{'$sample': {'size': sample_size }}])
这种方法比运行随机数查询(例如collection.find([random_int]))要快得多。对于大型集合尤其如此。
如果没有数据可以关闭,那就很难了。_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 也应该可以在分片数据库上使用。
如果您想从数据库中准确地选择 m 个文档中的 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)
})
})
})
在这里,您不需要花费空间来存储集合中的随机数。
当我遇到类似的解决方案时,我回溯并发现业务请求实际上是为了创建某种形式的库存轮换。在这种情况下,有更好的选择,它们有来自 Solr 等搜索引擎的答案,而不是 MongoDB 等数据存储。
简而言之,对于“智能轮换”内容的要求,我们应该做的不是在所有文档中使用随机数,而是包含一个个人 q 分数修饰符。为了自己实现这一点,假设用户数量很少,您可以为每个用户存储一个文档,其中包含 productId、展示次数、点击次数、上次查看日期以及企业认为对计算 aq 分数有意义的任何其他因素修饰符。检索要显示的集合时,通常您从数据存储中请求的文档比最终用户请求的多,然后应用 q 分数修饰符,获取最终用户请求的记录数,然后随机化结果页面,一个很小的设置,所以只需在应用层(内存中)对文档进行排序。
如果用户范围太大,您可以将用户分类为行为组,并按行为组而不是用户进行索引。
如果产品范围足够小,您可以为每个用户创建一个索引。
我发现这种技术效率更高,但更重要的是更有效地创造了使用该软件解决方案的相关、有价值的体验。
non of the solutions worked well for me. especially when there are many gaps and set is small. this worked very well for me(in php):
$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
我建议为每个对象添加一个随机 int 字段。然后你可以做一个
findOne({random_field: {$gte: rand()}})
选择一个随机文档。只要确保你确保Index({random_field:1})
以下聚合操作从集合中随机选择 3 个文档:
db.users.aggregate( [ { $sample: { size: 3 } } ] )
https://docs.mongodb.com/manual/reference/operator/aggregation/sample/
我的 PHP/MongoDB 按 RANDOM 解决方案排序/排序。希望这对任何人都有帮助。
注意:我的 MongoDB 集合中有引用 MySQL 数据库记录的数字 ID。
首先,我创建一个包含 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
]
];
MongoDB 现在有$rand
要选择 n 个非重复项,请使用{ $addFields: { _f: { $rand: {} } } }
then $sort
by_f
和$limit
n 进行聚合。
这很好用,速度很快,可以处理多个文档并且不需要填充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)
}
})
附言。How to find random records in mongodb question被标记为这个问题的重复。不同之处在于,这个问题明确询问了单个记录,而另一个问题明确询问了获取随机文档s。
如果您有一个简单的 id 键,则可以将所有 id 存储在一个数组中,然后选择一个随机 id。(红宝石回答):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
使用 Map/Reduce,您当然可以获得随机记录,但不一定非常有效,具体取决于您最终使用的过滤集合的大小。
我已经用 50,000 个文档(过滤器将其减少到大约 30,000 个)测试了这种方法,它在具有 16GB 内存和 SATA3 硬盘的 Intel i3 上执行大约400ms ...
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 听起来很长,而且确实如此,如果您有 5000 万条记录而不是 50000 万条记录,这可能会增加开销,使其在多用户情况下变得无法使用。
MongoDB 在核心中包含此功能存在一个未解决的问题... https://jira.mongodb.org/browse/SERVER-533
如果这种“随机”选择被构建到索引查找中,而不是将 id 收集到一个数组中然后选择一个,这将非常有用。(去投票吧!)
对我来说,我想以随机顺序获得相同的记录,所以我创建了一个用于排序的空数组,然后生成 1 到 7 之间的随机数(我有 7 个字段)。所以每次我得到不同的值时,我都会分配一个不同的随机排序。这是“外行”,但对我有用。
//generate random number
const randomval = some random value;
//declare sort array and initialize to empty
const sort = [];
//write a conditional if else to get to decide which sort to use
if(randomval == 1)
{
sort.push(...['createdAt',1]);
}
else if(randomval == 2)
{
sort.push(...['_id',1]);
}
....
else if(randomval == n)
{
sort.push(...['n',1]);
}
Mongoose 中最好的方法是使用 $sample 进行聚合调用。但是,Mongoose 不会将 Mongoose 文档应用于聚合 - 特别是如果要应用 populate() 则更是如此。
从数据库中获取“精益”数组:
/*
Sample model should be init first
const Sample = mongoose …
*/
const samples = await Sample.aggregate([
{ $match: {} },
{ $sample: { size: 33 } },
]).exec();
console.log(samples); //a lean Array
获取一组 mongoose 文档:
const samples = (
await Sample.aggregate([
{ $match: {} },
{ $sample: { size: 27 } },
{ $project: { _id: 1 } },
]).exec()
).map(v => v._id);
const mongooseSamples = await Sample.find({ _id: { $in: samples } });
console.log(mongooseSamples); //an Array of mongoose documents
如果您使用的是文档到对象包装器 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
从任何集合中随机获取文档。
您还可以在执行查询后使用shuffle-array
var shuffle = require('shuffle-array');
Accounts.find(qry,function(err,results_array){ newIndexArr= shuffle(results_array);
有效和可靠的工作是这样的:
为每个文档添加一个名为“random”的字段并为其分配一个随机值,为该随机字段添加一个索引,然后执行以下操作:
假设我们有一个名为“links”的网络链接集合,我们希望从中获得一个随机链接:
link = db.links.find().sort({random: 1}).limit(1)[0]
为确保不会再次弹出相同的链接,请使用新的随机数更新其随机字段:
db.links.update({random: Math.random()}, link)