SearchCop estende seus modelos ActiveRecord para oferecer suporte a mecanismos de pesquisa de texto completo, como consultas por meio de strings de consulta simples e consultas baseadas em hash. Suponha que você tenha um modelo Book
com vários atributos como title
, author
, stock
, price
, available
. Usando SearchCop você pode 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)" )
# ...
Assim, você pode distribuir uma string de consulta de pesquisa para seus modelos e você, os administradores e/ou usuários do seu aplicativo obterão recursos de consulta poderosos sem a necessidade de integração de servidores de pesquisa de terceiros adicionais, já que o SearchCop pode usar recursos de índice de texto completo do seu RDBMS em uma forma agnóstica de banco de dados (atualmente índices de texto completo MySQL e PostgreSQL são suportados) e otimiza as consultas para fazer uso ideal delas. Leia mais abaixo.
Consultas complexas baseadas em hash também são suportadas:
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" } } )
# ...
Adicione esta linha ao Gemfile da sua aplicação:
gem 'search_cop'
E então execute:
$ bundle
Ou instale você mesmo como:
$ gem install search_cop
Para ativar o SearchCop para um modelo, include SearchCop
e especifique os atributos que você deseja expor às consultas de pesquisa em um 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
É claro que você também pode especificar vários blocos search_scope
conforme desejar:
search_scope :admin_search do
attributes :title , :description , :stock , :price , :created_at , :available
# ...
end
search_scope :user_search do
attributes :title , :description
# ...
end
SearchCop analisa a consulta e a mapeia para uma consulta SQL de forma independente do banco de dados. Assim, o SearchCop não está vinculado a um 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á usando os métodos Beginning_of_year e End_of_year do ActiveSupport para os valores usados na construção da consulta SQL para este caso.
É claro que essas consultas LIKE '%...%'
não alcançarão o desempenho ideal, mas confira a seção abaixo sobre os recursos de texto completo do SearchCop para entender como as consultas resultantes podem ser otimizadas.
Como Book.search(...)
retorna um ActiveRecord::Relation
, você está livre para pré ou pós-processar os resultados da pesquisa de todas as maneiras possíveis:
Book . where ( available : true ) . search ( "Harry Potter" ) . order ( "books.id desc" ) . paginate ( page : params [ :page ] )
Quando você passa uma string de consulta para o SearchCop, ela é analisada, analisada e mapeada para finalmente construir uma consulta SQL. Para ser mais preciso, quando o SearchCop analisa a consulta, ele cria objetos (nós), que representam as expressões da consulta (nós And-, Or-, Not-, String-, Date-, etc). Para construir a consulta SQL, o SearchCop utiliza o conceito de visitantes como, por exemplo, usado no Arel, de forma que, para cada nó deve haver um visitante, o que transforma o nó em SQL. Quando não há visitante, uma exceção é gerada quando o construtor de consultas tenta “visitar” o nó. Os visitantes são responsáveis por higienizar as informações fornecidas pelo usuário. Isso é feito principalmente por meio de citações (string-, nome-da-tabela-, citação de coluna, etc.). SearchCop está usando os métodos fornecidos pelo adaptador de conexão ActiveRecord para limpeza/cotação para evitar injeção de SQL. Embora nunca possamos estar 100% protegidos contra problemas de segurança, o SearchCop leva os problemas de segurança a sério. Por favor, informe de forma responsável através da segurança em flakks ponto com caso você encontre algum problema relacionado à segurança.
SearchCop oferece suporte a campos json para MySQL, bem como campos json, jsonb e hstore para postgres. Atualmente, sempre se espera que os valores dos campos sejam strings e nenhuma matriz é suportada. Você pode especificar atributos json por meio de:
search_scope :search do
attributes user_agent : "context->browser->user_agent"
# ...
end
onde context
é uma coluna json/jsonb que, por exemplo, contém:
{
"browser" : {
"user_agent" : " Firefox ... "
}
}
Por padrão, ou seja, se você não informar ao SearchCop sobre seus índices de texto completo, o SearchCop usará consultas LIKE '%...%'
. Infelizmente, a menos que você crie um índice trigrama (somente postgres), essas consultas não podem usar índices SQL, de modo que cada linha precisa ser verificada pelo seu RDBMS quando você pesquisa por Book.search("Harry Potter")
ou similar. Para evitar a penalidade de consultas LIKE
, o SearchCop pode explorar os recursos de índice de texto completo do MySQL e PostgreSQL. Para usar índices de texto completo já existentes, basta dizer ao SearchCop para usá-los via:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes :title , :author
options :title , :type => :fulltext
options :author , :type => :fulltext
end
# ...
end
O SearchCop alterará de forma transparente suas consultas SQL para os atributos com índices de texto completo para:
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, essas consultas nem sempre retornarão os mesmos resultados que as consultas curinga LIKE
, porque procuramos palavras em vez de substrings. No entanto, os índices de texto completo geralmente proporcionam melhor desempenho.
Além disso, a consulta acima ainda não é perfeita. Para melhorá-lo ainda mais, o SearchCop tenta otimizar as consultas para fazer uso ideal dos índices de texto completo, ao mesmo tempo que permite misturá-los com atributos que não são de texto completo. Para melhorar ainda mais as consultas, você pode agrupar atributos e especificar um campo padrão para pesquisar, de modo que o SearchCop não precise mais pesquisar em todos os 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
Agora o SearchCop pode otimizar a seguinte consulta, ainda não ideal:
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
para a seguinte consulta com melhor desempenho:
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
O que está acontecendo aqui? Bem, especificamos all
como o nome de um grupo de atributos que consiste em author
e title
. Como, além disso, especificamos que all
é um atributo de texto completo, SearchCop assume que há um índice de texto completo composto presente em author
e title
, de modo que a consulta seja otimizada de acordo. Finalmente, especificamos all
como o atributo padrão para pesquisa, de modo que SearchCop possa ignorar outros atributos, como por exemplo stock
, desde que não sejam especificados diretamente nas consultas (como para stock > 0
).
Outras consultas serão otimizadas de maneira semelhante, de modo que SearchCop tente minimizar as restrições de texto completo dentro de uma consulta, nomeadamente MATCH() AGAINST()
para MySQL e 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 criar um índice de texto completo em books.title
no MySQL, basta usar:
add_index :books , :title , :type => :fulltext
Em relação aos índices compostos, que serão usados, por exemplo, para o campo padrão all
que já especificamos acima, use:
add_index :books , [ :author , :title ] , :type => :fulltext
Por favor note que o MySQL suporta índices de texto completo para MyISAM e, a partir da versão 5.6+ do MySQL, também para InnoDB. Para obter mais detalhes sobre os índices de texto completo do MySQL, visite http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html
Em relação ao PostgreSQL existem mais maneiras de criar um índice de texto completo. No entanto, uma das maneiras mais fáceis é:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
Além disso, para PostgreSQL você deve alterar o formato do esquema em config/application.rb
:
config . active_record . schema_format = :sql
Em relação aos índices compostos para PostgreSQL, use:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
Para lidar corretamente com valores NULL com PostgreSQL, use COALESCE no momento da criação do índice e ao especificar o search_scope
:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', COALESCE(author, '') || ' ' || COALESCE(title, '')))"
mais:
search_scope :search do
attributes :title
options :title , :type => :fulltext , coalesce : true
end
Para usar outro dicionário PostgreSQL que não simple
, você deve criar o índice de acordo e informar o SearchCop sobre isso, por exemplo:
search_scope :search do
attributes :title
options :title , :type => :fulltext , dictionary : "english"
end
Para obter mais detalhes sobre os índices de texto completo do PostgreSQL, visite http://www.postgresql.org/docs/9.3/static/textsearch.html
Caso você exponha atributos que não sejam de texto completo a consultas de pesquisa (preço, estoque, etc.), as respectivas consultas, como Book.search("stock > 0")
, lucrarão com os índices normais sem texto completo. Portanto, você deve adicionar um índice usual em cada coluna exposta às consultas de pesquisa, além de um índice de texto completo para cada atributo de texto completo.
Caso você não possa usar índices de texto completo, porque, por exemplo, ainda está no MySQL 5.5 enquanto usa o InnoDB ou outro RDBMS sem suporte de texto completo, você pode fazer com que seu RDBMS use índices usuais de texto não completo para colunas de string se não precisar do curinga esquerdo nas consultas LIKE
. Basta fornecer a seguinte opção:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username
options :username , left_wildcard : false
end
# ...
de forma que o SearchCop omitirá o curinga mais à esquerda.
User . search ( "admin" )
# ... WHERE users.username LIKE 'admin%'
Da mesma forma, você também pode desativar o curinga correto:
search_scope :search do
attributes :username
options :username , right_wildcard : false
end
Quando você define vários campos em um escopo de pesquisa, o SearcCop usará por padrão o operador AND para concatenar as condições, por exemplo:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username , :fullname
end
# ...
end
Portanto, uma pesquisa como User.search("something")
irá gerar uma consulta com as seguintes condições:
... WHERE username LIKE ' %something% ' AND fullname LIKE ' %something% '
No entanto, há casos em que o uso de AND como operador padrão não é desejado, portanto, o SearchCop permite substituí-lo e usar OR como operador padrão. Uma consulta como User.search("something", default_operator: :or)
irá gerar a consulta usando OR para concatenar as condições
... WHERE username LIKE ' %something% ' OR fullname LIKE ' %something% '
Por fim, observe que você também pode aplicá-lo a índices/consultas de texto completo.
Se você especificar atributos pesquisáveis de outro modelo, como
class Book < ActiveRecord :: Base
# ...
belongs_to :author
search_scope :search do
attributes author : "author.name"
end
# ...
end
Por padrão, o SearchCop irá eager_load
as associações referenciadas, quando você executar Book.search(...)
. Se você não deseja o eager_load
automático ou precisa realizar operações especiais, especifique um scope
:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
scope { joins ( :author ) . eager_load ( :comments ) } # etc.
end
# ...
end
O SearchCop ignorará qualquer carregamento automático de associação e usará o escopo. Você também pode usar scope
junto com aliases
para realizar junções arbitrariamente complexas e pesquisar nos modelos/tabelas unidas:
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
Associações de associações também podem ser referenciadas e usadas:
class Book < ActiveRecord :: Base
# ...
has_many :comments
has_many :users , :through => :comments
search_scope :search do
attributes user : "users.username"
end
# ...
end
SearchCop tenta inferir o nome da classe de um modelo e o alias SQL dos atributos especificados para detectar automaticamente as definições de tipo de dados, etc. Caso você esteja usando nomes de tabelas personalizados via self.table_name = ...
ou se um modelo for associado várias vezes, o SearchCop, entretanto, não pode inferir os nomes de classe e de alias SQL, por exemplo
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
Aqui, para que as consultas funcionem, você deve usar users_books.username
, porque o ActiveRecord atribui um alias SQL diferente para usuários em suas consultas SQL, porque o modelo de usuário é associado várias vezes. No entanto, como o SearchCop agora não pode inferir o modelo User
de users_books
, você deve adicionar:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
aliases :users_books => :users
end
# ...
end
para informar ao SearchCop sobre o alias e mapeamento SQL personalizado. Além disso, você sempre pode fazer as junções por meio de um bloco de scope {}
mais aliases
e usar seus próprios aliases SQL personalizados para se tornar independente dos nomes atribuídos automaticamente pelo ActiveRecord.
As consultas de string de consulta suportam AND/and
, OR/or
, :
, =
, !=
, <
, <=
, >
, >=
, NOT/not/-
, ()
, "..."
e '...'
. Os operadores padrão são AND
e matches
, OR
tem precedência sobre AND
. NOT
só pode ser usado como operador infixo em relação a um único atributo.
Consultas baseadas em hash suportam and: [...]
and or: [...]
, que usam uma matriz de not: {...}
, matches: {...}
, eq: {...}
, not_eq: {...}
, lt: {...}
, lteq: {...}
, gt: {...}
, gteq: {...}
e query: "..."
argumentos. Além disso, query: "..."
possibilita a criação de subconsultas. As outras regras para consultas de string de consulta também se aplicam a consultas baseadas em hash.
SearchCop também oferece a capacidade de definir operadores personalizados definindo um generator
em search_scope
. Eles podem então ser usados com a pesquisa de consulta baseada em hash. Isso é útil quando você deseja usar operadores de banco de dados que não são suportados pelo SearchCop.
Observe que, ao utilizar geradores, você é responsável por higienizar/cotar os valores (veja exemplo abaixo). Caso contrário, seu gerador permitirá a injeção de SQL. Portanto, use geradores apenas se você souber o que está fazendo.
Por exemplo, se você quiser realizar uma consulta LIKE
onde o título de um livro começa com uma string, você pode definir o escopo da pesquisa da seguinte forma:
search_scope :search do
attributes :title
generator :starts_with do | column_name , raw_value |
pattern = " #{ raw_value } %"
" #{ column_name } LIKE #{ quote pattern } "
end
end
Quando você quiser realizar a pesquisa você usa assim:
Book . search ( title : { starts_with : "The Great" } )
Nota de segurança: A consulta retornada do gerador será interpolada diretamente na consulta que vai para o seu banco de dados. Isso abre um possível ponto de injeção de SQL em seu aplicativo. Se você usar esse recurso, certifique-se de que a consulta que está retornando seja segura para execução.
Ao pesquisar em campos booleanos, data e hora, carimbo de data / hora, etc., o SearchCop realiza alguns mapeamentos. As seguintes consultas são equivalentes:
Book . search ( "available:true" )
Book . search ( "available:1" )
Book . search ( "available:yes" )
assim como
Book . search ( "available:false" )
Book . search ( "available:0" )
Book . search ( "available:no" )
Para campos de data e hora e carimbo de data/hora, o SearchCop expande determinados valores em intervalos:
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'
O encadeamento de pesquisas é possível. No entanto, o encadeamento atualmente não permite que o SearchCop otimize as consultas individuais para índices de texto completo.
Book . search ( "Harry" ) . search ( "Potter" )
irá gerar
# 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')
em vez de
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
Assim, se você usar índices de texto completo, é melhor evitar o encadeamento.
Ao usar Model#search
, SearchCop evita convenientemente que certas exceções sejam levantadas caso a string de consulta passada para ele seja inválida (erros de análise, erros de tipo de dados incompatíveis, etc.). Em vez disso, Model#search
retorna uma relação vazia. No entanto, se você precisar depurar determinados casos, use Model#unsafe_search
, que irá aumentá-los.
Book . unsafe_search ( "stock: None" ) # => raise SearchCop::IncompatibleDatatype
SearchCop fornece métodos reflexivos, nomeadamente #attributes
, #default_attributes
, #options
e #aliases
. Você pode usar esses métodos para, por exemplo, fornecer um widget de ajuda de pesquisa individual para seus modelos, que lista os atributos para pesquisar, bem como os padrões, 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 da versão 1.0.0, SearchCop usa Versionamento Semântico: SemVer
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)