SearchCop は、ActiveRecord モデルを拡張して、単純なクエリ文字列やハッシュベースのクエリを介したクエリなどの全文検索エンジンをサポートします。 title
、 author
、 stock
、 price
、 available
などのさまざまな属性を持つBook
モデルがあると仮定します。 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 は、この場合の SQL クエリの構築に使用される値として、ActiveSupport の begin_of_year メソッドと end_of_year メソッドを使用しています。
もちろん、これらの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 は、SQL インジェクションを防ぐために、ActiveRecord 接続アダプターが提供するサニタイズ/クォート用のメソッドを使用しています。セキュリティ問題から 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
ここで何が起こっているのでしょうか?さて、 author
とtitle
で構成される属性グループの名前としてall
指定しました。さらに、 all
フルテキスト属性として指定したため、SearchCop はauthor
とtitle
に複合フルテキスト インデックスが存在すると想定し、それに応じてクエリが最適化されます。最後に、検索対象のデフォルト属性としてall
指定しました。これにより、SearchCop は、クエリ内で直接指定されない限り ( stock > 0
など)、 stock
などの他の属性を無視できます。
他のクエリも同様の方法で最適化され、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 に関しては、フルテキスト インデックスを作成する方法が他にもあります。ただし、最も簡単な方法の 1 つは次のとおりです。
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
)