Случайная запись из MongoDB

257

Я хочу получить случайную запись из огромной (100 миллионов записей) mongodb.

Каков самый быстрый и эффективный способ сделать это? Данные уже есть, и нет поля, в котором я могу создать случайное число и получить случайную строку.

Любые предложения?

  • 3
    На что вы индексируете?
  • 2
    См. Также этот вопрос, озаглавленный «Упорядочение набора результатов случайным образом в монго» . Размышление о случайном порядке набора результатов является более общей версией этого вопроса - более мощной и более полезной.
Показать ещё 4 комментария
Теги:

25 ответов

163

Начиная с версии 3.2 MongoDB, вы можете получить N случайных документов из коллекции, используя оператор конвейера агрегации $sample:

// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])
  • 10
    Это хороший способ, но помните, что он НЕ гарантирует, что в образце не будет копий одного и того же объекта.
  • 7
    @MatheusAraujo, который не имеет значения, если вы хотите одну запись, но в любом случае, хорошая точка
Показать ещё 15 комментариев
115

Сделайте счетчик всех записей, создайте случайное число между 0 и счетчиком, а затем выполните:

db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
  • 135
    К сожалению, функция skip () довольно неэффективна, поскольку она сканирует столько документов. Кроме того, существует условие гонки, если между получением счетчика и выполнением запроса удаляются строки.
  • 6
    Обратите внимание, что случайное число должно быть между 0 и количеством (исключая). Т.е., если у вас есть 10 элементов, случайное число должно быть в диапазоне от 0 до 9. В противном случае курсор может попытаться пропустить последний элемент, и ничего не будет возвращено.
Показать ещё 7 комментариев
78

Обновление для MongoDB 3.2

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 к нему и добавьте его снова.

  • 7
    А вот простой способ добавить случайное поле к каждому документу в коллекции. function setRandom () {db.topics.find (). forEach (function (obj) {obj.random = Math.random (); db.topics.save (obj);}); } db.eval (setRandom);
  • 0
    Запрос о функции был вновь открыт, но еще не запланирован.
Показать ещё 11 комментариев
52

Вы также можете использовать функцию геопространственной индексации 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 )

Для этого требуется только один запрос и нулевые проверки, а также код чистый, простой и гибкий. Вы даже можете использовать Y-ось геотома, чтобы добавить к вашему запросу второе измерение случайности.

  • 8
    Мне нравится этот ответ. Это самый эффективный из тех, что я видел, который не требует много работы на стороне сервера.
  • 3
    Это также смещено к документам, которые, как оказалось, имеют несколько точек в их окрестностях.
Показать ещё 2 комментария
19

Следующий рецепт немного медленнее, чем решение поваренной книги монго (добавить случайный ключ в каждый документ), но возвращает более равномерно распределенные случайные документы. Он немного менее равномерно распределен, чем решение 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) и генерирует более равномерные случайные документы, чем метод "поваренной книги", сообщенный Майклом:

Для коллекции с 1 000 000 элементов:

  • Этот метод занимает меньше миллисекунды на моей машине.

  • Метод skip() в среднем занимает 180 мс

Метод поваренной книги приведет к тому, что большое количество документов никогда не будет выбрано, потому что их случайное число не одобряет их.

  • Этот метод будет выбирать все элементы равномерно с течением времени.

  • В моем тесте он был всего на 30% медленнее, чем метод поваренной книги.

  • случайность не совершенна на 100%, но она очень хороша (и при необходимости ее можно улучшить)

Этот рецепт не идеален - идеальное решение будет встроенной функцией, как отмечали другие.
Однако это должно быть хорошим компромиссом для многих целей.

7

Ниже приведен способ использования значений 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];

Это общая логика в представлении оболочки и легко адаптируемая.

Итак, в точках:

  • Найдите минимальные и максимальные значения первичного ключа в коллекции

  • Создайте случайное число, которое находится между отметками времени этих документов.

  • Добавьте случайное число к минимальному значению и найдите первый документ, который больше или равен этому значению.

В этом случае используется значение "padding" из значения timestamp в "hex", чтобы сформировать действительное значение ObjectId, так как это то, что мы ищем. Использование целых чисел в качестве значения _id существенно проще, но та же основная идея в точках.

7

В Python с использованием pymongo:

import random

def get_random_doc():
    count = collection.count()
    return collection.find()[random.randrange(count)]
  • 5
    Стоит отметить, что внутри, это будет использовать пропустить и ограничить, как и многие другие ответы.
6

это сложно, если нет данных, которые можно отключить. что такое _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);
  • 0
    Любые идеи, как это будет выглядеть в PHP? или хотя бы какой язык вы использовали выше? это питон?
5

Теперь вы можете использовать агрегат. Пример:

db.users.aggregate(
   [ { $sample: { size: 3 } } ]
)

См. Документ.

  • 1
    Примечание: $ sample может получать один и тот же документ более одного раза
5

Вы можете выбрать случайную метку времени и выполнить поиск первого объекта, который был создан впоследствии. Он будет сканировать только один документ, хотя он не обязательно дает вам равномерное распределение.

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)
    };
}();
  • 0
    Было бы легко исказить случайную дату, чтобы учесть суперлинейный рост базы данных.
  • 0
    это лучший метод для очень больших коллекций, он работает с O (1), unline skip () или count (), используемыми в других решениях здесь
3

Чтобы получить определенное количество случайных документов без дубликатов:

  • сначала получить все идентификаторы
  • получить размер документов
  • цикл, получающий случайный индекс и пропускающий дублированный

    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)
                });
            });
    
3

Мое решение по 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;
}
2

Используя Python (pymongo), функция агрегата также работает.

collection.aggregate([{'$sample': {'size': sample_size }}])

Этот подход намного быстрее, чем запуск запроса для случайного числа (например, collection.find([random_int]). Это особенно касается больших коллекций.

2

Вы можете выбрать случайный _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)
            })
        })
    })

Здесь вам не нужно тратить пространство на хранение случайных чисел в коллекции.

2

Я бы предложил использовать 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, подобное этому, также должно быть использовано на sharded db.

Если вы хотите выбрать ровно n из m документов из db, вы можете сделать это следующим образом:

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) - количество документов для извлечения.

Этот подход может привести к некоторым проблемам с оштрафованными базами данных.

  • 4
    Выполнение полного сканирования коллекции для возврата 1 элемента ... это должно быть наименее эффективным способом сделать это.
  • 1
    Хитрость в том, что это общее решение для возврата произвольного числа случайных элементов - в этом случае оно будет быстрее, чем другие решения, если получено> 2 случайных элемента.
2

Я бы предложил добавить случайное поле int каждому объекту. Тогда вы можете просто сделать

findOne({random_field: {$gte: rand()}}) 

выбрать случайный документ. Просто убедитесь, что вы гарантируетеIndex ({random_field: 1})

  • 0
    вероятный метод в этом: cookbook.mongodb.org/patterns/random-attribute
  • 2
    Если первая запись в вашей коллекции имеет относительно высокое значение random_field, будет ли оно возвращаться почти все время?
Показать ещё 3 комментария
1

Если вы используете мангуст, вы можете использовать mongoose-random mongoose-random

1

не решения для меня хорошо работали. особенно когда есть много пробелов, и набор мал. это работало очень хорошо для меня (в php):

$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
  • 0
    Вы указываете язык, но не библиотеку, которую используете?
  • 0
    К вашему сведению, здесь есть условие гонки, если документ удален между первой и третьей строкой. Также find + skip довольно плохо, вы возвращаете все документы, чтобы выбрать один: S.
1

Когда я столкнулся с аналогичным решением, я отступил и обнаружил, что бизнес-запрос был фактически предназначен для создания некоторой формы ротации представленного инвентаря. В этом случае есть намного лучшие варианты, в которых есть ответы от поисковых систем, таких как Solr, а не на хранилища данных, такие как MongoDB.

Короче говоря, с требованием "разумно вращать" контент, что мы должны делать вместо случайного числа во всех документах, это включить персональный модификатор оценки q. Чтобы реализовать это самостоятельно, принимая небольшое количество пользователей, вы можете хранить документ для каждого пользователя, у которого есть productId, количество показаний, количество кликов, дата последнего просмотра и любые другие факторы, которые бизнес считает значимыми для вычисления оценки aq модификатор. При получении набора для отображения обычно вы запрашиваете больше документов из хранилища данных, чем запрашивается конечным пользователем, затем применяйте модификатор q-оценки, берете количество записей, запрашиваемых конечным пользователем, затем производите рандомизацию страницы результатов, крошечную set, поэтому просто отсортируйте документы на прикладном уровне (в памяти).

Если юниверс пользователей слишком велик, вы можете классифицировать пользователей по группам поведения и индексировать по группам поведения, а не пользователю.

Если юниверс продуктов достаточно мал, вы можете создать индекс для каждого пользователя.

Я нашел этот метод намного более эффективным, но, что более важно, более эффективным в создании актуального и полезного опыта использования программного решения.

0

Мой PHP сортировка/заказ по случайному решению. Надеюсь, это кому-нибудь поможет.

Примечание: в моей коллекции 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 ] ] ] ],
        ],
    ];

После этого вы можете использовать сортировку Pipeline.

    $aggregate[] = [
        '$sort' => [
            'random_sort' => 1
        ]
    ];
0

Это работает хорошо, быстро, работает с несколькими документами и не требует заполнения поля rand, которое в конечном итоге заселоте себя:

  • добавить индекс в поле .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.

0

Используя 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 возможных документов.

Функция "Уменьшение" просто выбирает случайное целое число от 0 до количества элементов (-1) в массиве и затем возвращает этот массив _id.

400 мс звучит как долгое время, и это действительно так, если у вас было пятьдесят миллионов записей вместо пятидесяти тысяч, это может увеличить накладные расходы до того момента, когда он станет непригодным в многопользовательских ситуациях.

Есть открытая проблема для MongoDB, чтобы включить эту функцию в ядро ​​... https://jira.mongodb.org/browse/SERVER-533

Если этот "случайный" выбор был встроен в индексный поиск вместо того, чтобы собирать идентификаторы в массив, а затем выбирать один, это поможет невероятно. (проголосуйте!)

0

Если у вас есть простой ключ id, вы можете сохранить весь идентификатор в массиве, а затем выбрать случайный идентификатор. (Ответ Ruby):

ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
-3

Если вы используете mongoid, оболочку document-to-object, вы можете сделать следующее в Рубин. (Предположим, что ваша модель - Пользователь)

User.all.to_a[rand(User.count)]

В моем .irbrc у меня есть

def rando klass
    klass.all.to_a[rand(klass.count)]
end

поэтому в консоли rails я могу сделать, например,

rando User
rando Article

для случайного получения документов из любой коллекции.

  • 1
    Это ужасно неэффективно, так как будет считывать всю коллекцию в массив, а затем выбирать одну запись.
  • 0
    Хорошо, может быть, неэффективно, но, конечно, удобно. попробуйте это, если ваш размер данных не слишком велик
Показать ещё 1 комментарий
-7

Что работает эффективно и надежно:

Добавьте поле "случайный" в каждый документ и назначьте ему случайное значение, добавьте индекс для случайного поля и действуйте следующим образом:

Предположим, что у нас есть набор веб-ссылок, называемых "ссылками", и мы хотим получить от него случайную ссылку:

link = db.links.find().sort({random: 1}).limit(1)[0]

Чтобы убедиться, что одна и та же ссылка не появится во второй раз, обновите ее случайное поле новым случайным числом:

db.links.update({random: Math.random()}, link)
  • 2
    Зачем обновлять базу данных, когда вы можете просто выбрать другой случайный ключ?
  • 0
    У вас может не быть списка ключей для случайного выбора.
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню