SQL マテリアライズド ビューを使用した Rails と ActiveRecord の即時検索。
デモ アプリを参照してください (コードは、 demo_app/
フォルダーにあります)。
ElasticSearch などの外部システムを使用せずに、Rails アプリに超高速検索機能を追加します。私たちがすでに知っていて愛用している 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 ビューが Rails アプリ内の SearchCraft コードと一致するかどうかを追跡することはできません。 SearchCraft はマテリアライズド ビューを自動的に作成および更新します。
SearchCraft ビューを更新し、テストを実行すると、機能します。 SearchCraft ビューを更新し、開発アプリを更新すると、機能します。 rails console
開くと動作します。次にビューを更新し、 reload!
と入力します。 、そしてそれはうまくいきます。本番環境にどこにでもデプロイすれば、機能します。
SearchCraft を使用してマテリアライズド ビューをデザインするとどうなるでしょうか?上記のProductSearch
モデルでは、 SearchCraft::Builder
を継承し、 view_scope
メソッドまたはview_select_sql
メソッドのいずれかを提供するProductSearchBuilder
クラスを作成します。
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 で実行できるものはすべて使用できます。上の例では、次のことを行います。
products
テーブルからid
とname
列を選択します。ここで、後でproduct_id
アプリ内のProduct
モデルへの結合の外部キーとして使用できます。product_reviews
テーブルのrating
列をカウントして平均する SQL サブクエリを使用して、新しいreviews_count
列とreviews_average
列を構築します。 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
メソッドをビルダー クラスに追加することです。
たとえば、 NumberBuilder
のnumber
列に一意のインデックスを追加できます。
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
など、rails で利用できる他のインデックス作成メソッドを使用することもできます。また、 trigram
拡張機能を使用している場合は:gin_trgm_ops
を使用することもできます。これらは、以下で説明するように、テキスト検索の設定を検討するときに役立ちます。
マテリアライズド ビューのもう 1 つの利点は、検索用に最適化された列を作成できることです。たとえば、上記の例では、 ProductSearchBuilder
でreviews_average
事前に計算しているため、特定の平均評価を持つ製品を簡単に見つけることができます。
ProductSearch . where ( "reviews_average > 4" )
ActiveRecord の優れた機能は、クエリを結合できることです。マテリアライズド ビューはネイティブの ActiveRecord モデルであるため、他のクエリと結合できます。
MV のProductSearch#product_id
とテーブルProduct#id
主キーの間の関連付けを設定しましょう。
class ProductSearch < ActiveRecord :: Base
include SearchCraft :: Model
belongs_to :product , foreign_key : :product_id , primary_key : :id
end
ActiveRecord クエリを使用してテーブルを結合、つまり一括ロードできるようになりました。以下は、 ProductSearch
オブジェクトのリレーションを返します。各ProductSearch#product
関連付けはプリロードされています。
ProductSearch . includes ( :product ) . where ( "reviews_average > 4" )
以下は、 ProductSearch
マテリアライズド ビューの検索に基づいて、 Product
オブジェクトを返します。
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 マテリアライズド ビューを 1 つ作成したら、それに依存する別のビューを作成することもできます。これは、 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 に gem を追加します。
bundle add searchcraft
SearchCraft は必要な内部 DB テーブルを自動的に作成するため、データベースの移行を実行する必要はありません。そしてもちろん、マテリアライズド ビューが自動的に作成および再作成されます。
Rails アプリ内では、このチュートリアルに従うことができます。 Rails アプリをお持ちでない場合は、このプロジェクトのdemo_app
フォルダーにあるアプリを使用してください。
gem をインストールします。
bundle add searchcraft
既存のアプリケーション モデルの 1 つ (たとえばProduct
を選択すると、その単純な具体化されたビューが作成されます。たとえば、売上トップ 5 の製品と、そのために HTML ビューで使用する詳細をすばやく取得する方法が必要だとします。
新しい ActiveRecord モデル ファイルapp/models/product_latest_arrival.rb
を作成します。
class ProductLatestArrival < ActiveRecord :: Base
include SearchCraft :: Model
end
Rails の規則により、このモデルはproduct_latest_arrivals
という SQL テーブルまたはビューを探します。これはまだ存在しません。
これを確認するには、 rails console
開いてクエリを実行します。
ProductLatestArrival . all
# ActiveRecord::StatementInvalid ERROR: relation "product_latest_arrivals" does not exist
新しい SearchCraft ビルダー クラスを作成して、マテリアライズド ビューを定義できます。新しいファイルapp/searchcraft/product_latest_arrival_builder.rb
を作成します。
ビルダーにはapp/searchcraft
お勧めしますが、Rails によって自動ロードされる任意のapp/
サブフォルダーに入ることができます。
class ProductLatestArrivalBuilder < SearchCraft :: Builder
def view_scope
Product . order ( created_at : :desc ) . limit ( 5 )
end
end
rails console``, run
、クエリを再度確認します。
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" > ,
...
annotate
gem をGemfile
にインストールしている場合は、マテリアライズド ビューの列を反映するために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!
Rails コンソールで変更をテストします。
たとえば、各列に対して SQL 式を使用して、必要な列のみselect()
ことができます。
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 は 2 つの rake タスクを提供します。
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
実行して対話型プロンプトを表示し、実験することもできます。
この gem をローカル マシンにインストールするには、 bundle exec rake install
実行します。新しいバージョンをリリースするには、 version.rb
のバージョン番号を更新してから、 bundle exec rake release
を実行します。これにより、そのバージョンの git タグが作成され、git コミットと作成されたタグがプッシュされ、 .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) で受け付けています。このプロジェクトは、安全で居心地の良いコラボレーションの場となることを目的としており、貢献者は行動規範を遵守することが期待されています。
この gem は、MIT ライセンスの条件に基づいてオープン ソースとして利用できます。
Searchcraft プロジェクトのコードベース、問題トラッカー、チャット ルーム、メーリング リストでやり取りするすべての人は、行動規範に従うことが期待されます。
rails db:rollback
、移行 SQL の再構築、 rails db:migrate
、そして test) が遅くなりました。また、バグも発生しました。手順を実行するのを忘れてしまい、奇妙な動作が発生することがありました。比較的静的なビューまたはマテリアライズド ビューがあり、Rails の移行を使用したい場合は、 scenic
gem を試してください。このsearchcraft
gem は、ビューrefresh
機能とschema.rb
へのビューの追加で依然としてscenic
に依存しています。