巨大な(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-5 ですが、「修正しない」の下に提出されました。
クックブックには、コレクションからランダムなドキュメントを選択するための非常に優れたレシピがあります。 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 } } )
}
Rand
に最も近い乱数を持つドキュメントを見つけるには、$gte
と$lte
の両方を使用したクエリが必要です。
そしてもちろん、ランダムフィールドでインデックスを作成する必要があります。
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 )
これには1つのクエリのみが必要で、nullチェックは不要です。さらに、コードは簡潔でシンプルで柔軟です。ジオポイントのY軸を使用して、2番目のランダム性ディメンションをクエリに追加することもできます。
次のレシピは、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);
ベンチマーク結果
このメソッドは、(ceejayozの)skip()
メソッドよりもはるかに高速で、Michaelによって報告された「cookbook」メソッドよりも一様にランダムなドキュメントを生成します。
1,000,000要素のコレクションの場合:
この方法は、私のマシンで1ミリ秒未満かかります
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];
これがシェル表現の一般的なロジックであり、簡単に適応できます。
ポイントで:
コレクション内の最小および最大主キー値を見つける
それらのドキュメントのタイムスタンプの間にある乱数を生成します。
乱数を最小値に追加し、その値以上の最初のドキュメントを見つけます。
これは、「hex」のタイムスタンプ値から「padding」を使用して、有効なObjectId
値を形成するためです。 _id
の値として整数を使用することは本質的に簡単ですが、ポイントの基本的な考え方は同じです。
Pythonでpymongoを使用:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(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)
});
});
ランダムな_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)
})
})
})
ここでは、コレクションに乱数を保存するためにスペースを費やす必要はありません。
Python(pymongo)を使用すると、集約関数も機能します。
collection.aggregate([{'$sample': {'size': sample_size }}])
このアプローチは、はるかに高速乱数のクエリを実行するよりも(例:collection.find([random_int])。これは特に大きなコレクションの場合です。
ランダムなintフィールドを各オブジェクトに追加することをお勧めします。その後、あなたはちょうどすることができます
findOne({random_field: {$gte: Rand()}})
ランダムなドキュメントを選択します。必ず必ずIndex({random_field:1})を確認してください
Map/reduceを使用することをお勧めします。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つのキー(「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)はdb内のドキュメントの数であり、「countSubset」(n)は取得するドキュメントの数です。
このアプローチは、断片化されたデータベースでいくつかの問題を引き起こす可能性があります。
Mongooseを使用している場合は、mongoose-random mongoose-random を使用できます
私にとっては、どのソリューションもうまくいきませんでした。特に多くのギャップがあり、セットが小さい場合。これは私にとって非常にうまくいきました(phpで):
$count = $collection->count($search);
$skip = mt_Rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
私が同様のソリューションに直面したとき、私はバックトラックし、ビジネスリクエストが実際に提示されている在庫の何らかの形のローテーションを作成することであることがわかりました。その場合、MongoDBのようなデータストアではなく、Solrのような検索エンジンからの回答がある、はるかに優れたオプションがあります。
要するに、コンテンツを「インテリジェントにローテーション」する必要があるため、すべてのドキュメントで乱数の代わりに行うべきことは、個人のqスコア修飾子を含めることです。少数のユーザーを想定して、これを自分で実装するには、productId、インプレッションカウント、クリックスルーカウント、最終閲覧日、およびaqスコアを計算するのに意味があるとビジネスが判断するその他の要素を持つドキュメントをユーザーごとに保存できます修飾子。表示するセットを取得する場合、通常、エンドユーザーが要求したよりも多くのドキュメントをデータストアから要求し、qスコア修飾子を適用し、エンドユーザーが要求したレコード数を取得し、結果のページをランダム化します。設定するので、アプリケーション層(メモリ内)でドキュメントをソートするだけです。
ユーザーのユニバースが大きすぎる場合、ユーザーを行動グループに分類し、ユーザーではなく行動グループごとにインデックスを作成できます。
製品のユニバースが十分に小さい場合は、ユーザーごとにインデックスを作成できます。
この手法ははるかに効率的であることがわかりましたが、より重要なことは、ソフトウェアソリューションを使用する価値のある関連する経験を作成するのにより効果的であることです。
単純なIDキーがある場合、すべてのIDを配列に保存してから、ランダムなIDを選択できます。 (ルビーの答え):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
クエリの実行後にshuffle-arrayを使用することもできます
var shuffle = require( 'shuffle-array');
Accounts.find(qry、function(err、results_array){newIndexArr = shuffle(results_array);
これは素晴らしい動作で、高速で、複数のドキュメントで動作し、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)
}
})
追伸 mongodbでランダムなレコードを見つける方法 質問は、この質問の重複としてマークされています。違いは、この質問がランダムなドキュメントの取得について明示的に他の質問と同じように単一のレコードについて明示的に尋ねるということですs。
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を返します。
400ミリ秒は長いように聞こえますが、実際には、5万ではなく5千万のレコードがある場合、マルチユーザーの状況で使用できなくなるほどオーバーヘッドが増加する可能性があります。
MongoDBには、この機能をコアに含めるための未解決の問題があります... https://jira.mongodb.org/browse/SERVER-5
IDを配列に収集してから配列を選択する代わりに、この「ランダム」選択がインデックスルックアップに組み込まれている場合、これは非常に役立ちます。 (投票してください!)
ランダムソリューションによる私のPHP/MongoDBソート/順序。これが誰にも役立つことを願っています。
注:MySQLデータベースレコードを参照するMongoDBコレクション内に数値IDがあります。
最初に、ランダムに生成された10個の数字で配列を作成します
$randomNumbers = [];
for($i = 0; $i < 10; $i++){
$randomNumbers[] = Rand(0,1000);
}
集計では、$ arrayElemAtおよび$ mod(モジュラス)と組み合わせた$ addFieldパイプライン演算子を使用します。モジュラス演算子は、0から9までの数値を提供します。この数値を使用して、ランダムに生成された数値を持つ配列から数値を選択します。
$aggregate[] = [
'$addFields' => [
'random_sort' => [ '$arrayElemAt' => [ $randomNumbers, [ '$mod' => [ '$my_numeric_mysql_id', 10 ] ] ] ],
],
];
その後、並べ替えパイプラインを使用できます。
$aggregate[] = [
'$sort' => [
'random_sort' => 1
]
];