使用 SQL 物化視圖即時搜尋 Rails 和 ActiveRecord。
查看演示應用程式(程式碼在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
mixin 提供的。
ProductSearch . refresh!
您可以將此 ActiveRecord 關係/陣列傳遞到 Rails 視圖並渲染它們。您可以將其連接到其他表並套用更多範圍。
但SearchCraft 最大的功能是幫助您編寫物化視圖,然後迭代它們。
使用 ActiveRecord 表達式、Arel 表達式甚至純 SQL 來設計它們。無需回滾和重新運行遷移。不追蹤資料庫中的 SQL 視圖是否與 Rails 應用程式中的 SearchCraft 程式碼相符。 SearchCraft 將自動建立和更新您的物化視圖。
更新您的 SearchCraft 視圖,執行您的測試,它們可以運作。更新您的 SearchCraft 視圖,刷新您的開發應用程序,然後它就可以工作了。打開rails console
,它就可以工作了;然後更新您的視圖,輸入reload!
,並且它有效。部署到任何地方的生產中,它都可以工作。
使用 SearchCraft 設計物化視圖是什麼樣的?對於上面的ProductSearch
模型,我們建立一個繼承自SearchCraft::Builder
ProductSearchBuilder
類,並提供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 可以執行的任何其他操作。在上面的例子中我們:
products
表中選擇id
和name
列;稍後我們可以在應用程式中使用product_id
作為外鍵來連接到Product
模型reviews_count
和reviews_average
列,對product_reviews
表中的rating
列進行計數和平均。 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
索引方法。您也可以使用 Rails 中提供的其他索引方法,例如:gin
、 :gist
,或者如果您使用trigram
擴展,則可以使用:gin_trgm_ops
。當您考慮設定文字搜尋時,這些可能很有用,如下所述。
物化視圖的另一個好處是我們可以建立針對搜尋進行最佳化的列。例如上面的例子,由於我們已經在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 物化視圖,您可能會想要建立另一個依賴它的物化視圖。您也可以使用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 應用程式中,將 gem 新增到 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 約定,模型將尋找名為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! 並再次檢查你的查詢:
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
gem,您也會注意到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 提供了兩個 rake 任務:
rake searchcraft:refresh
- 刷新所有物化視圖rake searchcraft:rebuild
- 檢查是否需要重新建立任何視圖若要將這些新增至 Rails 應用程式中,請將以下內容新增至Rakefile
的底部:
SearchCraft . load_tasks
Builder
uilder子類,自動偵測物化視圖架構的變更並重新建立它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
檔案推送到ruby gems。
若要更改版本號:
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
,然後測試)變得很慢。它還引入了錯誤 - 我會忘記運行這些步驟,然後看到奇怪的行為。如果您有相對靜態的視圖或物化視圖,並且想要使用 Rails 遷移,請嘗試scenic
寶石。這個searchcraft
gem 仍然依賴scenic
的視圖refresh
功能,並將視圖加入到schema.rb
中。