Мгновенный поиск Rails и ActiveRecord с использованием материализованных представлений SQL.
См. демонстрационное приложение (код находится в папке demo_app/
):
Добавьте возможности молниеносного поиска в свои приложения Rails без использования внешних систем, таких как ElasticSearch. Теперь стало волшебно просто создавать выражения ActiveRecord/Arel, которые мы уже знаем и любим, и преобразовывать их в материализованные представления SQL, готовые к запросам и составлению с помощью ActiveRecord. Все, что вам нравится в Rails, но быстрее.
Что делает Rails медленным при поиске? Большие таблицы, множество объединений, подзапросов, отсутствующие или неиспользуемые индексы и сложные запросы. Тоже медленно? Координация данных из нескольких внешних систем через Ruby для получения результатов поиска.
SearchCraft упрощает написание и использование мощных материализованных представлений SQL для предварительного расчета результатов поиска и запросов отчетов. Это похоже на индекс базы данных, но для сложных запросов.
Материализованные представления — замечательная возможность PostgreSQL, Oracle и SQL Server*. Они представляют собой таблицу заранее рассчитанных результатов запроса. Они быстро запрашивают. Они потрясающие. Как и в других поисковых системах, вы сами контролируете, когда хотите обновить их новыми данными.
Внутри Rails и ActiveRecord вы можете получить доступ к материализованному представлению, доступному только для чтения, как к любой обычной таблице. Вы даже можете объединить их вместе. Вы можете использовать их в своих моделях, областях и ассоциациях ActiveRecord.
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
end
Сделанный. Какие бы столбцы вы ни описали в своем представлении, они станут атрибутами вашей модели.
Если в базовом представлении есть столбцы product_id
, product_name
, reviews_count
и reviews_average
, вы можете запросить его, как и любую другую модель ActiveRecord:
ProductSearch . all
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
ProductSearch . order ( reviews_average : :desc )
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
Если вы включаете внешние ключи, вы можете использовать ассоциации belongs_to
. Вы можете добавить области действия. Вы можете добавлять методы. Вы можете использовать его в качестве отправной точки для запросов к остальной части вашей базы данных SQL. Это обычная модель ActiveRecord.
Все это уже возможно с помощью Rails и ActiveRecord. Достижение SearchCraft — сделать жизнь с материализованными представлениями тривиальной. Тривиально обновлять их и писать.
Каждое материализованное представление SearchCraft представляет собой снимок результатов запроса на момент его создания или последнего обновления. Это похоже на таблицу, содержимое которой получено из запроса.
Если базовые данные вашего материализованного представления SearchCraft изменяются и вы хотите их обновить, вызовите refresh!
в вашем модельном классе. Это обеспечивается миксином SearchCraft::Model
.
ProductSearch . refresh!
Вы можете передать это отношение/массив ActiveRecord в свои представления Rails и визуализировать их. Вы можете объединить его с другими таблицами и применить дополнительные области действия.
Но самая замечательная особенность SearchCraft — это помощь в написании материализованных представлений , а затем в их работе.
Создавайте их с помощью выражений ActiveRecord, выражений Arel или даже простого SQL. Никаких миграций для отката и повторного запуска. Не нужно отслеживать, соответствует ли представление SQL в вашей базе данных коду SearchCraft в вашем приложении Rails. SearchCraft автоматически создаст и обновит ваши материализованные представления.
Обновите представление SearchCraft, запустите тесты, они работают. Обновите представление SearchCraft, обновите приложение для разработки, и все заработает. Откройте rails console
, и все работает; затем обновите свое представление, введите reload!
, и это работает. Развертывайте в производстве где угодно, и все работает.
Как выглядит создание материализованного представления с помощью SearchCraft? Для нашей модели ProductSearch
, описанной выше, мы создаем класс ProductSearchBuilder
, который наследуется от SearchCraft::Builder
и предоставляет либо метод view_scope
, либо метод view_select_sql
.
class ProductSearchBuilder < SearchCraft :: Builder
def view_scope
Product . where ( active : true )
. select (
"products.id AS product_id" ,
"products.name AS product_name" ,
"(SELECT COUNT(*) FROM product_reviews WHERE product_reviews.product_id = products.id) AS reviews_count" ,
"(SELECT AVG(rating) FROM product_reviews WHERE product_reviews.product_id = products.id) AS reviews_average"
)
end
end
Метод view_scope
должен возвращать отношение ActiveRecord. Это может быть настолько просто или сложно, насколько вам нравится. Он может использовать соединения, подзапросы и все остальное, что вы можете делать с помощью ActiveRecord. В приведенном выше примере мы:
id
и name
из таблицы products
; где мы позже сможем использовать product_id
в качестве внешнего ключа для присоединения к модели Product
в нашем приложении.reviews_count
и reviews_average
используя подзапросы SQL, которые подсчитывают и усредняют столбец rating
из таблицы product_reviews
. SearchCraft преобразует это в материализованное представление, создаст его в вашей базе данных, и модель ProductSearch
описанная выше, начнет использовать его, когда вы в следующий раз перезагрузите приложение для разработки или запустите тесты. Если вы внесете изменение, SearchCraft автоматически закроет и заново создаст представление.
Когда мы загружаем наше приложение в консоль Rails, запускаем тесты или обновляем приложение для разработки, модель ProductSearch
будет автоматически обновляться в соответствии с любыми изменениями в ProductSearchBuilder
.
ProductSearch . all
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
ProductSearch . order ( reviews_average : :desc )
[ #<ProductSearch product_id: 2, product_name: "iPhone 15", reviews_count: 5, reviews_average: 0.38e1>,
#<ProductSearch product_id: 1, product_name: "Laptop 3", reviews_count: 5, reviews_average: 0.28e1>,
#<ProductSearch product_id: 4, product_name: "Monopoly", reviews_count: 3, reviews_average: 0.2e1>]
Если вы хотите написать SQL, вместо этого вы можете использовать метод view_select_sql
.
class NumberBuilder < SearchCraft :: Builder
# Write SQL that produces 5 rows, with a 'number' column containing the number of the row
def view_select_sql
"SELECT generate_series(1, 5) AS number;"
end
end
class Number < ActiveRecord :: Base
include SearchCraft :: Model
end
Number . all
[ #<Number number: 1>, #<Number number: 2>, #<Number number: 3>, #<Number number: 4>, #<Number number: 5>]
Замечательная особенность материализованных представлений — к ним можно добавлять индексы; даже уникальные индексы.
В настоящее время механизм добавления индексов заключается в добавлении метода view_indexes
в ваш класс построителя.
Например, мы можем добавить уникальный индекс в number
столбец NumberBuilder
:
class NumberBuilder < SearchCraft :: Builder
def view_indexes
{
number : { columns : [ "number" ] , unique : true }
}
end
Или несколько индексов ProductSearchBuilder
ранее:
class ProductSearchBuilder < SearchCraft :: Builder
def view_indexes
{
id : { columns : [ "product_id" ] , unique : true } ,
product_name : { columns : [ "product_name" ] } ,
reviews_count : { columns : [ "reviews_count" ] } ,
reviews_average : { columns : [ "reviews_average" ] }
}
end
end
По умолчанию индексы будут using: :btree
. Вы также можете использовать другие методы индексации, доступные в рельсах, такие как :gin
, :gist
, или, если вы используете расширение trigram
вы можете использовать :gin_trgm_ops
. Они могут быть полезны при настройке текстового поиска, как описано ниже.
Еще одним преимуществом материализованных представлений является то, что мы можем создавать столбцы, оптимизированные для поиска. Например, выше, поскольку мы предварительно рассчитали reviews_average
в ProductSearchBuilder
, мы можем легко найти продукты с определенным средним рейтингом.
ProductSearch . where ( "reviews_average > 4" )
Замечательная особенность ActiveRecord — возможность объединять запросы. Поскольку наши материализованные представления являются собственными моделями ActiveRecord, мы можем объединять их с другими запросами.
Давайте настроим связь между ProductSearch#product_id
нашего MV и первичным ключом Product#id
таблицы:
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
belongs_to :product , foreign_key : :product_id , primary_key : :id
end
Теперь мы можем объединять или загружать таблицы вместе с помощью запросов ActiveRecord. To next возвращает отношение объектов ProductSearch
с каждой предварительно загруженной ассоциацией ProductSearch#product
.
ProductSearch . includes ( :product ) . where ( "reviews_average > 4" )
Следующий код возвращает объекты Product
на основе поиска в материализованном представлении ProductSearch
:
class Product
has_one :product_search , foreign_key : :product_id , primary_key : :id , class_name : "ProductSearch"
end
Product . joins ( :product_searches ) . merge (
ProductSearch . where ( "reviews_average > 4" )
)
PostgreSQL поставляется с решением для текстового поиска с использованием комбинации таких функций, как to_tsvector
, ts_rank
и websearch_to_tsquery
.
В ожидании дополнительных документов см. test/searchcraft/builder/test_text_search.rb
где приведен пример использования этих функций в ваших материализованных представлениях.
Я все еще работаю над извлечением этого решения из нашего кода в Store Connect.
Если у вас есть одно материализованное представление SearchCraft, вы можете захотеть создать другое, зависящее от него. Вы также можете сделать это с помощью depends_on
.
class SquaredBuilder < SearchCraft :: Builder
depends_on "NumberBuilder"
def view_select_sql
"SELECT number, number * number AS squared FROM #{ Number . table_name } ;"
end
end
class Squared < ActiveRecord :: Base
include SearchCraft :: Model
end
Если вы внесете изменения в NumberBuilder
, SearchCraft автоматически удалит и заново создаст материализованные представления Number
и Squared
.
Squared . all
[ #<Squared number: 1, squared: 1>,
#<Squared number: 2, squared: 4>,
#<Squared number: 3, squared: 9>,
#<Squared number: 4, squared: 16>,
#<Squared number: 5, squared: 25>]
Не уверены в написании сложных выражений SQL или Arel? Я тоже. Я спрашиваю GPT4 или GitHub Copilot. Я объясняю природу моей схемы и таблиц и прошу написать немного SQL, а затем прошу преобразовать его в Arel. Или я даю ему небольшой фрагмент SQL и прошу преобразовать его в Arel. Затем я копирую/вставляю результаты в свой класс построителя SearchCraft.
Абсолютно стоит научиться выражать поисковые запросы с помощью SQL или Arel и помещать их в материализованное представление SearchCraft. Ваши пользователи получат молниеносный опыт.
Внутри вашего приложения Rails добавьте драгоценный камень в свой Gemfile:
bundle add searchcraft
SearchCraft автоматически создаст необходимую внутреннюю таблицу БД, поэтому миграцию базы данных выполнять не придется. И, конечно же, он автоматически создаст и воссоздаст ваши материализованные представления.
В любом приложении Rails вы можете следовать этому руководству. Если у вас нет приложения Rails, используйте приложение, которое находится в папке demo_app
этого проекта.
Установите драгоценный камень:
bundle add searchcraft
Выберите одну из существующих моделей приложения, скажем Product
, и мы создадим для нее тривиальное материализованное представление. Скажем, нам нужен быстрый способ получить 5 самых продаваемых продуктов и некоторые детали, которые мы будем использовать для этого в нашем HTML-представлении.
Создайте новый файл модели ActiveRecord app/models/product_latest_arrival.rb
:
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Согласно соглашениям Rails, эта модель будет искать таблицу или представление SQL с именем product_latest_arrivals
. Этого пока не существует.
Мы можем подтвердить это, открыв rails console
и попытавшись запросить ее:
ProductLatestArrival . all
# ActiveRecord::StatementInvalid ERROR: relation "product_latest_arrivals" does not exist
Мы можем создать новый класс компоновщика SearchCraft, чтобы определить наше материализованное представление. Создайте новый файл app/searchcraft/product_latest_arrival_builder.rb
.
Я предлагаю вашим разработчикам app/searchcraft
, но они могут войти в любое app/
подпапку, автоматически загружаемую Rails.
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product . order ( created_at : :desc ) . limit ( 5 )
end
end
Внутри rails console``, run
reload!` и еще раз проверьте свой запрос:
reload!
ProductLatestArrival . all
ProductLatestArrival Load ( 1.3 ms ) SELECT "product_latest_arrivals" . * FROM "product_latest_arrivals"
=>
[ #<ProductLatestArrival:0x000000010a737d18
id : 1 ,
name : "Rustic Wool Coat" ,
active : true ,
created_at : Fri , 25 Aug 2023 07 :15 :16 . 995228000 UTC + 00 : 00 ,
updated_at : Fri , 25 Aug 2023 07 :15 :16 . 995228000 UTC + 00 : 00 ,
image_url : "https://loremflickr.com/g/320/320/coat?lock=1" > ,
...
Если в вашем Gemfile
установлен драгоценный камень annotate
, вы также заметите, что модель product_latest_arrival.rb
была обновлена, чтобы отразить столбцы в материализованном представлении.
# == Schema Information
#
# Table name: product_latest_arrivals
#
# id :bigint
# name :string
# active :boolean
# created_at :datetime
# updated_at :datetime
# image_url :string
#
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Если ваше приложение находится под контролем исходного кода, вы также можете увидеть, что db/schema.rb
обновлен и отражает последнее определение представления. Запустите git diff db/schema.rb
:
create_view "product_latest_arrivals" , materialized : true , sql_definition : <<-SQL
SELECT products.id,
products.name,
products.active,
products.created_at,
products.updated_at,
products.image_url
FROM products
LIMIT 5;
SQL
Теперь вы можете продолжить изменение view_scope
в своем конструкторе и запустить reload!
в консоли рельсов, чтобы проверить ваши изменения.
Например, вы можете select()
только те столбцы, которые вам нужны, используя выражение SQL для каждого из них:
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product
. order ( created_at : :desc )
. limit ( 5 )
. select (
"products.id as product_id" ,
"products.name as product_name" ,
"products.image_url as product_image_url" ,
)
end
end
Или вы можете использовать выражения Arel для построения SQL:
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product
. order ( created_at : :desc )
. limit ( 5 )
. select (
Product . arel_table [ :id ] . as ( "product_id" ) ,
Product . arel_table [ :name ] . as ( "product_name" ) ,
Product . arel_table [ :image_url ] . as ( "product_image_url" ) ,
)
end
end
А как насчет обновления данных? Давайте создадим больше Products
:
Product . create! ( name : "Starlink" )
Product . create! ( name : "Fishing Rod" )
Если бы вы проверили ProductLatestArrival.all
вы бы не нашли эти новые продукты. Это связано с тем, что материализованное представление представляет собой снимок данных на момент его создания или последнего обновления.
Чтобы обновить представление:
ProductLatestArrival . refresh!
Альтернативно, чтобы обновить все представления:
SearchCraft :: Model . refresh_all!
И подтвердите, что последние новинки теперь находятся в материализованном виде:
ProductLatestArrival . pluck ( :name )
=> [ "Fishing Rod" , "Starlink" , "Sleek Steel Bag" , "Ergonomic Plastic Bench" , "Fantastic Wooden Keyboard" ]
Если вы хотите удалить артефакты этого урока. Сначала удалите материализованное представление из схемы базы данных:
ProductLatestArrivalBuilder . new . drop_view!
Затем удалите файлы и git checkout .
чтобы отменить любые другие изменения.
rm app/searchcraft/product_latest_arrival_builder.rb
rm app/models/product_latest_arrival.rb
git checkout db/schema.rb
SearchCraft предоставляет две рейк-задачи:
rake searchcraft:refresh
— обновить все материализованные представленияrake searchcraft:rebuild
— проверяет, нужно ли воссоздавать какие-либо представления Чтобы добавить их в свое приложение Rails, добавьте следующее в конец вашего Rakefile
:
SearchCraft . load_tasks
Builder
и автоматически обнаруживает изменения для материализации схемы представления и воссоздает ее.refresh!
содержимого материализованного представленияdb/schema.rb
при каждом обновлении материализованного представления.annotate
gem.rake searchcraft:refresh
и проверка необходимости воссоздания каких-либо представлений rake searchcraft:rebuild
После проверки репозитория запустите bin/setup
, чтобы установить зависимости. Затем запустите rake test
чтобы запустить тесты. Вы также можете запустить bin/console
для получения интерактивного приглашения, которое позволит вам поэкспериментировать.
Чтобы установить этот драгоценный камень на свой локальный компьютер, запустите bundle exec rake install
. Чтобы выпустить новую версию, обновите номер версии в version.rb
, а затем запустите bundle exec rake release
, который создаст тег git для этой версии, отправит git commits и созданный тег, а затем отправит файл .gem
в Rubygems. орг.
Чтобы увеличить номер версии:
gem bump
, например, gem bump -v patch
demo_app/Gemfile.lock
, например (cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
rake release
релиз gem bump -v patch
(cd demo_app; bundle)
git add demo_app/Gemfile.lock; git commit --amend --no-edit
git push
rake release
Отчеты об ошибках и запросы на включение приветствуются на GitHub по адресу https://github.com/drnic/searchcraft. Этот проект призван стать безопасным и гостеприимным пространством для сотрудничества, и ожидается, что участники будут соблюдать кодекс поведения.
Гем доступен с открытым исходным кодом в соответствии с условиями лицензии MIT.
Ожидается, что все, кто взаимодействует с кодовыми базами проекта Searchcraft, системами отслеживания проблем, чатами и списками рассылки, будут следовать кодексу поведения.
rails db:rollback
, перестроение SQL миграции, rails db:migrate
и затем тестирование - стал медленным. Это также приводило к ошибкам: я забывал выполнить шаги и затем видел странное поведение. Если у вас относительно статические представления или материализованные представления и вы хотите использовать миграции Rails, попробуйте scenic
Geme. Этот драгоценный камень searchcraft
по-прежнему зависит от scenic
из-за функции refresh
представления и добавления представлений в schema.rb
.