SearchCop amplía sus modelos ActiveRecord para admitir consultas similares a motores de búsqueda de texto completo a través de cadenas de consulta simples y consultas basadas en hash. Supongamos que tiene un modelo Book
que tiene varios atributos como title
, author
, stock
, price
y available
. Usando SearchCop puedes realizar:
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)" )
# ...
Por lo tanto, puede entregar una cadena de consulta de búsqueda a sus modelos y usted, los administradores y/o usuarios de su aplicación obtendrán potentes funciones de consulta sin la necesidad de integrar servidores de búsqueda de terceros adicionales, ya que SearchCop puede utilizar las capacidades de índice de texto completo de su RDBMS en una forma independiente de la base de datos (actualmente se admiten índices de texto completo MySQL y PostgreSQL) y optimiza las consultas para hacer un uso óptimo de ellas. Lea más a continuación.
También se admiten consultas complejas basadas en hash:
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" } } )
# ...
Agregue esta línea al Gemfile de su aplicación:
gem 'search_cop'
Y luego ejecuta:
$ bundle
O instálelo usted mismo como:
$ gem install search_cop
Para habilitar SearchCop para un modelo, include SearchCop
y especifique los atributos que desea exponer a las consultas de búsqueda dentro de 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
Por supuesto, también puedes especificar múltiples bloques search_scope
como quieras:
search_scope :admin_search do
attributes :title , :description , :stock , :price , :created_at , :available
# ...
end
search_scope :user_search do
attributes :title , :description
# ...
end
SearchCop analiza la consulta y la asigna a una consulta SQL de forma independiente de la base de datos. Por lo tanto, SearchCop no está vinculado a un RDBMS específico.
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 está utilizando los métodos de inicio_de_año y fin_de_año de ActiveSupport para los valores utilizados en la creación de la consulta SQL para este caso.
Por supuesto, estas consultas LIKE '%...%'
no lograrán un rendimiento óptimo, pero consulte la siguiente sección sobre las capacidades de texto completo de SearchCop para comprender cómo se pueden optimizar las consultas resultantes.
Como Book.search(...)
devuelve un ActiveRecord::Relation
, usted es libre de preprocesar o posprocesar los resultados de la búsqueda de todas las formas posibles:
Book . where ( available : true ) . search ( "Harry Potter" ) . order ( "books.id desc" ) . paginate ( page : params [ :page ] )
Cuando pasa una cadena de consulta a SearchCop, se analiza, analiza y asigna para finalmente generar una consulta SQL. Para ser más precisos, cuando SearchCop analiza la consulta, crea objetos (nodos) que representan las expresiones de la consulta (nodos And-, Or-, Not-, String-, Date-, etc.). Para crear la consulta SQL, SearchCop utiliza el concepto de visitantes como, por ejemplo, el utilizado en Arel, de modo que, para cada nodo debe haber un visitante, lo que transforma el nodo a SQL. Cuando no hay ningún visitante, se genera una excepción cuando el generador de consultas intenta "visitar" el nodo. Los visitantes son responsables de desinfectar la entrada proporcionada por el usuario. Esto se hace principalmente mediante comillas (cadena, nombre de tabla, comillas de columna, etc.). SearchCop está utilizando los métodos proporcionados por el adaptador de conexión ActiveRecord para desinfectar/entrecomillar y evitar la inyección de SQL. Si bien nunca podremos estar 100 % a salvo de los problemas de seguridad, SearchCop se toma en serio los problemas de seguridad. Informe de manera responsable a través de seguridad en flakks punto com en caso de que encuentre algún problema relacionado con la seguridad.
SearchCop admite campos json para MySQL, así como campos json, jsonb y hstore para postgres. Actualmente, siempre se espera que los valores de los campos sean cadenas y no se admiten matrices. Puede especificar atributos json a través de:
search_scope :search do
attributes user_agent : "context->browser->user_agent"
# ...
end
donde context
es una columna json/jsonb que, por ejemplo, contiene:
{
"browser" : {
"user_agent" : " Firefox ... "
}
}
De forma predeterminada, es decir, si no le informa a SearchCop acerca de sus índices de texto completo, SearchCop utilizará consultas LIKE '%...%'
. Desafortunadamente, a menos que cree un índice de trigramas (solo postgres), estas consultas no pueden usar índices SQL, de modo que su RDBMS debe escanear cada fila cuando busca Book.search("Harry Potter")
o similar. Para evitar la penalización de las consultas LIKE
, SearchCop puede explotar las capacidades de índice de texto completo de MySQL y PostgreSQL. Para utilizar índices de texto completo ya existentes, simplemente dígale a SearchCop que los use a través de:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes :title , :author
options :title , :type => :fulltext
options :author , :type => :fulltext
end
# ...
end
Luego, SearchCop cambiará de forma transparente sus consultas SQL para los atributos que tienen índices de texto completo a:
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'))
Obviamente, estas consultas no siempre arrojarán los mismos resultados que las consultas LIKE
con comodines, porque buscamos palabras en lugar de subcadenas. Sin embargo, los índices de texto completo normalmente proporcionarán un mejor rendimiento.
Además, la consulta anterior aún no es perfecta. Para mejorarlo aún más, SearchCop intenta optimizar las consultas para hacer un uso óptimo de los índices de texto completo y al mismo tiempo permitir mezclarlos con atributos que no son de texto completo. Para mejorar aún más las consultas, puede agrupar atributos y especificar un campo predeterminado para buscar, de modo que SearchCop ya no deba buscar en todos los campos:
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
Ahora SearchCop puede optimizar la siguiente consulta, que aún no es óptima:
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
a la siguiente consulta, más eficaz:
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
¿Qué está pasando aquí? Bueno, especificamos all
como el nombre de un grupo de atributos que consta de author
y title
. Como, además, especificamos que all
sea un atributo de texto completo, SearchCop supone que hay un índice compuesto de texto completo presente en author
y title
, de modo que la consulta se optimiza en consecuencia. Finalmente, especificamos que all
sea el atributo predeterminado para buscar, de modo que SearchCop pueda ignorar otros atributos, como por ejemplo stock
, siempre y cuando no se especifiquen directamente en las consultas (como stock > 0
).
Otras consultas se optimizarán de manera similar, de modo que SearchCop intente minimizar las restricciones de texto completo dentro de una consulta, concretamente MATCH() AGAINST()
para MySQL y to_tsvector() @@ to_tsquery()
para 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')
Para crear un índice de texto completo en books.title
en MySQL, simplemente use:
add_index :books , :title , :type => :fulltext
Con respecto a los índices compuestos, que se usarán, por ejemplo, para el campo predeterminado all
ya especificamos anteriormente, use:
add_index :books , [ :author , :title ] , :type => :fulltext
Tenga en cuenta que MySQL admite índices de texto completo para MyISAM y, a partir de la versión 5.6+ de MySQL, también para InnoDB. Para obtener más detalles sobre los índices de texto completo de MySQL, visite http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html
Respecto a PostgreSQL, hay más formas de crear un índice de texto completo. Sin embargo, una de las formas más sencillas es:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
Además, para PostgreSQL debes cambiar el formato del esquema en config/application.rb
:
config . active_record . schema_format = :sql
Respecto a los índices compuestos para PostgreSQL, utilice:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
Para manejar correctamente los valores NULL con PostgreSQL, use COALESCE tanto en el momento de la creación del índice como al especificar el search_scope
:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', COALESCE(author, '') || ' ' || COALESCE(title, '')))"
más:
search_scope :search do
attributes :title
options :title , :type => :fulltext , coalesce : true
end
Para utilizar otro diccionario PostgreSQL que no sea simple
, debe crear el índice en consecuencia y debe informar a SearchCop al respecto, por ejemplo:
search_scope :search do
attributes :title
options :title , :type => :fulltext , dictionary : "english"
end
Para obtener más detalles sobre los índices de texto completo de PostgreSQL, visite http://www.postgresql.org/docs/9.3/static/textsearch.html
En caso de que exponga atributos que no son de texto completo a consultas de búsqueda (precio, acciones, etc.), las consultas respectivas, como Book.search("stock > 0")
, se beneficiarán de los índices habituales que no son de texto completo. Por lo tanto, debe agregar un índice habitual en cada columna que exponga a consultas de búsqueda más un índice de texto completo para cada atributo de texto completo.
En caso de que no pueda usar índices de texto completo, porque, por ejemplo, todavía está en MySQL 5.5 mientras usa InnoDB u otro RDBMS sin soporte de texto completo, puede hacer que su RDBMS use índices habituales que no son de texto completo para columnas de cadena si no necesita el comodín izquierdo dentro de las consultas LIKE
. Simplemente proporcione la siguiente opción:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username
options :username , left_wildcard : false
end
# ...
de modo que SearchCop omitirá el comodín más a la izquierda.
User . search ( "admin" )
# ... WHERE users.username LIKE 'admin%'
De manera similar, también puedes desactivar el comodín derecho:
search_scope :search do
attributes :username
options :username , right_wildcard : false
end
Cuando define varios campos en un alcance de búsqueda, SearcCop utilizará de forma predeterminada el operador AND para concatenar las condiciones, por ejemplo:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username , :fullname
end
# ...
end
Entonces, una búsqueda como User.search("something")
generará una consulta con las siguientes condiciones:
... WHERE username LIKE ' %something% ' AND fullname LIKE ' %something% '
Sin embargo, hay casos en los que no se desea utilizar AND como operador predeterminado, por lo que SearchCop le permite anularlo y utilizar OR como operador predeterminado. Una consulta como User.search("something", default_operator: :or)
generará la consulta usando OR para concatenar las condiciones
... WHERE username LIKE ' %something% ' OR fullname LIKE ' %something% '
Finalmente, tenga en cuenta que también puede aplicarlo a índices/consultas de texto completo.
Si especifica atributos de búsqueda de otro modelo, como
class Book < ActiveRecord :: Base
# ...
belongs_to :author
search_scope :search do
attributes author : "author.name"
end
# ...
end
SearchCop eager_load
de forma predeterminada las asociaciones a las que se hace referencia cuando realice Book.search(...)
. Si no desea la eager_load
automática o necesita realizar operaciones especiales, especifique un scope
:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
scope { joins ( :author ) . eager_load ( :comments ) } # etc.
end
# ...
end
SearchCop luego omitirá la carga automática de cualquier asociación y utilizará el alcance en su lugar. También puede utilizar scope
junto con aliases
para realizar uniones arbitrariamente complejas y buscar en los modelos/tablas unidos:
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
También se pueden hacer referencia y utilizar asociaciones de asociaciones:
class Book < ActiveRecord :: Base
# ...
has_many :comments
has_many :users , :through => :comments
search_scope :search do
attributes user : "users.username"
end
# ...
end
SearchCop intenta inferir el nombre de clase de un modelo y el alias SQL a partir de los atributos especificados para detectar automáticamente definiciones de tipos de datos, etc. Esto normalmente funciona bastante bien. En caso de que esté utilizando nombres de tablas personalizados a través de self.table_name = ...
o si un modelo está asociado varias veces, SearchCop no puede inferir los nombres de clase y alias SQL, por ejemplo
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
Aquí, para que las consultas funcionen, debe usar users_books.username
, porque ActiveRecord asigna un alias SQL diferente para los usuarios dentro de sus consultas SQL, porque el modelo de usuario está asociado varias veces. Sin embargo, como SearchCop ahora no puede inferir el modelo User
a partir de users_books
, debe agregar:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
aliases :users_books => :users
end
# ...
end
para informar a SearchCop sobre el alias SQL personalizado y la asignación. Además, siempre puede realizar las uniones usted mismo a través de un bloque scope {}
más aliases
y usar sus propios alias de SQL personalizados para independizarse de los nombres asignados automáticamente por ActiveRecord.
Las consultas de cadenas de consulta admiten AND/and
, OR/or
, :
, =
, !=
, <
, <=
, >
, >=
, NOT/not/-
, ()
, "..."
y '...'
. Los operadores predeterminados son AND
y matches
, OR
tiene prioridad sobre AND
. NOT
solo se puede utilizar como operador infijo con respecto a un único atributo.
Las consultas basadas en hash admiten and: [...]
and or: [...]
, que toman una matriz de not: {...}
, matches: {...}
, eq: {...}
, not_eq: {...}
, lt: {...}
, lteq: {...}
, gt: {...}
, gteq: {...}
y query: "..."
argumentos. Además, query: "..."
permite crear subconsultas. Las otras reglas para consultas de cadenas de consulta también se aplican a consultas basadas en hash.
SearchCop también brinda la capacidad de definir operadores personalizados definiendo un generator
en search_scope
. Luego se pueden utilizar con la búsqueda de consultas basada en hash. Esto resulta útil cuando desea utilizar operadores de bases de datos que no son compatibles con SearchCop.
Tenga en cuenta que, cuando utilice generadores, usted es responsable de desinfectar/cotizar los valores (consulte el ejemplo a continuación). De lo contrario, su generador permitirá la inyección de SQL. Por lo tanto, utilice generadores únicamente si sabe lo que está haciendo.
Por ejemplo, si desea realizar una consulta LIKE
donde el título de un libro comienza con una cadena, puede definir el alcance de la búsqueda de esta manera:
search_scope :search do
attributes :title
generator :starts_with do | column_name , raw_value |
pattern = " #{ raw_value } %"
" #{ column_name } LIKE #{ quote pattern } "
end
end
Cuando quieras realizar la búsqueda lo usas así:
Book . search ( title : { starts_with : "The Great" } )
Nota de seguridad: la consulta devuelta por el generador se interpolará directamente en la consulta que va a su base de datos. Esto abre un posible punto de inyección SQL en su aplicación. Si utiliza esta función, querrá asegurarse de que la consulta que está devolviendo sea segura de ejecutar.
Al buscar en campos booleanos, de fecha y hora, de marca de tiempo, etc., SearchCop realiza algunos mapeos. Las siguientes consultas son equivalentes:
Book . search ( "available:true" )
Book . search ( "available:1" )
Book . search ( "available:yes" )
así como
Book . search ( "available:false" )
Book . search ( "available:0" )
Book . search ( "available:no" )
Para los campos de fecha, hora y marca de tiempo, SearchCop expande ciertos valores a rangos:
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'
Es posible encadenar búsquedas. Sin embargo, actualmente el encadenamiento no permite a SearchCop optimizar las consultas individuales para índices de texto completo.
Book . search ( "Harry" ) . search ( "Potter" )
generará
# 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')
en lugar de
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
Por lo tanto, si utiliza índices de texto completo, será mejor que evite el encadenamiento.
Cuando se utiliza Model#search
, SearchCop evita convenientemente que se generen ciertas excepciones en caso de que la cadena de consulta que se le pasa no sea válida (errores de análisis, errores de tipos de datos incompatibles, etc.). En cambio, Model#search
devuelve una relación vacía. Sin embargo, si necesita depurar ciertos casos, use Model#unsafe_search
, que los generará.
Book . unsafe_search ( "stock: None" ) # => raise SearchCop::IncompatibleDatatype
SearchCop proporciona métodos reflexivos, a saber, #attributes
, #default_attributes
, #options
y #aliases
. Puede utilizar estos métodos, por ejemplo, para proporcionar un widget de ayuda de búsqueda individual para sus modelos, que enumere los atributos para buscar, así como los predeterminados, 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"]}
# ...
A partir de la versión 1.0.0, SearchCop utiliza el control de versiones semántico: SemVer
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)