Я ищу случайную запись из огромной коллекции (100 миллионов записей).
Как это сделать быстрее и эффективнее?
Данные уже есть, и нет поля, в котором я мог бы сгенерировать случайное число и получить случайную строку.
Я ищу случайную запись из огромной коллекции (100 миллионов записей).
Как это сделать быстрее и эффективнее?
Данные уже есть, и нет поля, в котором я мог бы сгенерировать случайное число и получить случайную строку.
Начиная с версии MongoDB 3.2, вы можете получить N случайных документов из коллекции, используя _ 1_ оператор конвейера агрегирования:
// 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, в возвращаемом образце документа могут быть дубликаты.
db.mycoll.aggregate([{ $sample: { size: 1 } }, { $match: {key1: value1, key2: value2, ...}}])
- person ThisIsNotAnId; 22.02.2019
$match
.
- person JohnnyHK; 22.02.2019
Подсчитайте все записи, сгенерируйте случайное число от 0 до count, а затем выполните:
db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
count
.
- person Boaz - CorporateShillExchange; 11.02.2015
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.
- person Ryan Schumacher; 30.10.2013
findAndModify()
и обновлять случайное поле вместе с каждым запросом.
- person Julien; 05.10.2014
Вы также можете использовать функцию геопространственного индексирования 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 геоточки, чтобы добавить в запрос второе измерение случайности.
Следующий рецепт немного медленнее, чем решение 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;
}
Это также требует, чтобы вы добавили случайное «случайное» поле в ваши документы, поэтому не забудьте добавить это при их создании: вам может потребоваться инициализировать вашу коллекцию, как показано Джеффри
function addRandom(collection) {
collection.find().forEach(function (obj) {
obj.random = Math.random();
collection.save(obj);
});
}
db.eval(addRandom, db.things);
Результаты сравнения
Этот метод намного быстрее, чем метод skip()
(ceejayoz), и генерирует более равномерно случайные документы, чем метод "поваренной книги", о котором сообщил Майкл:
Для коллекции из 1000000 элементов:
На моем компьютере этот метод занимает меньше миллисекунды.
метод 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];
Это общая логика представления оболочки, которую легко адаптировать.
Итак, по пунктам:
Найдите минимальное и максимальное значения первичного ключа в коллекции.
Сгенерируйте случайное число, которое находится между отметками времени этих документов.
Добавьте случайное число к минимальному значению и найдите первый документ, который больше или равен этому значению.
При этом используется «отступ» от значения временной метки в «шестнадцатеричном формате» для формирования допустимого значения ObjectId
, поскольку это то, что мы ищем. Использование целых чисел в качестве значения _id
существенно проще, но та же основная идея в точках.
Теперь вы можете использовать агрегат. Пример:
db.users.aggregate(
[ { $sample: { size: 3 } } ]
)
В Python с использованием pymongo:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
count()
на estimated_document_count()
, поскольку count()
устарел в Mongdo v4.2.
- person user3848207; 12.06.2020
При использовании Python (pymongo) агрегатная функция также работает.
collection.aggregate([{'$sample': {'size': sample_size }}])
Этот подход намного быстрее, чем выполнение запроса для случайного числа (например, collection.find ([random_int]). Это особенно актуально для больших коллекций.
это сложно, если нет данных, которые можно было бы отключить. что такое поле _id? это идентификаторы объекта mongodb? Если это так, вы можете получить самые высокие и самые низкие значения:
lowest = db.coll.find().sort({_id:1}).limit(1).next()._id;
highest = db.coll.find().sort({_id:-1}).limit(1).next()._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 работает, потому что только одна клавиша ('1') испускается из функции карты.
Значение «вероятности» определяется в «области видимости» при вызове mapRreduce (...)
Подобное использование mapReduce также можно использовать с сегментированной базой данных.
Если вы хотите выбрать ровно 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) - количество документов в базе данных, а 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})
Когда я столкнулся с аналогичным решением, я отступил и обнаружил, что бизнес-запрос был на самом деле для создания некоторой формы ротации представляемого инвентаря. В этом случае есть гораздо лучшие варианты, на которые есть ответы от поисковых систем, таких как Solr, а не от хранилищ данных, таких как MongoDB.
Короче говоря, с требованием «разумной ротации» контента мы должны вместо случайного числа во всех документах включить личный модификатор q-оценки. Чтобы реализовать это самостоятельно, предполагая, что небольшая группа пользователей, вы можете сохранить документ для каждого пользователя, который имеет productId, количество показов, количество переходов по кликам, дату последнего посещения и любые другие факторы, которые бизнес считает значимыми для вычисления оценки aq. модификатор. При извлечении набора для отображения обычно вы запрашиваете из хранилища данных больше документов, чем запрошено конечным пользователем, затем применяете модификатор оценки q, берете количество записей, запрошенных конечным пользователем, затем рандомизируете страницу результатов, крошечный set, поэтому просто отсортируйте документы на уровне приложения (в памяти).
Если набор пользователей слишком велик, вы можете распределить пользователей по группам поведения и индексировать по группам поведения, а не по пользователям.
Если набор продуктов достаточно мал, вы можете создать индекс для каждого пользователя.
Я обнаружил, что этот метод намного более эффективен, но, что более важно, более эффективен в создании релевантного и полезного опыта использования программного решения.
ни одно из решений не сработало для меня. особенно когда много зазоров и набор небольшой. это сработало для меня очень хорошо (в php):
$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
find
+ skip
- это очень плохо, вы возвращаете все документы, чтобы выбрать один: S.
- person Martin Konecny; 28.07.2014
Моя сортировка / сортировка PHP / MongoDB по СЛУЧАЙНОМУ решению. Надеюсь, это кому-нибудь поможет.
Примечание. У меня есть числовые идентификаторы в моей коллекции MongoDB, которые относятся к записи базы данных MySQL.
Сначала я создаю массив из 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
]
];
Следующая операция агрегирования случайным образом выбирает 3 документа из коллекции:
db.users.aggregate ([{$ sample: {size: 3}}])
https://docs.mongodb.com/manual/reference/operator/aggregation/sample/
Если у вас есть простой ключ идентификатора, вы можете сохранить все идентификаторы в массиве, а затем выбрать случайный идентификатор. (Рубиновый ответ):
ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
Используя Map / Reduce, вы, безусловно, можете получить случайную запись, но не обязательно очень эффективно, в зависимости от размера результирующей отфильтрованной коллекции, с которой вы в конечном итоге работаете.
Я протестировал этот метод с 50 000 документов (фильтр уменьшает его примерно до 30 000), и он выполняется примерно за 400 мс на Intel i3 с оперативной памятью 16 ГБ и жестким диском SATA3 ...
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 просто создает массив идентификаторов всех документов, соответствующих запросу. В моем случае я проверил это примерно с 30 000 из 50 000 возможных документов.
Функция Reduce просто выбирает случайное целое число от 0 до количества элементов (-1) в массиве, а затем возвращает этот _id из массива.
400 мс - это долгий срок, и на самом деле, если у вас было пятьдесят миллионов записей вместо пятидесяти тысяч, это может увеличить накладные расходы до такой степени, что они станут непригодными для использования в многопользовательских ситуациях.
У MongoDB есть открытый вопрос, чтобы включить эту функцию в ядро ... https://jira.mongodb.org/browse/SERVER-533
Если бы этот «случайный» выбор был встроен в поиск по индексу вместо того, чтобы собирать идентификаторы в массив и затем выбирать один, это было бы невероятно полезно. (иди проголосуй!)
Это работает хорошо, быстро, работает с несколькими документами и не требует заполнения поля 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 вопрос отмечен как дубликат этого вопрос. Разница в том, что в этом вопросе явно задается вопрос об отдельной записи, а в другом - о получении случайных документов .
MongoDB теперь имеет $ rand
Чтобы выбрать n неповторяющихся элементов, объедините их с { $addFields: { _f: { $rand: {} } } }
, затем $sort
по _f
и $limit
n.
Лучший способ в Mongoose - выполнить вызов агрегирования с помощью $ sample. Однако Mongoose не применяет документы Mongoose к Aggregation, особенно если также необходимо применить populate ().
Для получения бережливого массива из базы данных:
/*
Sample model should be init first
const Sample = mongoose …
*/
const samples = await Sample.aggregate([
{ $match: {} },
{ $sample: { size: 33 } },
]).exec();
console.log(samples); //a lean Array
Чтобы получить массив документов мангуста:
const samples = (
await Sample.aggregate([
{ $match: {} },
{ $sample: { size: 27 } },
{ $project: { _id: 1 } },
]).exec()
).map(v => v._id);
const mongooseSamples = await Sample.find({ _id: { $in: samples } });
console.log(mongooseSamples); //an Array of mongoose documents
Если вы используете 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
получать документы случайным образом из любой коллекции.
вы также можете использовать shuffle-array после выполнения вашего запроса
var shuffle = require ('случайный массив');
Accounts.find (qry, function (err, results_array) {newIndexArr = shuffle (results_array);
Эффективно и надежно работает следующее:
Добавьте поле под названием «random» в каждый документ и присвойте ему случайное значение, добавьте индекс для случайного поля и действуйте следующим образом:
Предположим, у нас есть набор веб-ссылок, называемых «ссылками», и нам нужна случайная ссылка из нее:
link = db.links.find().sort({random: 1}).limit(1)[0]
Чтобы эта ссылка не появлялась во второй раз, обновите ее случайное поле новым случайным числом:
db.links.update({random: Math.random()}, link)