来自MongoDB的随机logging
我正在寻找从一个巨大的(1亿条logging) mongodb
随机logging。
什么是最快和最有效的方式呢? 数据已经存在,并且没有可以生成随机数并获得随机行的字段。
有什么build议么?
从MongoDB的3.2版本开始,您可以使用$sample
聚合pipe道运算符从集合中获取N个随机文档:
// Get one random document from the mycoll collection. db.mycoll.aggregate( { $sample: { size: 1 } } )
做一个所有logging的计数,产生一个介于0和计数之间的随机数,然后做:
db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
MongoDB 3.2的更新
3.2将$ sample引入到聚合pipe道中。
还有一个很好的博客文章将其付诸实践。
对于旧版本(以前的答案)
这实际上是一个function请求: http : //jira.mongodb.org/browse/SERVER-533,但它被提交下“不会修复”。
食谱有一个非常好的配方,从一个集合中select一个随机文档: http : //cookbook.mongodb.org/patterns/random-attribute/
为了解释配方,您将随机数字分配给您的文档:
db.docs.save( { key : 1, ..., random : Math.random() } )
然后select一个随机文件:
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
添加random: 1
,然后重新添加。
您还可以使用MongoDB的地理空间索引function来select“最接近”一个随机数的文档。
首先,在集合上启用地理空间索引:
db.docs.ensureIndex( { random_point: '2d' } )
在X轴上创build一堆随机点的文档:
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解决scheme慢一点(在每个文档上添加一个随机密钥),但返回的是更均匀分布的随机文档。 与skip( random )
解决scheme相比,它的分布要小一些,但是在删除文档的情况下,速度更快,更安全。
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; }
它还要求你在文档中添加一个随机的“随机”字段,所以当你创build它们时不要忘记添加这个字段:你可能需要初始化你的集合,如Geoffrey
function addRandom(collection) { collection.find().forEach(function (obj) { obj.random = Math.random(); collection.save(obj); }); } db.eval(addRandom, db.things);
基准testing结果
这个方法比skip()
方法(ceejayoz)要快得多,而且比Michael报告的“cookbook”方法生成的文档更为一致:
对于有100万个元素的集合:
-
这个方法在我的机器上花费的时间不到一毫秒
-
skip()
方法平均需要180 ms
食谱方法将导致大量的文件不会被挑选,因为他们的随机数不利于他们。
-
这个方法会随着时间的推移均匀地选取所有元
-
在我的基准testing中,它只比烹饪方法慢了30%。
-
随机性不是100%完美,但是非常好(如果需要的话可以改进)
这个配方并不完美 – 完美的解决scheme将是其他人已经注意到的内置function。
但是,对于许多目的来说,这应该是一个很好的妥协。
在Python中使用pymongo:
import random def get_random_doc(): count = collection.count() return collection.find()[random.randrange(count)]
这是一种使用_id
的默认ObjectId
值和一点math和逻辑的方法。
// 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];
这是壳代表性的一般逻辑,容易适应。
所以在几点:
-
查找集合中的最小和最大主键值
-
生成一个落在这些文档时间戳之间的随机数字。
-
将随机数添加到最小值,并查找大于或等于该值的第一个文档。
这使用“hex”中的时间戳值“填充”来形成一个有效的ObjectId
值,因为这是我们正在寻找。 使用整数作为_id
值本质上是简单的,但是在相同的基本思想。
如果没有关键数据,这是很难的。 什么是_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是均匀分布的(但它们不是,但至less这是一个开始):
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);
您可以随机select一个时间戳并search之后创build的第一个对象。 它只会扫描一个单一的文件,虽然它不一定会给你一个统一的分配。
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) }; }();
现在你可以使用聚合。 例:
db.users.aggregate( [ { $sample: { size: 3 } } ] )
看文档 。
我build议添加一个随机的int字段给每个对象。 那么你可以做一个
findOne({random_field: {$gte: rand()}})
挑选一个随机文件。 只要确保你确保索引({random_field:1})
我会build议使用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函数工作,因为只有一个键('1')从地图函数发出。
当调用mapRreduce(…)时,“概率”的值在“范围”
像这样使用mapReduce也应该可以在分片数据库上使用。
如果你想从数据库中精确selectm个文档,你可以这样做:
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)是要检索的文档数量。
这种方法可能会给分片数据库带来一些问题。
我的解决scheme在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; }
您可以随机select_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) }) }) })
在这里你不需要花费空间来存储随机数字。
为了获得确定数量的随机文档而不重复:
- 先得到所有的ID
- 获取文件的大小
-
循环生成随机索引并跳过重复
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) }); });
当我遇到类似的解决scheme时,我退缩,发现业务请求实际上是为了创build某种forms的正在呈现的库存轮换。 在这种情况下,有更好的select,从search引擎,如Solr,而不是像MongoDB的数据存储的答案。
简而言之,在“智能旋转”内容的要求中,我们应该做的是在所有文档中,而不是随机数字,包括个人q分数修饰符。 为了实现这个目标,假设用户人数很less,可以为每个用户存储具有productId,展示次数,点击次数,上次看到date以及业务认为对于计算aq得分有意义的任何其他因素的文档修改。 当检索要显示的集合时,通常您从数据存储中请求比最终用户请求更多的文档,然后应用q分数修改器,获取最终用户请求的logging数,然后随机化结果页面,设置,所以只需在应用程序层(在内存中)对文档进行sorting。
如果用户的范围过大,则可以将用户分类为行为组,并按行为组而不是用户进行索引。
如果产品的范围足够小,则可以为每个用户创build一个索引。
我发现这种技术要高效得多,但更重要的是创造一个相关的,值得使用软件解决scheme的经验。
没有解决scheme为我工作得很好。 特别是在差距很大,设置很小的时候。 这工作得很好(在PHP中):
$count = $collection->count($search); $skip = mt_rand(0, $count - 1); $result = $collection->find($search)->skip($skip)->limit(1)->getNext();
如果你有一个简单的ID键,你可以将所有的ID存储在一个数组中,然后select一个随机的ID。 (Ruby答案):
ids = @coll.find({},fields:{_id:1}).to_a @coll.find(ids.sample).first
使用Map / Reduce,你当然可以得到一个随机logging,不一定非常有效,取决于你最终使用的结果过滤集合的大小。
我已经用50,000个文档testing了这个方法(filter将其减less到大约30,000),并且它在具有16GB RAM和SATA3 HDD的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函数只是创build一个匹配查询的所有文档的id数组。 在我的情况下,我用50,000个可能的文档中的大约30,000个进行了testing。
Reduce函数只需从0到数组中的项数(-1)之间选取一个随机整数,然后从数组中返回_id 。
400ms听起来好像很长一段时间,实际上,如果你有五千万条logging而不是五万条logging,这可能会增加开销,使其在多用户情况下变得不可用。
MongoDB在核心中包含此function是一个开放的问题… https://jira.mongodb.org/browse/SERVER-533
如果将这个“随机”选项内置到索引查找中,而不是将数据收集到数组中,然后select一个,这将有助于令人难以置信。 (去投票!)
这工作很好,它很快,与多个文件工作,并不需要填充rand
字段,它最终会填充本身:
- 将索引添加到集合中的.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问题中find随机logging被标记为这个问题的重复。 不同之处在于,这个问题明确地要求单个logging作为另一个明确要求获取随机文档的logging 。
如果您使用的是文档到对象的包装器,您可以在Ruby中执行以下操作。 (假设你的模型是用户)
User.all.to_a[rand(User.count)]
在我的.irbrc中,我有
def rando klass klass.all.to_a[rand(klass.count)] end
所以在轨道控制台,我可以做,例如,
rando User rando Article
从任何集合中随机获取文档。
如果你正在使用mongoose,那么你可以使用mongoose随机mongoose随机
有效而可靠的工作是这样的:
为每个文档添加一个名为“random”的字段并为其指定一个随机值,为该随机字段添加一个索引并按以下步骤进行:
假设我们有一个称为“链接”的网页链接集合,我们需要从它的一个随机链接:
link = db.links.find().sort({random: 1}).limit(1)[0]
为确保相同的链接不会再次popup,请使用新的随机数更新其随机字段:
db.links.update({random: Math.random()}, link)