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
)