使用 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
子类,自动检测物化视图架构的更改并重新创建它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
,然后测试)变得很慢。它还引入了错误 - 我会忘记运行这些步骤,然后看到奇怪的行为。如果您有相对静态的视图或物化视图,并且想要使用 Rails 迁移,请尝试scenic
宝石。这个searchcraft
gem 仍然依赖于scenic
的视图refresh
功能,并将视图添加到schema.rb
中。