Pull to refresh

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

Reading time2 min
Views6.1K
Недавно я столкнулся с одной довольно тривиальной задачей, где мне нужно было случайным образом выбирать из базы посты, написанные пользователями сайта. Проект написан на 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}
Tags:
Hubs:
Total votes 9: ↑6 and ↓3+3
Comments5

Articles