Выборка случайных документов из коллекции MongoDB

  • Tutorial
Недавно я столкнулся с одной довольно тривиальной задачей, где мне нужно было случайным образом выбирать из базы посты, написанные пользователями сайта. Проект написан на Rails с использованием MongoDB в качестве базы данных и джем mongoid для работы с ней. Не то что бы задача была сложной для выполнения, но в то же время, на удивление, нет абсолютно простого решения на подобие sort_by_random или вроде того. Под катом пару примеров как это можно решить.


Для начала давайте рассмотрим простой способ решения задачи. В монгоиде есть метод, который позволяет пропустить несколько записей или, другими словами, установить курсор для точки отсчета. Этот метод называется skip и ему можно передать количество записей, которые стоит пропустить. Если у нас есть коллекция с тремя записями, то чтобы получить вторую, можно сделать что-то наподобие этого Post.skip(1).first. Зная количество документов в коллекции, мы можем сделать сдвиг на случайное количество документов и начать читать оттуда:
proxy = Post.where(...)
skip = rand(proxy.count - COUNT_OF_POSTS_TO_SHOW)
@posts = proxy.skip(skip).limit(COUNT_OF_POSTS_TO_SHOW)


Если у вас не будет специальных условий по которым вы делаете выборку, то код будет выглядеть проще. Обычно, некоторые условия все таки будут присутствовать, такие как дата создания или статус. Данная выборка довольно таки случайна, но не совсем, так как мы выбираем случайным образом точку отсчета, а дальше все документы идут подряд. Возможно, кому-то подойдет и этот вариант случайности, особенно если нужно выбрать только одну запись. Но данный метод может быть абсолютно неприемлемым в случаях, когда мы выбираем товары, показывая таким образом товары из одинаковой категории или с одинаковой ценой (в зависимости от индексов коллекции)
Мое решение для получения абсолютно случайных записей было немного сложнее, но давало более корректные результаты. Для этого мне понадобилось добавить новое поле к коллекции из которой делалась выборка, я назвал его rand_order. В него мы записывали случайное число с плавающей точкой от 0 до 1. Наиболее аккуратный способ заполнения этого поля, это добавить before_save фильтр для модели, который может выглядеть таким образом:
 def set_rand_order
    self.rand_order = (rand 0.0..1).round(15) unless rand_order
  end


Таким образом, каждый раз при сохранении объекта, мы проверяем заполнено ли значение для поля rand_order и заполняем его если оно пустое. Получение случайных записей теперь будет происходить таким образом:
proxy = Post.where(...)
skip = rand(proxy.count - COUNT_OF_POSTS_TO_SHOW)
@posts = proxy.asc(:rand_order).skip(skip).limit(COUNT_OF_POSTS_TO_SHOW)


Стоит брать во внимание, что если вы применяете этот способ для уже существующей коллекции, которая содержит документы, то для них нужно сгенерировать случайные числа для поля rand_order. Это можно сделать в миграции и с учетом того, что мы сделали это в before_filter, вам достаточно вызвать метод save для каждого из объектов:
Post.all.each{|p| p.save}
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 5

    +1
    У вас получается одна и та же выборка, если делать просто asc(:rand_order), не?
    Вот вариант рабочий cookbook.mongodb.org/patterns/random-attribute/, возможно я не допонял что-то в вашем методе.
      0
      да, вы правы, сейчас исправлю. Во втором методе так же нужно делать skip на случайное количество записей
        +1
        skip на случайное количество записей тоже не спасет, ибо последовательность, определенная этим рандомным полем будет отсортирована всегда одинаково, и рандомный skip лишь сместит начало цепочки, а дальше записи будут идти последовательно… Либо надо запрашивать COUNT_OF_POSTS_TO_SHOW раз по одной записи, либо как по ссылке через mapReduce
          0
          да, вы правы, это сделает выборку только чуть более случайной и избавит именно от той проблемы, что рядом окажутся, например товары одной категории или с одинаковой ценой. Собственно, пример, который я описал в статье
        0
        Вы знаете, мне их решение из cookbook, на которое вы ссылаетесь, не кажется таким уж хорошим. По сути, они тоже делают генерацию случайного числа для каждого документа, после чего, для вывода случайной записи, генерируют новое случайное число и выбирают те записи у которых это число больше (или меньше, это без разницы). Это может привести к ситуации, что число, которое мы генерируем на лету, очень большое или очень маленькое и в ответ мы просто не получим то количество записей, на которое рассчитываем.
        В моем варианте, я позволяю себе использовать тяжелую для базы операцию skip, но это дает гарантии того, что мы получим в ответ то количество записей, которое ожидаем.
        Собственно, у каждого из решений есть свои минусы, которые нужно учитывать, при выборе.

      Only users with full accounts can post comments. Log in, please.