SearchCop 扩展您的 ActiveRecord 模型,以支持全文搜索引擎,例如通过简单查询字符串和基于哈希的查询进行查询。假设您有一个Book
模型,具有各种属性,如title
、 author
、 stock
、 price
、 available
。使用 SearchCop 您可以执行:
Book . search ( "Joanne Rowling Harry Potter" )
Book . search ( "author: Rowling title:'Harry Potter'" )
Book . search ( "price > 10 AND price < 20 -stock:0 (Potter OR Rowling)" )
# ...
因此,您可以将搜索查询字符串分发给您的模型,而您、您的应用程序的管理员和/或用户将获得强大的查询功能,而无需集成其他第三方搜索服务器,因为 SearchCop 可以使用 RDBMS 的全文索引功能与数据库无关的方式(当前支持 MySQL 和 PostgreSQL 全文索引)并优化查询以充分利用它们。阅读下文了解更多内容。
还支持复杂的基于哈希的查询:
Book . search ( author : "Rowling" , title : "Harry Potter" )
Book . search ( or : [ { author : "Rowling" } , { author : "Tolkien" } ] )
Book . search ( and : [ { price : { gt : 10 } } , { not : { stock : 0 } } , or : [ { title : "Potter" } , { author : "Rowling" } ] ] )
Book . search ( or : [ { query : "Rowling -Potter" } , { query : "Tolkien -Rings" } ] )
Book . search ( title : { my_custom_sql_query : "Rowl" } } )
# ...
将此行添加到应用程序的 Gemfile 中:
gem 'search_cop'
然后执行:
$ bundle
或者自己安装:
$ gem install search_cop
要为模型启用 SearchCop, include SearchCop
并指定要向search_scope
内的搜索查询公开的属性:
class Book < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :title , :description , :stock , :price , :created_at , :available
attributes comment : [ "comments.title" , "comments.message" ]
attributes author : "author.name"
# ...
end
has_many :comments
belongs_to :author
end
当然,您也可以根据需要指定多个search_scope
块:
search_scope :admin_search do
attributes :title , :description , :stock , :price , :created_at , :available
# ...
end
search_scope :user_search do
attributes :title , :description
# ...
end
SearchCop 解析查询并将其以与数据库无关的方式映射到 SQL 查询。因此,SearchCop 不绑定到特定的 RDBMS。
Book . search ( "stock > 0" )
# ... WHERE books.stock > 0
Book . search ( "price > 10 stock > 0" )
# ... WHERE books.price > 10 AND books.stock > 0
Book . search ( "Harry Potter" )
# ... WHERE (books.title LIKE '%Harry%' OR books.description LIKE '%Harry%' OR ...) AND (books.title LIKE '%Potter%' OR books.description LIKE '%Potter%' ...)
Book . search ( "available:yes OR created_at:2014" )
# ... WHERE books.available = 1 OR (books.created_at >= '2014-01-01 00:00:00.00000' and books.created_at <= '2014-12-31 23:59:59.99999')
SearchCop 使用 ActiveSupport 的 begin_of_year 和 end_of_year 方法来获取用于构建本例 SQL 查询的值。
当然,这些LIKE '%...%'
查询不会实现最佳性能,但请查看下面有关 SearchCop 全文功能的部分,以了解如何优化生成的查询。
由于Book.search(...)
返回ActiveRecord::Relation
,您可以自由地以各种可能的方式对搜索结果进行预处理或后处理:
Book . where ( available : true ) . search ( "Harry Potter" ) . order ( "books.id desc" ) . paginate ( page : params [ :page ] )
当您将查询字符串传递给 SearchCop 时,它会被解析、分析和映射,最终构建 SQL 查询。更准确地说,当 SearchCop 解析查询时,它会创建代表查询表达式的对象(节点)(And-、Or-、Not-、String-、Date- 等节点)。为了构建 SQL 查询,SearchCop 使用访问者的概念,例如在 Arel 中使用的概念,这样,对于每个节点都必须有一个访问者,它将节点转换为 SQL。当没有访问者时,当查询构建器尝试“访问”该节点时会引发异常。访问者负责清理用户提供的输入。这主要是通过引用(字符串、表名、列引用等)来完成的。 SearchCop 使用 ActiveRecord 连接适配器提供的方法进行清理/引用以防止 SQL 注入。虽然我们永远不可能 100% 免受安全问题的影响,但 SearchCop 非常重视安全问题。如果您发现任何与安全相关的问题,请通过 flakks dot com 的安全部门负责任地报告。
SearchCop 支持 MySQL 的 json 字段,以及 postgres 的 json、jsonb 和 hstore 字段。目前,字段值始终应为字符串,并且不支持数组。您可以通过以下方式指定 json 属性:
search_scope :search do
attributes user_agent : "context->browser->user_agent"
# ...
end
其中context
是 json/jsonb 列,例如包含:
{
"browser" : {
"user_agent" : " Firefox ... "
}
}
默认情况下,即如果您不告诉 SearchCop 有关全文索引的信息,SearchCop 将使用LIKE '%...%'
查询。不幸的是,除非您创建 trigram 索引(仅限 postgres),否则这些查询不能使用 SQL 索引,因此当您搜索Book.search("Harry Potter")
或类似内容时,RDBMS 需要扫描每一行。为了避免LIKE
查询的损失,SearchCop 可以利用 MySQL 和 PostgreSQL 的全文索引功能。要使用现有的全文索引,只需告诉 SearchCop 通过以下方式使用它们:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes :title , :author
options :title , :type => :fulltext
options :author , :type => :fulltext
end
# ...
end
然后,SearchCop 将透明地将其对具有全文索引的属性的 SQL 查询更改为:
Book . search ( "Harry Potter" )
# MySQL: ... WHERE (MATCH(books.title) AGAINST('+Harry' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Harry' IN BOOLEAN MODE)) AND (MATCH(books.title) AGAINST ('+Potter' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Potter' IN BOOLEAN MODE))
# PostgreSQL: ... WHERE (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Harry')) AND (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Potter') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Potter'))
显然,这些查询并不总是返回与通配符LIKE
查询相同的结果,因为我们搜索单词而不是子字符串。然而,全文索引通常会提供更好的性能。
而且,上面的查询还不完善。为了进一步改进它,SearchCop 尝试优化查询以充分利用全文索引,同时仍然允许将它们与非全文属性混合。为了进一步改进查询,您可以对属性进行分组并指定要搜索的默认字段,这样 SearchCop 就不必再在所有字段中搜索:
search_scope :search do
attributes all : [ :author , :title ]
options :all , :type => :fulltext , default : true
# Use default: true to explicitly enable fields as default fields (whitelist approach)
# Use default: false to explicitly disable fields as default fields (blacklist approach)
end
现在 SearchCop 可以优化以下尚未优化的查询:
Book . search ( "Rowling OR Tolkien stock > 1" )
# MySQL: ... WHERE ((MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Rowling' IN BOOLEAN MODE)) OR (MATCH(books.author) AGAINST('+Tolkien' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE))) AND books.stock > 1
# PostgreSQL: ... WHERE ((to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Rowling')) OR (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Tolkien') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien'))) AND books.stock > 1
以下是性能更高的查询:
Book . search ( "Rowling OR Tolkien stock > 1" )
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('Rowling Tolkien' IN BOOLEAN MODE) AND books.stock > 1
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', 'Rowling | Tokien') and books.stock > 1
这里发生了什么?好吧,我们将all
指定为由author
和title
组成的属性组的名称。此外,由于我们将all
指定为全文属性,SearchCop 假设在author
和title
上存在复合全文索引,以便相应地优化查询。最后,我们将all
指定为要搜索的默认属性,这样 SearchCop 就可以忽略其他属性,例如stock
,只要它们没有在查询中直接指定(例如stock > 0
)。
其他查询将以类似的方式进行优化,例如 SearchCop 尝试最小化查询中的全文约束,即 MySQL 的MATCH() AGAINST()
和 PostgreSQL 的to_tsvector() @@ to_tsquery()
。
Book . search ( "(Rowling -Potter) OR Tolkien" )
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('(+Rowling -Potter) Tolkien' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', '(Rowling & !Potter) | Tolkien')
要在 MySQL 中为books.title
创建全文索引,只需使用:
add_index :books , :title , :type => :fulltext
关于复合索引,例如将用于我们上面已经指定的默认all
,请使用:
add_index :books , [ :author , :title ] , :type => :fulltext
请注意,MySQL 支持 MyISAM 的全文索引,从 MySQL 5.6+ 版本开始,还支持 InnoDB。有关 MySQL 全文索引的更多详细信息,请访问 http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html
关于 PostgreSQL,有更多创建全文索引的方法。然而,最简单的方法之一是:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
此外,对于 PostgreSQL,您应该更改config/application.rb
中的模式格式:
config . active_record . schema_format = :sql
关于 PostgreSQL 的复合索引,请使用:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
要使用 PostgreSQL 正确处理 NULL 值,请在索引创建时和指定search_scope
时使用 COALESCE :
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', COALESCE(author, '') || ' ' || COALESCE(title, '')))"
加:
search_scope :search do
attributes :title
options :title , :type => :fulltext , coalesce : true
end
要使用simple
以外的其他 PostgreSQL 字典,您必须相应地创建索引,并且需要告诉 SearchCop,例如:
search_scope :search do
attributes :title
options :title , :type => :fulltext , dictionary : "english"
end
有关 PostgreSQL 全文索引的更多详细信息,请访问 http://www.postgresql.org/docs/9.3/static/textsearch.html
如果您向搜索查询(价格、股票等)公开非全文属性,则相应的查询(例如Book.search("stock > 0")
将从通常的非全文索引中受益。因此,您应该在向搜索查询公开的每一列上添加一个常用索引,并为每个全文属性添加一个全文索引。
如果您无法使用全文索引,因为您在使用 InnoDB 或其他没有全文支持的 RDBMS 时仍使用 MySQL 5.5,则可以让您的 RDBMS 对字符串列使用常用的非全文索引(如果不需要) LIKE
查询中留下通配符。只需提供以下选项:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username
options :username , left_wildcard : false
end
# ...
这样 SearchCop 将省略最左边的通配符。
User . search ( "admin" )
# ... WHERE users.username LIKE 'admin%'
同样,您也可以禁用右侧通配符:
search_scope :search do
attributes :username
options :username , right_wildcard : false
end
当您在搜索范围上定义多个字段时,SearcCop 将默认使用 AND 运算符来连接条件,例如:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username , :fullname
end
# ...
end
因此,像User.search("something")
这样的搜索将生成具有以下条件的查询:
... WHERE username LIKE ' %something% ' AND fullname LIKE ' %something% '
但是,在某些情况下,不需要使用 AND 作为默认运算符,因此 SearchCop 允许您覆盖它并使用 OR 作为默认运算符。像User.search("something", default_operator: :or)
这样的查询将使用 OR 连接条件来生成查询
... WHERE username LIKE ' %something% ' OR fullname LIKE ' %something% '
最后,请注意,您也可以将其应用于全文索引/查询。
如果您指定来自另一个模型的可搜索属性,例如
class Book < ActiveRecord :: Base
# ...
belongs_to :author
search_scope :search do
attributes author : "author.name"
end
# ...
end
当您执行Book.search(...)
时,SearchCop 默认情况下会eager_load
引用的关联。如果您不希望自动eager_load
或需要执行特殊操作,请指定scope
:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
scope { joins ( :author ) . eager_load ( :comments ) } # etc.
end
# ...
end
然后,SearchCop 将跳过任何关联自动加载并使用范围。您还可以将scope
与aliases
一起使用来执行任意复杂的连接并在连接的模型/表中进行搜索:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes similar : [ "similar_books.title" , "similar_books.description" ]
scope do
joins "left outer join books similar_books on ..."
end
aliases similar_books : Book # Tell SearchCop how to map SQL aliases to models
end
# ...
end
也可以引用和使用关联的关联:
class Book < ActiveRecord :: Base
# ...
has_many :comments
has_many :users , :through => :comments
search_scope :search do
attributes user : "users.username"
end
# ...
end
SearchCop 尝试从指定的属性推断模型的类名和 SQL 别名,以自动检测数据类型定义等。这通常工作得很好。如果您通过self.table_name = ...
使用自定义表名称,或者模型关联多次,SearchCop 无法推断类和 SQL 别名,例如
class Book < ActiveRecord :: Base
# ...
has_many :users , :through => :comments
belongs_to :user
search_scope :search do
attributes user : [ "user.username" , "users_books.username" ]
end
# ...
end
在这里,要使查询正常工作,您必须使用users_books.username
,因为 ActiveRecord 会在其 SQL 查询中为用户分配不同的 SQL 别名,因为用户模型会关联多次。但是,由于 SearchCop 现在无法从users_books
推断出User
模型,因此您必须添加:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
aliases :users_books => :users
end
# ...
end
告诉 SearchCop 有关自定义 SQL 别名和映射的信息。此外,您始终可以通过scope {}
块加上aliases
自行进行联接,并使用您自己的自定义 sql 别名来独立于 ActiveRecord 自动分配的名称。
查询字符串查询支持AND/and
、 OR/or
、 :
、 =
、 !=
、 <
、 <=
、 >
、 >=
、 NOT/not/-
、 ()
、 "..."
和'...'
。默认运算符是AND
和matches
, OR
优先于AND
。 NOT
只能用作单个属性的中缀运算符。
基于哈希的查询支持and: [...]
和or: [...]
,它们采用not: {...}
、 matches: {...}
、 eq: {...}
、 not_eq: {...}
的数组not_eq: {...}
、 lt: {...}
、 lteq: {...}
、 gt: {...}
、 gteq: {...}
和query: "..."
参数。此外, query: "..."
可以创建子查询。查询字符串查询的其他规则也适用于基于哈希的查询。
SearchCop 还提供通过在search_scope
中定义generator
来定义自定义运算符的功能。然后可以将它们与基于哈希的查询搜索一起使用。当您想要使用 SearchCop 不支持的数据库运算符时,这非常有用。
请注意,使用生成器时,您有责任清理/引用这些值(请参见下面的示例)。否则你的生成器将允许 SQL 注入。因此,请仅在您知道自己在做什么的情况下使用生成器。
例如,如果您想要执行LIKE
查询,其中书名以字符串开头,您可以像这样定义搜索范围:
search_scope :search do
attributes :title
generator :starts_with do | column_name , raw_value |
pattern = " #{ raw_value } %"
" #{ column_name } LIKE #{ quote pattern } "
end
end
当你想执行搜索时,你可以像这样使用它:
Book . search ( title : { starts_with : "The Great" } )
安全注意事项:从生成器返回的查询将直接插入到进入数据库的查询中。这会在您的应用程序中打开一个潜在的 SQL 注入点。如果您使用此功能,您将需要确保返回的查询可以安全执行。
当在布尔值、日期时间、时间戳等字段中搜索时,SearchCop 会执行一些映射。以下查询是等效的:
Book . search ( "available:true" )
Book . search ( "available:1" )
Book . search ( "available:yes" )
也
Book . search ( "available:false" )
Book . search ( "available:0" )
Book . search ( "available:no" )
对于日期时间和时间戳字段,SearchCop 将某些值扩展到范围:
Book . search ( "created_at:2014" )
# ... WHERE created_at >= '2014-01-01 00:00:00' AND created_at <= '2014-12-31 23:59:59'
Book . search ( "created_at:2014-06" )
# ... WHERE created_at >= '2014-06-01 00:00:00' AND created_at <= '2014-06-30 23:59:59'
Book . search ( "created_at:2014-06-15" )
# ... WHERE created_at >= '2014-06-15 00:00:00' AND created_at <= '2014-06-15 23:59:59'
搜索链是可能的。然而,链接目前不允许 SearchCop 优化全文索引的单个查询。
Book . search ( "Harry" ) . search ( "Potter" )
将生成
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry' IN BOOLEAN MODE) AND MATCH(...) AGAINST('+Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry') AND to_tsvector(...) @@ to_tsquery('simple', 'Potter')
而不是
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
因此,如果您使用全文索引,最好避免链接。
使用Model#search
时,SearchCop 可以方便地防止在传递给它的查询字符串无效(解析错误、不兼容的数据类型错误等)时引发某些异常。相反, Model#search
返回一个空关系。但是,如果您需要调试某些情况,请使用Model#unsafe_search
,这会引发它们。
Book . unsafe_search ( "stock: None" ) # => raise SearchCop::IncompatibleDatatype
SearchCop 提供反射方法,即#attributes
、 #default_attributes
、 #options
和#aliases
。您可以使用这些方法为您的模型提供单独的搜索帮助小部件,其中列出要搜索的属性以及默认属性等。
class Product < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :title , :description
options :title , default : true
end
end
Product . search_reflection ( :search ) . attributes
# {"title" => ["products.title"], "description" => ["products.description"]}
Product . search_reflection ( :search ) . default_attributes
# {"title" => ["products.title"]}
# ...
从版本 1.0.0 开始,SearchCop 使用语义版本控制:SemVer
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)