거대한 (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 "Wo n't fix"아래에 제출되었습니다.
요리 책에는 컬렉션에서 임의의 문서를 선택하기위한 매우 좋은 레시피가 있습니다. 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 )
여기에는 쿼리가 하나만 필요하고 null 검사가 필요하지 않으며 코드는 깨끗하고 단순하며 유연합니다. 지오 포인트의 Y 축을 사용하여 쿼리에 두 번째 임의성 차원을 추가 할 수도 있습니다.
다음 레시피는 몽고 요리 책 솔루션보다 약간 느리지 만 (모든 문서에 임의의 키를 추가하십시오) 더 균등하게 분산 된 임의의 문서를 반환합니다. 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 개의 요소가있는 컬렉션의 경우 :
이 방법은 내 컴퓨터에서 1 밀리 초도 채 걸리지 않습니다
이 skip()
방법은 평균 180ms 소요
요리 책 방법은 임의의 숫자가 선호하지 않기 때문에 많은 수의 문서를 선택하지 않습니다.
이 방법은 시간이 지남에 따라 모든 요소를 고르게 선택합니다.
내 벤치 마크에서는 요리 책 방법보다 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
값 으로 사용하는 것은 본질적으로 간단하지만 포인트의 기본 아이디어는 동일합니다.
Python에서 pymongo를 사용하는 경우 :
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
count()
와 estimated_document_count()
같은 count()
Mongdo의 버전 4.2에서 더 이상 사용되지 않습니다.
키 오프 할 데이터가 없으면 힘들다. _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 함수를 사용하여 map / reduce를 사용하는 것이 좋습니다.
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를 사용하면 샤드 DB에서도 사용할 수 있습니다.
db에서 정확히 n 개의 m 개의 문서를 선택하려면 다음과 같이하십시오.
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)은 검색 할 문서 수입니다.
이 방법은 샤드 데이터베이스에서 일부 문제를 야기 할 수 있습니다.
임의의 _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()}})
임의의 문서를 선택합니다. Index ({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
는 매우 나쁩니다. 하나를 선택하기 위해 모든 문서를 반환합니다.
RANDOM 솔루션별로 내 PHP / MongoDB 정렬 / 순서. 이것이 누군가를 돕기를 바랍니다.
참고 : MongoDB 컬렉션에는 MySQL 데이터베이스 레코드를 나타내는 숫자 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
]
];
간단한 ID 키가 있으면 모든 ID를 배열에 저장 한 다음 임의의 ID를 선택할 수 있습니다. (루비 답변) :
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
Map / Reduce를 사용하면 결과적으로 필터링 된 컬렉션의 크기에 따라 반드시 효율적으로 무작위 레코드를 얻을 수는 없습니다.
나는이 방법을 50,000 문서 (필터는 약 30,000으로 줄였습니다)로 테스트 했으며 16GB 램과 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를 배열로 수집 한 다음 선택하는 대신 인덱스 조회에 내장 된 경우 엄청나게 도움이됩니다. (투표하세요!)
이것은 잘 작동하고 빠르며 여러 문서와 함께 작동하며 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 질문 에서 임의의 레코드를 찾는 방법 은이 질문의 복제본으로 표시됩니다. 차이점은이 질문에 명시 적으로 임의 문서 얻기에 관하여 다른 하나 하나의 레코드에 대해 명시 적으로 요구한다는 것입니다 들 .
문서 대 오브젝트 랩퍼 인 mongoid를 사용하는 경우 Ruby에서 다음을 수행 할 수 있습니다. (모델이 사용자라고 가정)
User.all.to_a[rand(User.count)]
내 .irbrc에서 나는
def rando klass
klass.all.to_a[rand(klass.count)]
end
레일 콘솔에서 예를 들어
rando User
rando Article
컬렉션에서 임의로 문서를 가져옵니다.
효율적이고 안정적으로 작동하는 것은 다음과 같습니다.
각 문서에 "random"이라는 필드를 추가하고 임의의 값을 할당하고 임의 필드에 대한 색인을 추가 한 후 다음과 같이 진행하십시오.
"links"라는 웹 링크 모음이 있고 그로부터 임의의 링크를 원한다고 가정합니다.
link = db.links.find().sort({random: 1}).limit(1)[0]
동일한 링크가 두 번째로 나타나지 않도록하려면 임의의 필드를 새로운 임의의 숫자로 업데이트하십시오.
db.links.update({random: Math.random()}, link)