SearchCop étend vos modèles ActiveRecord pour prendre en charge les requêtes des moteurs de recherche en texte intégral via de simples chaînes de requête et des requêtes basées sur le hachage. Supposons que vous disposiez d'un modèle Book
ayant divers attributs tels que title
, author
, stock
, price
, available
. En utilisant SearchCop, vous pouvez effectuer :
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)" )
# ...
Ainsi, vous pouvez distribuer une chaîne de requête de recherche à vos modèles et vous, les administrateurs et/ou les utilisateurs de votre application bénéficierez de fonctionnalités de requête puissantes sans avoir besoin d'intégrer des serveurs de recherche tiers supplémentaires, puisque SearchCop peut utiliser les capacités d'indexation en texte intégral de votre SGBDR dans de manière indépendante de la base de données (actuellement, les index de texte intégral MySQL et PostgreSQL sont pris en charge) et optimise les requêtes pour en faire un usage optimal. En savoir plus ci-dessous.
Les requêtes complexes basées sur le hachage sont également prises en charge :
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" } } )
# ...
Ajoutez cette ligne au Gemfile de votre application :
gem 'search_cop'
Et puis exécutez :
$ bundle
Ou installez-le vous-même en tant que :
$ gem install search_cop
Pour activer SearchCop pour un modèle, include SearchCop
et spécifiez les attributs que vous souhaitez exposer aux requêtes de recherche dans un 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
Vous pouvez bien sûr également spécifier plusieurs blocs search_scope
à votre guise :
search_scope :admin_search do
attributes :title , :description , :stock , :price , :created_at , :available
# ...
end
search_scope :user_search do
attributes :title , :description
# ...
end
SearchCop analyse la requête et la mappe à une requête SQL d'une manière indépendante de la base de données. Ainsi, SearchCop n'est pas lié à un SGBDR spécifique.
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 utilise les méthodes early_of_year et end_of_year d'ActiveSupport pour les valeurs utilisées dans la création de la requête SQL pour ce cas.
Bien sûr, ces requêtes LIKE '%...%'
n'atteindront pas des performances optimales, mais consultez la section ci-dessous sur les capacités de texte intégral de SearchCop pour comprendre comment les requêtes résultantes peuvent être optimisées.
Comme Book.search(...)
renvoie un ActiveRecord::Relation
, vous êtes libre de pré- ou post-traiter les résultats de la recherche de toutes les manières possibles :
Book . where ( available : true ) . search ( "Harry Potter" ) . order ( "books.id desc" ) . paginate ( page : params [ :page ] )
Lorsque vous transmettez une chaîne de requête à SearchCop, elle est analysée, analysée et mappée pour finalement créer une requête SQL. Pour être plus précis, lorsque SearchCop analyse la requête, il crée des objets (nœuds) qui représentent les expressions de requête (nœuds And-, Or-, Not-, String-, Date-, etc.). Pour construire la requête SQL, SearchCop utilise le concept de visiteurs comme par exemple celui utilisé dans Arel, tel que, pour chaque nœud, il doit y avoir un visiteur, qui transforme le nœud en SQL. Lorsqu'il n'y a pas de visiteur, une exception est levée lorsque le générateur de requêtes tente de « visiter » le nœud. Les visiteurs sont responsables de nettoyer les entrées fournies par l’utilisateur. Cela se fait principalement via des guillemets (chaîne, nom de table, citation de colonne, etc.). SearchCop utilise les méthodes fournies par l'adaptateur de connexion ActiveRecord pour nettoyer/citer afin d'empêcher l'injection SQL. Même si nous ne pouvons jamais être à l’abri à 100 % des problèmes de sécurité, SearchCop prend les problèmes de sécurité au sérieux. Veuillez signaler de manière responsable via la sécurité sur flakks dot com si vous rencontrez des problèmes liés à la sécurité.
SearchCop prend en charge les champs json pour MySQL, ainsi que les champs json, jsonb et hstore pour postgres. Actuellement, les valeurs de champ sont toujours censées être des chaînes et aucun tableau n'est pris en charge. Vous pouvez spécifier les attributs json via :
search_scope :search do
attributes user_agent : "context->browser->user_agent"
# ...
end
où context
est une colonne json/jsonb qui contient par exemple :
{
"browser" : {
"user_agent" : " Firefox ... "
}
}
Par défaut, c'est-à-dire si vous ne communiquez pas à SearchCop vos index de texte intégral, SearchCop utilisera les requêtes LIKE '%...%'
. Malheureusement, à moins que vous ne créiez un index trigramme (postgres uniquement), ces requêtes ne peuvent pas utiliser d'index SQL, de sorte que chaque ligne doit être analysée par votre SGBDR lorsque vous recherchez Book.search("Harry Potter")
ou similaire. Pour éviter la pénalité des requêtes LIKE
, SearchCop peut exploiter les capacités d'indexation en texte intégral de MySQL et PostgreSQL. Pour utiliser des index de texte intégral déjà existants, dites simplement à SearchCop de les utiliser via :
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes :title , :author
options :title , :type => :fulltext
options :author , :type => :fulltext
end
# ...
end
SearchCop modifiera alors de manière transparente ses requêtes SQL pour les attributs ayant des indices de texte intégral en :
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'))
Évidemment, ces requêtes ne renverront pas toujours les mêmes résultats que les requêtes génériques LIKE
, car nous recherchons des mots plutôt que des sous-chaînes. Cependant, les indices de texte intégral offrent généralement de meilleures performances.
De plus, la requête ci-dessus n’est pas encore parfaite. Pour l'améliorer encore plus, SearchCop tente d'optimiser les requêtes pour utiliser de manière optimale les index fulltext tout en permettant de les mélanger avec des attributs non fulltext. Pour améliorer encore plus les requêtes, vous pouvez regrouper les attributs et spécifier un champ par défaut dans lequel effectuer la recherche, de sorte que SearchCop ne doive plus rechercher dans tous les champs :
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
Désormais, SearchCop peut optimiser la requête suivante, qui n'est pas encore optimale :
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
à la requête suivante, plus performante :
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
Que se passe-t-il ici ? Eh bien, nous avons spécifié all
comme nom d'un groupe d'attributs composé de author
et title
. Comme nous avons en outre spécifié all
comme étant un attribut de texte intégral, SearchCop suppose qu'il existe un index de texte intégral composé présent sur author
et title
, de sorte que la requête est optimisée en conséquence. Enfin, nous avons spécifié all
comme étant l'attribut par défaut dans lequel effectuer la recherche, de sorte que SearchCop puisse ignorer d'autres attributs, comme par exemple stock
, tant qu'ils ne sont pas spécifiés directement dans les requêtes (comme pour stock > 0
).
D'autres requêtes seront optimisées de la même manière, de sorte que SearchCop essaie de minimiser les contraintes fultext au sein d'une requête, à savoir MATCH() AGAINST()
pour MySQL et to_tsvector() @@ to_tsquery()
pour PostgreSQL.
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')
Pour créer un index de texte intégral sur books.title
dans MySQL, utilisez simplement :
add_index :books , :title , :type => :fulltext
Concernant les indices composés, qui serviront par exemple pour le champ par défaut all
que nous avons déjà précisé plus haut, utilisez :
add_index :books , [ :author , :title ] , :type => :fulltext
Veuillez noter que MySQL prend en charge les index de texte intégral pour MyISAM et, à partir de la version MySQL 5.6+, pour InnoDB également. Pour plus de détails sur les index de texte intégral MySQL, visitez http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html
Concernant PostgreSQL, il existe d'autres façons de créer un index de texte intégral. Cependant, l’un des moyens les plus simples consiste à :
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
De plus, pour PostgreSQL vous devez changer le format du schéma dans config/application.rb
:
config . active_record . schema_format = :sql
Concernant les index composés pour PostgreSQL, utilisez :
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
Pour gérer correctement les valeurs NULL avec PostgreSQL, utilisez COALESCE à la fois au moment de la création de l'index et lors de la spécification du search_scope
:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', COALESCE(author, '') || ' ' || COALESCE(title, '')))"
plus:
search_scope :search do
attributes :title
options :title , :type => :fulltext , coalesce : true
end
Pour utiliser un autre dictionnaire PostgreSQL que simple
, vous devez créer l'index en conséquence et vous devez en informer SearchCop, par exemple :
search_scope :search do
attributes :title
options :title , :type => :fulltext , dictionary : "english"
end
Pour plus de détails sur les index de texte intégral PostgreSQL, visitez http://www.postgresql.org/docs/9.3/static/textsearch.html
Dans le cas où vous exposez des attributs non fulltext aux requêtes de recherche (prix, stock, etc.), les requêtes respectives, comme Book.search("stock > 0")
, bénéficieront des indices non fulltext habituels. Ainsi, vous devez ajouter un index habituel sur chaque colonne que vous exposez aux requêtes de recherche, ainsi qu'un index de texte intégral pour chaque attribut de texte intégral.
Dans le cas où vous ne pouvez pas utiliser les index de texte intégral, parce que vous êtes par exemple toujours sur MySQL 5.5 tout en utilisant InnoDB ou un autre SGBDR sans support de texte intégral, vous pouvez faire en sorte que votre SGBDR utilise les index non-fulltext habituels pour les colonnes de chaînes si vous n'en avez pas besoin. caractère générique laissé dans les requêtes LIKE
. Fournissez simplement l'option suivante :
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username
options :username , left_wildcard : false
end
# ...
de telle sorte que SearchCop omettra le caractère générique le plus à gauche.
User . search ( "admin" )
# ... WHERE users.username LIKE 'admin%'
De même, vous pouvez également désactiver le bon caractère générique :
search_scope :search do
attributes :username
options :username , right_wildcard : false
end
Lorsque vous définissez plusieurs champs sur une étendue de recherche, SearcCop utilisera par défaut l'opérateur AND pour concaténer les conditions, par exemple :
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username , :fullname
end
# ...
end
Ainsi, une recherche comme User.search("something")
générera une requête avec les conditions suivantes :
... WHERE username LIKE ' %something% ' AND fullname LIKE ' %something% '
Cependant, il existe des cas où l'utilisation de AND comme opérateur par défaut n'est pas souhaitée, donc SearchCop vous permet de le remplacer et d'utiliser OR comme opérateur par défaut à la place. Une requête comme User.search("something", default_operator: :or)
générera la requête en utilisant OR pour concaténer les conditions
... WHERE username LIKE ' %something% ' OR fullname LIKE ' %something% '
Enfin, veuillez noter que vous pouvez également l'appliquer aux index/requêtes de texte intégral.
Si vous spécifiez des attributs consultables à partir d'un autre modèle, comme
class Book < ActiveRecord :: Base
# ...
belongs_to :author
search_scope :search do
attributes author : "author.name"
end
# ...
end
SearchCop chargera par défaut les associations référencées eager_load
lorsque vous effectuerez Book.search(...)
. Si vous ne souhaitez pas le chargement automatique eager_load
ou si vous devez effectuer des opérations spéciales, spécifiez une scope
:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
scope { joins ( :author ) . eager_load ( :comments ) } # etc.
end
# ...
end
SearchCop ignorera alors tout chargement automatique d'association et utilisera la portée à la place. Vous pouvez également utiliser scope
avec aliases
pour effectuer des jointures arbitrairement complexes et rechercher dans les modèles/tables joints :
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
Les associations d'associations peuvent également être référencées et utilisées :
class Book < ActiveRecord :: Base
# ...
has_many :comments
has_many :users , :through => :comments
search_scope :search do
attributes user : "users.username"
end
# ...
end
SearchCop essaie de déduire le nom de classe et l'alias SQL d'un modèle à partir des attributs spécifiés pour détecter automatiquement les définitions de types de données, etc. Cela fonctionne généralement très bien. Dans le cas où vous utilisez des noms de table personnalisés via self.table_name = ...
ou si un modèle est associé plusieurs fois, SearchCop ne peut cependant pas déduire les noms de classe et d'alias SQL, par exemple
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
Ici, pour que les requêtes fonctionnent, vous devez utiliser users_books.username
, car ActiveRecord attribue un alias SQL différent aux utilisateurs dans ses requêtes SQL, car le modèle utilisateur est associé plusieurs fois. Cependant, comme SearchCop ne peut désormais pas déduire le modèle User
à partir de users_books
, vous devez ajouter :
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
aliases :users_books => :users
end
# ...
end
pour informer SearchCop de l'alias SQL personnalisé et du mappage. De plus, vous pouvez toujours effectuer les jointures vous-même via un bloc scope {}
plus aliases
et utiliser vos propres alias SQL personnalisés pour devenir indépendant des noms automatiquement attribués par ActiveRecord.
Les requêtes de chaîne de requête prennent en charge AND/and
, OR/or
, :
, =
, !=
, <
, <=
, >
, >=
, NOT/not/-
, ()
, "..."
et '...'
. Les opérateurs par défaut sont AND
et matches
, OR
a priorité sur AND
. NOT
ne peut être utilisé que comme opérateur infixe concernant un seul attribut.
Prise en charge des requêtes basées sur le hachage and: [...]
et or: [...]
, qui prennent un tableau de not: {...}
, matches: {...}
, eq: {...}
, not_eq: {...}
, lt: {...}
, lteq: {...}
, gt: {...}
, gteq: {...}
et query: "..."
arguments. De plus, query: "..."
permet de créer des sous-requêtes. Les autres règles relatives aux requêtes de chaîne de requête s'appliquent également aux requêtes basées sur le hachage.
SearchCop offre également la possibilité de définir des opérateurs personnalisés en définissant un generator
dans search_scope
. Ils peuvent ensuite être utilisés avec la recherche de requêtes basée sur le hachage. Ceci est utile lorsque vous souhaitez utiliser des opérateurs de base de données qui ne sont pas pris en charge par SearchCop.
Veuillez noter que lorsque vous utilisez des générateurs, vous êtes responsable de nettoyer/citer les valeurs (voir exemple ci-dessous). Sinon votre générateur permettra l'injection SQL. Par conséquent, n'utilisez des générateurs que si vous savez ce que vous faites.
Par exemple, si vous souhaitez effectuer une requête LIKE
dans laquelle le titre d'un livre commence par une chaîne, vous pouvez définir la portée de la recherche comme suit :
search_scope :search do
attributes :title
generator :starts_with do | column_name , raw_value |
pattern = " #{ raw_value } %"
" #{ column_name } LIKE #{ quote pattern } "
end
end
Lorsque vous souhaitez effectuer la recherche, vous l'utilisez comme ceci :
Book . search ( title : { starts_with : "The Great" } )
Note de sécurité : la requête renvoyée par le générateur sera interpolée directement dans la requête envoyée à votre base de données. Cela ouvre un point d’injection SQL potentiel dans votre application. Si vous utilisez cette fonctionnalité, vous devez vous assurer que la requête que vous renvoyez peut être exécutée en toute sécurité.
Lors de la recherche dans les champs booléens, date/heure, horodatage, etc., SearchCop effectue un mappage. Les requêtes suivantes sont équivalentes :
Book . search ( "available:true" )
Book . search ( "available:1" )
Book . search ( "available:yes" )
ainsi que
Book . search ( "available:false" )
Book . search ( "available:0" )
Book . search ( "available:no" )
Pour les champs datetime et timestamp, SearchCop étend certaines valeurs en plages :
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'
Un chaînage de recherches est possible. Cependant, le chaînage ne permet actuellement pas à SearchCop d'optimiser les requêtes individuelles pour les index de texte intégral.
Book . search ( "Harry" ) . search ( "Potter" )
générera
# 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')
au lieu de
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
Ainsi, si vous utilisez des index de texte intégral, il vaut mieux éviter le chaînage.
Lors de l'utilisation Model#search
, SearchCop empêche commodément certaines exceptions d'être déclenchées au cas où la chaîne de requête qui lui est transmise n'est pas valide (erreurs d'analyse, erreurs de type de données incompatibles, etc.). Au lieu de cela, Model#search
renvoie une relation vide. Cependant, si vous devez déboguer certains cas, utilisez Model#unsafe_search
, ce qui les déclenchera.
Book . unsafe_search ( "stock: None" ) # => raise SearchCop::IncompatibleDatatype
SearchCop fournit des méthodes réfléchissantes, à savoir #attributes
, #default_attributes
, #options
et #aliases
. Vous pouvez utiliser ces méthodes pour, par exemple, fournir un widget d'aide à la recherche individuel pour vos modèles, qui répertorie les attributs dans lesquels rechercher ainsi que ceux par défaut, etc.
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"]}
# ...
À partir de la version 1.0.0, SearchCop utilise le versionnement sémantique : SemVer
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)