巨大なコレクション (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 )
これには 1 つのクエリのみが必要で、null チェックは必要ありません。また、コードはクリーンでシンプルかつ柔軟です。ジオポイントの Y 軸を使用して、2 つ目のランダム性ディメンションをクエリに追加することもできます。
これは と のデフォルト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」のタイムスタンプ値から「パディング」を使用して有効な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)
};
}();
マップ/リデュースを使用することをお勧めします。マップ関数を使用して、ランダムな値が特定の確率を超えている場合にのみ出力します。
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 を使用すると、シャードされたデータベースでも使用できるはずです。
データベースから 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) は取得するドキュメントの数です。
このアプローチでは、シャード データベースで問題が発生する可能性があります。
各オブジェクトにランダムな int フィールドを追加することをお勧めします。その後、あなたはただ行うことができます
findOne({random_field: {$gte: rand()}})
ランダムなドキュメントを選択します。確実に Index({random_field:1}) を確認してください
どのソリューションもうまくいきませんでした。特にギャップが多く、セットが小さい場合。これは私にとって非常にうまくいきました(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 スコア修飾子を適用し、エンド ユーザーが要求した数のレコードを取得し、結果のページをランダム化します。設定されているため、アプリケーション層 (メモリ内) でドキュメントを並べ替えるだけです。
ユーザーの範囲が大きすぎる場合は、ユーザーを行動グループに分類し、ユーザーではなく行動グループごとにインデックスを作成できます。
製品の範囲が十分に小さい場合は、ユーザーごとにインデックスを作成できます。
この手法ははるかに効率的であることがわかりましたが、さらに重要なことに、ソフトウェア ソリューションを使用する価値のある適切なエクスペリエンスを作成する上でより効果的であることがわかりました。
次の集計操作では、コレクションから 3 つのドキュメントがランダムに選択されます。
db.users.aggregate( [ { $sample: { サイズ: 3 } } ] )
https://docs.mongodb.com/manual/reference/operator/aggregation/sample/
単純なIDキーがある場合は、すべてのIDを配列に格納してから、ランダムなIDを選択できます。(ルビーの答え):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
Map/Reduce を使用すると、ランダム レコードを確実に取得できますが、結果として得られるフィルター処理されたコレクションのサイズによっては、必ずしも効率的であるとは限りません。
このメソッドを 50,000 のドキュメントでテストしました (フィルターにより約 30,000 に削減されます) 。16 GB の RAM と 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を返します。
400 ミリ秒は長い時間のように聞こえますが、実際には、5 万ではなく 5 千万のレコードがある場合、これによりオーバーヘッドが増加し、マルチユーザーの状況では使用できなくなる可能性があります。
この機能をコアに含めるには、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 の質問でランダムなレコードを見つける方法は、この質問の重複としてマークされています。違いは、この質問は単一のレコードについて明示的に質問するのに対し、他の質問はランダム ドキュメントs の取得について明示的に質問することです。
効率的かつ確実に機能するのは次のとおりです。
各ドキュメントに「ランダム」というフィールドを追加してランダムな値を割り当て、ランダムフィールドのインデックスを追加して、次の手順に従います。
「リンク」と呼ばれるWebリンクのコレクションがあり、そこからランダムなリンクが必要であると仮定します。
link = db.links.find().sort({random: 1}).limit(1)[0]
同じリンクが2回目にポップアップしないようにするには、そのランダムフィールドを新しい乱数で更新します。
db.links.update({random: Math.random()}, link)