Поиск по postgres массивам в Rails

В рельсах, ActiveRecord позволяет хранить и извлекать структуры данных, как например массивы или хэши, используя тип колонки string или text и взаимодействуя с сериализацией на уровне приложения.

Это общий паттерн для работы с моделям, которые имеют поля tags или nicknames и для которых нужно выводить эти значения списком.

class Post < ActiveRecord::Base
  serialize :tags
end

Это неплохое решение, пока вам не потребуется найти все посты с тегом ‘hobbits’ или любым другим. Ну…

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

class Post
  scope :including_all_tags, -> (tags) { where(matching_tag_query(tags, ‘AND’)) }
 scope :including_any_tags, -> (tags) { where(matching_tag_query(tags, ‘OR’)) }
private
  def matching_tag_query(tags, condition_separator = ‘OR’)
    tags.map { |tag| “(tags LIKE ‘%#{tag}%’)” }.join(“ #  {condition_separator} “)
  end
end
# Использование
Post.including_all_tags([‘hobbits’, ‘gandalf’])
Post.including_any_tags([‘hobbits’, ‘gandalf’])

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

POSTGRESQL массивы

Postgres имеет встроенный тип для создания колонок типа массив. Что это дает для нашего рельсового приложения? Если мы храним массивы в отдельных колонках, то наша база и приложение могут без труда с ними работать. И что более важно, мы может использовать SQL для эффективного поиска внутри наших колонок-массивов.

Автор пишет, что нужно установить гем PostgresExt, но у меня заработало без него. Единственное, что необходимо при создании миграции, это добавить array: true аттрибут нужной колонке.

class PostgresHaveMyBabiesMigration < Migration
  def change
    create_table :posts do |t|
      t.text :tags, array: true
    end
  end
end

Теперь в модель не требуется добавлять serialize :tags

Overlap Operator

Overlap operator (&&) — полезный оператор, если вам необходимо сравнить несколько массивов на общие значения.

SQL:

SELECT *
FROM posts
WHERE posts.tags && ‘{hobbits,gandalf}’

Arel:

Post.where(Post.arel_table[:tags].overlap([‘hobbits’, ‘gandalf’]))
# or
Post.where.overlap(tags: [‘hobbits’, ‘gandalf’])

Эти запросы вернут все посты, которые имеют теги ‘hobbit’ или ‘gandalf’.

Contains Operator

Contains operator (@>) полезен, если нужно искать несколько тегов в определенном посте.

SQL:

SELECT *
FROM posts
WHERE posts.tags @> ‘{hobbits,gandalf}’

Arel:

Post.where(Post.arel_table[:tags].contains([‘hobbits’, ‘gandalf’]))
# or
Post.where.contains(tags: [‘hobbits’, ‘gandalf’])

Данные запросы вернут все посты, в которых встречается тег ‘hobbits’ совместно с тегом ‘gandalf’.

Примеры такой выборки через Overlap оператор

Оригинал статьи