Поиск по 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 оператор

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

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.