私は巨大な(1億レコード)からランダムなレコードを取得しようとしています mongodb
ます。
そうするための最速かつ最も効率的な方法は何ですか?データはすでにそこにあり、乱数を生成してランダムな行を取得できるフィールドはありません。
助言がありますか?
私は巨大な(1億レコード)からランダムなレコードを取得しようとしています mongodb
ます。
そうするための最速かつ最も効率的な方法は何ですか?データはすでにそこにあり、乱数を生成してランダムな行を取得できるフィールドはありません。
助言がありますか?
回答:
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 )
これにはクエリが1つだけ必要で、nullチェックは必要ありません。さらに、コードはクリーンでシンプルで柔軟です。ジオポイントのY軸を使用して、クエリに2番目のランダム次元を追加することもできます。
次のレシピは、mongoクックブックソリューションよりも少し低速ですが(すべてのドキュメントにランダムキーを追加)、より均等に分散されたランダムドキュメントを返します。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によって報告された「料理本」メソッドよりも均一にランダムなドキュメントを生成します。
1,000,000要素のコレクションの場合:
この方法は私のマシンでミリ秒未満かかります
このskip()
方法は平均で180ミリ秒かかります
クックブック方式では、乱数が優先されないため、多数のドキュメントが選択されることはありません。
このメソッドは、時間の経過とともにすべての要素を均等に選択します。
私のベンチマークでは、クックブック方式よりも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];
これがシェル表現の一般的なロジックであり、簡単に適応できます。
だからポイントで:
コレクション内の主キーの最小値と最大値を見つける
それらのドキュメントのタイムスタンプの間にある乱数を生成します。
乱数を最小値に追加し、その値以上の最初のドキュメントを見つけます。
これは、「16進数」のタイムスタンプ値からの「パディング」を使用して、有効なObjectId
値を形成します。整数を_id
値として使用することは基本的には簡単ですが、ポイントの基本的な考え方は同じです。
pymongoを使用するPythonでは:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
count()
てください。estimated_document_count()
count()
そこにキーオフするデータがない場合は難しいです。_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つのキー( '1')だけが出力されるため、機能します。
「確率」の値は、mapRreduce(...)を呼び出すときに「スコープ」で定義されます。
このようなmapReduceの使用は、分割されたデータベースでも使用できるはずです。
dbからちょうど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()}})
ランダムなドキュメントを選択します。必ず確認してください。ensureIndex({random_field:1})
私が同様のソリューションに直面したとき、私は逆戻りして、ビジネス要求が実際に提示されている在庫の何らかの形のローテーションを作成することであることがわかりました。その場合、MongoDBのようなデータストアではなく、Solrのような検索エンジンからの回答を持つ、より優れたオプションがあります。
つまり、コンテンツを「インテリジェントにローテーション」する必要があるため、すべてのドキュメントにわたって乱数の代わりに行うべきことは、個人の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
はかなり悪いです。1つの:Sを選択するためだけにすべてのドキュメントを返します。
PHP / MongoDBのRANDOMソリューションによる並べ替え/順序付け。これが誰にも役立つことを願っています。
注:私のMongoDBコレクションには、MySQLデータベースレコードを参照する数値IDがあります。
まず、ランダムに生成された10個の数値で配列を作成します
$randomNumbers = [];
for($i = 0; $i < 10; $i++){
$randomNumbers[] = rand(0,1000);
}
私の集計では、$ addFieldパイプライン演算子と$ arrayElemAtおよび$ mod(modulus)を組み合わせて使用しています。モジュラス演算子により、0〜9の数値が得られます。これを使用して、ランダムに生成された数値を持つ配列から数値を選択します。
$aggregate[] = [
'$addFields' => [
'random_sort' => [ '$arrayElemAt' => [ $randomNumbers, [ '$mod' => [ '$my_numeric_mysql_id', 10 ] ] ] ],
],
];
その後、並べ替えパイプラインを使用できます。
$aggregate[] = [
'$sort' => [
'random_sort' => 1
]
];
単純なIDキーがある場合は、すべてのIDを配列に格納してから、ランダムなIDを選択できます。(ルビー回答):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
Map / Reduceを使用すると、ランダムなレコードを確実に取得できますが、結果としてフィルター処理されたコレクションのサイズによっては、必ずしも非常に効率的ではありません。
私はこの方法を50,000のドキュメントでテストしました(フィルターはそれを約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関数は、クエリに一致するすべてのドキュメントのIDの配列を作成するだけです。私の場合、50,000の可能なドキュメントのうち約30,000でこれをテストしました。
Reduce関数は、0と配列内の項目の数(-1)の間のランダムな整数を選択し、その_idを返します。を配列から。
400msは長い時間のように聞こえますが、実際には、5万ではなく5千万のレコードがあった場合、マルチユーザーの状況で使用できなくなるまでオーバーヘッドが増える可能性があります。
MongoDBには、コアにこの機能を含めるための未解決の問題があります... https://jira.mongodb.org/browse/SERVER-533
この「ランダムな」選択が、IDを配列に収集してから1つを選択するのではなく、インデックス検索に組み込まれた場合、これは信じられないほど役立ちます。(投票してください!)
これはうまく動作し、高速で、複数のドキュメントで動作し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
任意のコレクションからランダムにドキュメントを取得します。
効率的かつ確実に機能するのはこれです。
「ランダム」というフィールドを各ドキュメントに追加し、それにランダムな値を割り当て、ランダムフィールドのインデックスを追加して、次のように進めます。
「リンク」と呼ばれるWebリンクのコレクションがあり、そこからランダムなリンクが必要だとします。
link = db.links.find().sort({random: 1}).limit(1)[0]
同じリンクが2度目にポップアップしないようにするには、そのランダムフィールドを新しい乱数で更新します。
db.links.update({random: Math.random()}, link)