PgSearch cria escopos nomeados que aproveitam a pesquisa de texto completo do PostgreSQL.
Leia a postagem do blog apresentando o PgSearch em https://tanzu.vmware.com/content/blog/pg-search-how-i-learned-to-stop-worrying-and-love-postgresql-full-text-search
$ gem install pg_search
ou adicione esta linha ao seu Gemfile:
gem 'pg_search'
Além de instalar e exigir a gema, você pode incluir as tarefas de rake do PgSearch em seu Rakefile. Isto não é necessário para projetos Rails, que ganham as tarefas Rake através de um Railtie.
load "pg_search/tasks.rb"
Para adicionar PgSearch a um modelo Active Record, basta incluir o módulo PgSearch.
class Shape < ActiveRecord :: Base
include PgSearch :: Model
end
multisearchable
pg_search_scope
:tsearch
(pesquisa de texto completo):prefix
(somente PostgreSQL 8.4 e mais recente):negation
:dictionary
:normalization
:any_word
:sort_only
:highlight
:dmetaphone
(pesquisa semelhante a Double Metaphone):trigram
(pesquisa de trigrama):threshold
:word_similarity
:ranked_by
(Escolhendo um algoritmo de classificação):order_within_rank
(Quebrando laços)PgSearch#pg_search_rank
(Lendo a classificação de um registro como Float)pg_search suporta duas técnicas diferentes para pesquisa, pesquisa múltipla e escopos de pesquisa.
A primeira técnica é a pesquisa múltipla, na qual registros de muitas classes diferentes do Active Record podem ser misturados em um índice de pesquisa global em todo o aplicativo. A maioria dos sites que desejam oferecer suporte a uma página de pesquisa genérica desejarão usar esse recurso.
A outra técnica são os escopos de pesquisa, que permitem fazer pesquisas mais avançadas em apenas uma classe do Active Record. Isso é mais útil para criar itens como preenchimentos automáticos ou filtrar uma lista de itens em uma pesquisa facetada.
Antes de usar a pesquisa múltipla, você deve gerar e executar uma migração para criar a tabela do banco de dados pg_search_documents.
$ rails g pg_search:migration:multisearch
$ rake db:migrate
Para adicionar um modelo ao índice de pesquisa global do seu aplicativo, chame multisearchable em sua definição de classe.
class EpicPoem < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :title , :author ]
end
class Flower < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : :color
end
Se este modelo já tiver registros existentes, você precisará reindexar este modelo para obter os registros existentes na tabela pg_search_documents. Veja a tarefa de reconstrução abaixo.
Sempre que um registro é criado, atualizado ou destruído, um retorno de chamada do Active Record será acionado, levando à criação de um registro PgSearch::Document correspondente na tabela pg_search_documents. A opção :against pode ser um ou vários métodos que serão chamados no registro para gerar seu texto de busca.
Você também pode passar um Proc ou nome de método para chamar para determinar se um registro específico deve ou não ser incluído.
class Convertible < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :make , :model ] ,
if : :available_in_red?
end
class Jalopy < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :make , :model ] ,
if : lambda { | record | record . model_year > 1970 }
end
Observe que o Proc ou nome do método é chamado em um gancho after_save. Isso significa que você deve ter cuidado ao usar Time ou outros objetos. No exemplo a seguir, se o registro foi salvo pela última vez antes do carimbo de data e hora publicado_at, ele não será listado na pesquisa global até que seja tocado novamente após o carimbo de data e hora.
class AntipatternExample
include PgSearch :: Model
multisearchable against : [ :contents ] ,
if : :published?
def published?
published_at < Time . now
end
end
problematic_record = AntipatternExample . create! (
contents : "Using :if with a timestamp" ,
published_at : 10 . minutes . from_now
)
problematic_record . published? # => false
PgSearch . multisearch ( "timestamp" ) # => No results
sleep 20 . minutes
problematic_record . published? # => true
PgSearch . multisearch ( "timestamp" ) # => No results
problematic_record . save!
problematic_record . published? # => true
PgSearch . multisearch ( "timestamp" ) # => Includes problematic_record
Atualizar condicionalmente pg_search_documents
Você também pode usar a opção :update_if
para passar um Proc ou nome de método para chamar e determinar se um registro específico deve ou não ser atualizado.
Observe que o Proc ou o nome do método é chamado em um gancho after_save
, portanto, se você estiver contando com sinalizadores sujos do ActiveRecord, use *_previously_changed?
.
class Message < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :body ] ,
update_if : :body_previously_changed?
end
Especifique atributos adicionais a serem salvos na tabela pg_search_documents
Você pode especificar :additional_attributes
para serem salvos na tabela pg_search_documents
. Por exemplo, talvez você esteja indexando um modelo de livro e um modelo de artigo e queira incluir o autor_id.
Primeiro, precisamos adicionar uma referência ao autor para a migração criando nossa tabela pg_search_documents
.
create_table :pg_search_documents do | t |
t . text :content
t . references :author , index : true
t . belongs_to :searchable , polymorphic : true , index : true
t . timestamps null : false
end
Então, podemos enviar este atributo adicional em um lambda
multisearchable (
against : [ :title , :body ] ,
additional_attributes : -> ( article ) { { author_id : article . author_id } }
)
Isso permite pesquisas muito mais rápidas sem junções posteriores, fazendo algo como:
PgSearch . multisearch ( params [ 'search' ] ) . where ( author_id : 2 )
NOTA: No momento, você deve chamar manualmente record.update_pg_search_document
para que o atributo adicional seja incluído na tabela pg_search_documents
Duas associações são construídas automaticamente. No registro original, há uma associação has_one :pg_search_document apontando para o registro PgSearch::Document, e no registro PgSearch::Document há uma associação polimórfica contains_to :searchable apontando de volta para o registro original.
odyssey = EpicPoem . create! ( title : "Odyssey" , author : "Homer" )
search_document = odyssey . pg_search_document #=> PgSearch::Document instance
search_document . searchable #=> #<EpicPoem id: 1, title: "Odyssey", author: "Homer">
Para buscar as entradas PgSearch::Document para todos os registros que correspondem a uma determinada consulta, use PgSearch.multisearch.
odyssey = EpicPoem . create! ( title : "Odyssey" , author : "Homer" )
rose = Flower . create! ( color : "Red" )
PgSearch . multisearch ( "Homer" ) #=> [#<PgSearch::Document searchable: odyssey>]
PgSearch . multisearch ( "Red" ) #=> [#<PgSearch::Document searchable: rose>]
PgSearch.multisearch retorna um ActiveRecord::Relation, assim como os escopos, para que você possa encadear chamadas de escopo até o final. Isso funciona com gemas como Kaminari que adicionam métodos de escopo. Assim como acontece com os escopos regulares, o banco de dados só receberá solicitações SQL quando necessário.
PgSearch . multisearch ( "Bertha" ) . limit ( 10 )
PgSearch . multisearch ( "Juggler" ) . where ( searchable_type : "Occupation" )
PgSearch . multisearch ( "Alamo" ) . page ( 3 ) . per ( 30 )
PgSearch . multisearch ( "Diagonal" ) . find_each do | document |
puts document . searchable . updated_at
end
PgSearch . multisearch ( "Moro" ) . reorder ( "" ) . group ( :searchable_type ) . count ( :all )
PgSearch . multisearch ( "Square" ) . includes ( :searchable )
PgSearch.multisearch pode ser configurado usando as mesmas opções de pg_search_scope
(explicadas com mais detalhes abaixo). Basta definir PgSearch.multisearch_options em um inicializador:
PgSearch . multisearch_options = {
using : [ :tsearch , :trigram ] ,
ignoring : :accents
}
Se você alterar a opção :against em uma classe, adicionar multisearchable a uma classe que já possui registros no banco de dados ou remover multisearchable de uma classe para removê-la do índice, você descobrirá que a tabela pg_search_documents pode se tornar out- sincronizado com os registros reais em suas outras tabelas.
O índice também pode ficar fora de sincronia se você modificar registros de uma forma que não acione retornos de chamada do Active Record. Por exemplo, o método de instância #update_attribute e o método de classe .update_all ignoram retornos de chamada e modificam diretamente o banco de dados.
Para remover todos os documentos de uma determinada classe, você pode simplesmente excluir todos os registros PgSearch::Document.
PgSearch :: Document . delete_by ( searchable_type : "Animal" )
Para regenerar os documentos de uma determinada classe, execute:
PgSearch :: Multisearch . rebuild ( Product )
O método rebuild
excluirá todos os documentos de uma determinada classe antes de regenerá-los. Em algumas situações isso pode não ser desejável, como quando você usa herança de tabela única e searchable_type
é sua classe base. Você pode impedir que rebuild
exclua seus registros da seguinte forma:
PgSearch :: Multisearch . rebuild ( Product , clean_up : false )
rebuild
é executada dentro de uma única transação. Para executar fora de uma transação, você pode passar transactional: false
assim:
PgSearch :: Multisearch . rebuild ( Product , transactional : false )
Reconstruir também está disponível como uma tarefa Rake, por conveniência.
$ rake pg_search:multisearch:rebuild[BlogPost]
Um segundo argumento opcional pode ser passado para especificar o caminho de pesquisa do esquema PostgreSQL a ser usado, para bancos de dados multilocatários que possuem várias tabelas pg_search_documents. A seguir, definiremos o caminho de pesquisa do esquema como "my_schema" antes da reindexação.
$ rake pg_search:multisearch:rebuild[BlogPost,my_schema]
Para modelos que são multipesquisáveis :against
que mapeiam diretamente para atributos do Active Record, uma instrução SQL única e eficiente é executada para atualizar a tabela pg_search_documents
de uma só vez. No entanto, se você chamar qualquer método dinâmico em :against
então update_pg_search_document
será chamado nos registros individuais que estão sendo indexados em lotes.
Você também pode fornecer uma implementação personalizada para reconstruir os documentos adicionando um método de classe chamado rebuild_pg_search_documents
ao seu modelo.
class Movie < ActiveRecord :: Base
belongs_to :director
def director_name
director . name
end
multisearchable against : [ :name , :director_name ]
# Naive approach
def self . rebuild_pg_search_documents
find_each { | record | record . update_pg_search_document }
end
# More sophisticated approach
def self . rebuild_pg_search_documents
connection . execute <<~SQL . squish
INSERT INTO pg_search_documents (searchable_type, searchable_id, content, created_at, updated_at)
SELECT 'Movie' AS searchable_type,
movies.id AS searchable_id,
CONCAT_WS(' ', movies.name, directors.name) AS content,
now() AS created_at,
now() AS updated_at
FROM movies
LEFT JOIN directors
ON directors.id = movies.director_id
SQL
end
end
Nota: Se estiver usando o PostgreSQL antes da versão 9.1, substitua a chamada da função CONCAT_WS()
pela concatenação de canal duplo, por exemplo. (movies.name || ' ' || directors.name)
. No entanto, agora esteja ciente de que se algum dos valores unidos for NULL, o valor content
final também será NULL, enquanto CONCAT_WS()
ignorará seletivamente os valores NULL.
Se você tiver uma grande operação em massa para realizar, como importar muitos registros de uma fonte externa, talvez queira acelerar as coisas desativando a indexação temporariamente. Você poderia então usar uma das técnicas acima para reconstruir os documentos de pesquisa off-line.
PgSearch . disable_multisearch do
Movie . import_from_xml_file ( File . open ( "movies.xml" ) )
end
Você pode usar pg_search_scope para construir um escopo de pesquisa. O primeiro parâmetro é um nome de escopo e o segundo parâmetro é um hash de opções. A única opção necessária é :against, que informa ao pg_search_scope em qual coluna ou colunas pesquisar.
Para pesquisar em uma coluna, passe um símbolo como opção :against.
class BlogPost < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_title , against : :title
end
Agora temos um escopo ActiveRecord denominado search_by_title em nosso modelo BlogPost. Leva um parâmetro, uma string de consulta de pesquisa.
BlogPost . create! ( title : "Recent Developments in the World of Pastrami" )
BlogPost . create! ( title : "Prosciutto and You: A Retrospective" )
BlogPost . search_by_title ( "pastrami" ) # => [#<BlogPost id: 2, title: "Recent Developments in the World of Pastrami">]
Basta passar um Array se quiser pesquisar mais de uma coluna.
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_full_name , against : [ :first_name , :last_name ]
end
Agora, nossa consulta de pesquisa pode corresponder a uma ou ambas as colunas.
person_1 = Person . create! ( first_name : "Grant" , last_name : "Hill" )
person_2 = Person . create! ( first_name : "Hugh" , last_name : "Grant" )
Person . search_by_full_name ( "Grant" ) # => [person_1, person_2]
Person . search_by_full_name ( "Grant Hill" ) # => [person_1]
Assim como acontece com os escopos nomeados do Active Record, você pode passar um objeto Proc que retorna um hash de opções. Por exemplo, o escopo a seguir usa um parâmetro que escolhe dinamicamente em qual coluna pesquisar.
Importante: O hash retornado deve incluir uma chave :query. Seu valor não precisa necessariamente ser dinâmico. Você pode optar por codificá-lo para um valor específico, se desejar.
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_name , lambda { | name_part , query |
raise ArgumentError unless [ :first , :last ] . include? ( name_part )
{
against : name_part ,
query : query
}
}
end
person_1 = Person . create! ( first_name : "Grant" , last_name : "Hill" )
person_2 = Person . create! ( first_name : "Hugh" , last_name : "Grant" )
Person . search_by_name :first , "Grant" # => [person_1]
Person . search_by_name :last , "Grant" # => [person_2]
É possível pesquisar colunas em modelos associados. Observe que se você fizer isso, será impossível acelerar as pesquisas com índices de banco de dados. No entanto, é suportado como uma maneira rápida de experimentar a pesquisa entre modelos.
Você pode passar um Hash para a opção :associated_against para configurar a pesquisa por meio de associações. As chaves são os nomes das associações e o valor funciona como uma opção :against para o outro modelo. No momento, não há suporte para pesquisas mais profundas do que uma associação de distância. Você pode contornar isso configurando uma série de associações :through para apontar até o fim.
class Cracker < ActiveRecord :: Base
has_many :cheeses
end
class Cheese < ActiveRecord :: Base
end
class Salami < ActiveRecord :: Base
include PgSearch :: Model
belongs_to :cracker
has_many :cheeses , through : :cracker
pg_search_scope :tasty_search , associated_against : {
cheeses : [ :kind , :brand ] ,
cracker : :kind
}
end
salami_1 = Salami . create!
salami_2 = Salami . create!
salami_3 = Salami . create!
limburger = Cheese . create! ( kind : "Limburger" )
brie = Cheese . create! ( kind : "Brie" )
pepper_jack = Cheese . create! ( kind : "Pepper Jack" )
Cracker . create! ( kind : "Black Pepper" , cheeses : [ brie ] , salami : salami_1 )
Cracker . create! ( kind : "Ritz" , cheeses : [ limburger , pepper_jack ] , salami : salami_2 )
Cracker . create! ( kind : "Graham" , cheeses : [ limburger ] , salami : salami_3 )
Salami . tasty_search ( "pepper" ) # => [salami_1, salami_2]
Por padrão, pg_search_scope usa a pesquisa de texto integrada do PostgreSQL. Se você passar a opção :using para pg_search_scope, poderá escolher técnicas de pesquisa alternativas.
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name , against : :name , using : [ :tsearch , :trigram , :dmetaphone ]
end
Aqui está um exemplo se você passar várias opções :using com configurações adicionais.
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name ,
against : :name ,
using : {
:trigram => { } ,
:dmetaphone => { } ,
:tsearch => { :prefix => true }
}
end
Os recursos atualmente implementados são
A pesquisa de texto completo integrada do PostgreSQL suporta ponderação, pesquisas de prefixo e lematização em vários idiomas.
Cada coluna pesquisável pode receber um peso de "A", "B", "C" ou "D". Colunas com letras anteriores têm peso maior do que aquelas com letras posteriores. Assim, no exemplo a seguir, o título é o mais importante, seguido do subtítulo e por último o conteúdo.
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : {
title : 'A' ,
subtitle : 'B' ,
content : 'C'
}
end
Você também pode passar os pesos como uma matriz de matrizes ou qualquer outra estrutura que responda a #each e produza um único símbolo ou um símbolo e um peso. Se você omitir o peso, um padrão será usado.
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : [
[ :title , 'A' ] ,
[ :subtitle , 'B' ] ,
[ :content , 'C' ]
]
end
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : [
[ :title , 'A' ] ,
{ subtitle : 'B' } ,
:content
]
end
A pesquisa de texto completo do PostgreSQL corresponde a palavras inteiras por padrão. Se você quiser pesquisar palavras parciais, entretanto, você pode definir :prefix como true. Como esta é uma opção específica de :tsearch, você deve passá-la diretamente para :tsearch, conforme mostrado no exemplo a seguir.
class Superhero < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :whose_name_starts_with ,
against : :name ,
using : {
tsearch : { prefix : true }
}
end
batman = Superhero . create name : 'Batman'
batgirl = Superhero . create name : 'Batgirl'
robin = Superhero . create name : 'Robin'
Superhero . whose_name_starts_with ( "Bat" ) # => [batman, batgirl]
A pesquisa de texto completo do PostgreSQL corresponde a todos os termos de pesquisa por padrão. Se quiser excluir certas palavras, você pode definir :negation como true. Então qualquer termo que comece com um ponto de exclamação !
serão excluídos dos resultados. Como esta é uma opção específica de :tsearch, você deve passá-la diretamente para :tsearch, conforme mostrado no exemplo a seguir.
Observe que combinar isso com outros recursos de pesquisa pode gerar resultados inesperados. Por exemplo, pesquisas :trigram não têm um conceito de termos excluídos e, portanto, se você usar :tsearch e :trigram em conjunto, ainda poderá encontrar resultados que contenham o termo que estava tentando excluir.
class Animal < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :with_name_matching ,
against : :name ,
using : {
tsearch : { negation : true }
}
end
one_fish = Animal . create ( name : "one fish" )
two_fish = Animal . create ( name : "two fish" )
red_fish = Animal . create ( name : "red fish" )
blue_fish = Animal . create ( name : "blue fish" )
Animal . with_name_matching ( "fish !red !blue" ) # => [one_fish, two_fish]
A pesquisa de texto completo do PostgreSQL também oferece suporte a vários dicionários para lematização. Você pode aprender mais sobre como os dicionários funcionam lendo a documentação do PostgreSQL. Se você usar um dos dicionários de idiomas, como "inglês", as variantes das palavras (por exemplo, "pular" e "pular") corresponderão entre si. Se você não deseja lematização, você deve escolher o dicionário "simples" que não faz lematização. Se você não especificar um dicionário, o dicionário "simples" será usado.
class BoringTweet < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :kinda_matching ,
against : :text ,
using : {
tsearch : { dictionary : "english" }
}
pg_search_scope :literally_matching ,
against : :text ,
using : {
tsearch : { dictionary : "simple" }
}
end
sleep = BoringTweet . create! text : "I snoozed my alarm for fourteen hours today. I bet I can beat that tomorrow! #sleep"
sleeping = BoringTweet . create! text : "You know what I like? Sleeping. That's what. #enjoyment"
sleeps = BoringTweet . create! text : "In the jungle, the mighty jungle, the lion sleeps #tonight"
BoringTweet . kinda_matching ( "sleeping" ) # => [sleep, sleeping, sleeps]
BoringTweet . literally_matching ( "sleeps" ) # => [sleeps]
PostgreSQL oferece suporte a vários algoritmos para classificar resultados em relação a consultas. Por exemplo, você pode considerar o tamanho geral do documento ou a distância entre vários termos de pesquisa no texto original. Esta opção recebe um número inteiro, que é passado diretamente para o PostgreSQL. De acordo com a documentação mais recente do PostgreSQL, os algoritmos suportados são:
0 (the default) ignores the document length
1 divides the rank by 1 + the logarithm of the document length
2 divides the rank by the document length
4 divides the rank by the mean harmonic distance between extents
8 divides the rank by the number of unique words in document
16 divides the rank by 1 + the logarithm of the number of unique words in document
32 divides the rank by itself + 1
Este número inteiro é uma máscara de bits, portanto, se você quiser combinar algoritmos, poderá somar seus números. (por exemplo, para usar os algoritmos 1, 8 e 32, você passaria 1 + 8 + 32 = 41)
class BigLongDocument < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :regular_search ,
against : :text
pg_search_scope :short_search ,
against : :text ,
using : {
tsearch : { normalization : 2 }
}
long = BigLongDocument . create! ( text : "Four score and twenty years ago" )
short = BigLongDocument . create! ( text : "Four score" )
BigLongDocument . regular_search ( "four score" ) #=> [long, short]
BigLongDocument . short_search ( "four score" ) #=> [short, long]
Definir este atributo como verdadeiro realizará uma pesquisa que retornará todos os modelos que contenham qualquer palavra nos termos de pesquisa.
class Number < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_any_word ,
against : :text ,
using : {
tsearch : { any_word : true }
}
pg_search_scope :search_all_words ,
against : :text
end
one = Number . create! text : 'one'
two = Number . create! text : 'two'
three = Number . create! text : 'three'
Number . search_any_word ( 'one two three' ) # => [one, two, three]
Number . search_all_words ( 'one two three' ) # => []
Definir esse atributo como true disponibilizará esse recurso para classificação, mas não o incluirá na condição WHERE da consulta.
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search ,
against : :name ,
using : {
tsearch : { any_word : true } ,
dmetaphone : { any_word : true , sort_only : true }
}
end
exact = Person . create! ( name : 'ash hines' )
one_exact_one_close = Person . create! ( name : 'ash heinz' )
one_exact = Person . create! ( name : 'ash smith' )
one_close = Person . create! ( name : 'leigh heinz' )
Person . search ( 'ash hines' ) # => [exact, one_exact_one_close, one_exact]
Adicionando .with_pg_search_highlight após pg_search_scope você pode acessar o atributo pg_highlight
para cada objeto.
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search ,
against : :bio ,
using : {
tsearch : {
highlight : {
StartSel : '<b>' ,
StopSel : '</b>' ,
MaxWords : 123 ,
MinWords : 456 ,
ShortWord : 4 ,
HighlightAll : true ,
MaxFragments : 3 ,
FragmentDelimiter : '…'
}
}
}
end
Person . create! ( :bio => "Born in rural Alberta, where the buffalo roam." )
first_match = Person . search ( "Alberta" ) . with_pg_search_highlight . first
first_match . pg_search_highlight # => "Born in rural <b>Alberta</b>, where the buffalo roam."
A opção destaque aceita todas as opções suportadas por ts_headline e usa os padrões do PostgreSQL.
Consulte a documentação para obter detalhes sobre o significado de cada opção.
Double Metaphone é um algoritmo para combinar palavras que têm sons semelhantes, mesmo que sejam escritas de maneira muito diferente. Por exemplo, "Geoff" e "Jeff" soam idênticos e, portanto, combinam. Atualmente, este não é um verdadeiro metafone duplo, pois apenas o primeiro metafone é usado para pesquisa.
O suporte Double Metaphone está atualmente disponível como parte da extensão fuzzystrmatch que deve ser instalada antes que esse recurso possa ser usado. Além da extensão, você deve instalar uma função utilitária em seu banco de dados. Para gerar e executar uma migração para isso, execute:
$ rails g pg_search:migration:dmetaphone
$ rake db:migrate
O exemplo a seguir mostra como usar :dmetaphone.
class Word < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :that_sounds_like ,
against : :spelling ,
using : :dmetaphone
end
four = Word . create! spelling : 'four'
far = Word . create! spelling : 'far'
fur = Word . create! spelling : 'fur'
five = Word . create! spelling : 'five'
Word . that_sounds_like ( "fir" ) # => [four, far, fur]
A pesquisa de trigramas funciona contando quantas substrings de três letras (ou "trigramas") correspondem entre a consulta e o texto. Por exemplo, a string "Lorem ipsum" pode ser dividida nos seguintes trigramas:
[" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
A pesquisa de trigrama tem alguma capacidade de funcionar mesmo com erros de digitação e ortografia na consulta ou no texto.
O suporte Trigram está atualmente disponível como parte da extensão pg_trgm que deve ser instalada antes que este recurso possa ser usado.
class Website < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :kinda_spelled_like ,
against : :name ,
using : :trigram
end
yahooo = Website . create! name : "Yahooo!"
yohoo = Website . create! name : "Yohoo!"
gogle = Website . create! name : "Gogle"
facebook = Website . create! name : "Facebook"
Website . kinda_spelled_like ( "Yahoo!" ) # => [yahooo, yohoo]
Por padrão, as pesquisas de trigramas encontram registros que possuem uma similaridade de pelo menos 0,3 usando os cálculos do pg_trgm. Você pode especificar um limite personalizado, se preferir. Números mais altos correspondem de forma mais estrita e, portanto, retornam menos resultados. Números mais baixos correspondem de forma mais permissiva, permitindo a entrada de mais resultados. Observe que definir um limite de trigrama forçará uma varredura da tabela, pois a consulta derivada usa a função similarity()
em vez do operador %
.
class Vegetable < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :strictly_spelled_like ,
against : :name ,
using : {
trigram : {
threshold : 0.5
}
}
pg_search_scope :roughly_spelled_like ,
against : :name ,
using : {
trigram : {
threshold : 0.1
}
}
end
cauliflower = Vegetable . create! name : "cauliflower"
Vegetable . roughly_spelled_like ( "couliflower" ) # => [cauliflower]
Vegetable . strictly_spelled_like ( "couliflower" ) # => [cauliflower]
Vegetable . roughly_spelled_like ( "collyflower" ) # => [cauliflower]
Vegetable . strictly_spelled_like ( "collyflower" ) # => []
Permite combinar palavras em strings mais longas. Por padrão, as pesquisas de trigramas usam %
ou similarity()
como valor de similaridade. Defina word_similarity
como true
para optar por <%
e word_similarity
. Isso faz com que a busca do trigrama use a similaridade do termo de consulta e a palavra com maior similaridade.
class Sentence < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :similarity_like ,
against : :name ,
using : {
trigram : {
word_similarity : true
}
}
pg_search_scope :word_similarity_like ,
against : :name ,
using : [ :trigram ]
end
sentence = Sentence . create! name : "Those are two words."
Sentence . similarity_like ( "word" ) # => []
Sentence . word_similarity_like ( "word" ) # => [sentence]
Às vezes, ao fazer consultas que combinam recursos diferentes, você pode querer pesquisar apenas alguns dos campos com determinados recursos. Por exemplo, talvez você queira fazer apenas uma pesquisa de trigrama nos campos mais curtos, para não precisar reduzir excessivamente o limite. Você pode especificar quais campos usando a opção 'somente':
class Image < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :combined_search ,
against : [ :file_name , :short_description , :long_description ]
using : {
tsearch : { dictionary : 'english' } ,
trigram : {
only : [ :file_name , :short_description ]
}
}
end
Agora você pode recuperar com sucesso uma imagem com um file_name: 'image_foo.jpg' e long_description: 'Esta descrição é tão longa que faria uma pesquisa de trigrama falhar em qualquer limite razoável' com:
Image . combined_search ( 'reasonable' ) # found with tsearch
Image . combined_search ( 'foo' ) # found with trigram
Na maioria das vezes, você desejará ignorar os acentos ao pesquisar. Isso permite encontrar palavras como "piñata" ao pesquisar com a consulta "pinata". Se você definir um pg_search_scope para ignorar acentos, ele ignorará os acentos tanto no texto pesquisável quanto nos termos da consulta.
Ignorar acentos usa a extensão sem acentos que deve ser instalada antes que esse recurso possa ser usado.
class SpanishQuestion < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :gringo_search ,
against : :word ,
ignoring : :accents
end
what = SpanishQuestion . create ( word : "Qué" )
how_many = SpanishQuestion . create ( word : "Cuánto" )
how = SpanishQuestion . create ( word : "Cómo" )
SpanishQuestion . gringo_search ( "Que" ) # => [what]
SpanishQuestion . gringo_search ( "Cüåñtô" ) # => [how_many]
Usuários avançados podem querer adicionar índices para as expressões geradas por pg_search. Infelizmente, a função unaccent fornecida por esta extensão não é indexável (a partir do PostgreSQL 9.1). Portanto, você pode querer escrever sua própria função wrapper e usá-la. Isso pode ser configurado chamando o código a seguir, talvez em um inicializador.
PgSearch . unaccent_function = "my_unaccent"
O PostgreSQL permite pesquisar em uma coluna com o tipo tsvector em vez de usar uma expressão; isso acelera drasticamente a pesquisa, pois descarrega a criação do tsvector contra o qual o tsquery é avaliado.
Para usar esta funcionalidade, você precisará fazer algumas coisas:
Crie uma coluna do tipo tsvector que você gostaria de pesquisar. Se quiser pesquisar usando vários métodos de pesquisa, por exemplo tsearch e dmetaphone, você precisará de uma coluna para cada um.
Crie uma função de gatilho que atualizará as colunas usando a expressão apropriada para esse tipo de pesquisa. Consulte: a documentação do PostgreSQL para gatilhos de pesquisa de texto
Se você tiver dados pré-existentes na tabela, atualize as colunas tsvector recém-criadas com a expressão que sua função de gatilho usa.
Adicione a opção ao pg_search_scope, por exemplo:
pg_search_scope :fast_content_search ,
against : :content ,
using : {
dmetaphone : {
tsvector_column : 'tsvector_content_dmetaphone'
} ,
tsearch : {
dictionary : 'english' ,
tsvector_column : 'tsvector_content_tsearch'
} ,
trigram : { } # trigram does not use tsvectors
}
Observe que a coluna :against só é usada quando tsvector_column não está presente para o tipo de pesquisa.
É possível pesquisar mais de um tsvector por vez. Isso pode ser útil se você quiser manter vários escopos de pesquisa, mas não quiser manter tsvectors separados para cada escopo. Por exemplo:
pg_search_scope :search_title ,
against : :title ,
using : {
tsearch : {
tsvector_column : "title_tsvector"
}
}
pg_search_scope :search_body ,
against : :body ,
using : {
tsearch : {
tsvector_column : "body_tsvector"
}
}
pg_search_scope :search_title_and_body ,
against : [ :title , :body ] ,
using : {
tsearch : {
tsvector_column : [ "title_tsvector" , "body_tsvector" ]
}
}
Por padrão, pg_search classifica os resultados com base na semelhança :tsearch entre o texto pesquisável e a consulta. Para usar um algoritmo de classificação diferente, você pode passar uma opção :ranked_by para pg_search_scope.
pg_search_scope :search_by_tsearch_but_rank_by_trigram ,
against : :title ,
using : [ :tsearch ] ,
ranked_by : ":trigram"
Observe que :ranked_by usa uma String para representar a expressão de classificação. Isso permite possibilidades mais complexas. Strings como ":tsearch", ":trigram" e ":dmetaphone" são automaticamente expandidas nas expressões SQL apropriadas.
# Weighted ranking to balance multiple approaches
ranked_by : ":dmetaphone + (0.25 * :trigram)"
# A more complex example, where books.num_pages is an integer column in the table itself
ranked_by : "(books.num_pages * :trigram) + (:tsearch / 2.0)"
O PostgreSQL não garante uma ordem consistente quando vários registros têm o mesmo valor na cláusula ORDER BY. Isso pode causar problemas com a paginação. Imagine um caso em que 12 registros têm o mesmo valor de classificação. Se você usar uma biblioteca de paginação como kaminari ou will_paginate para retornar resultados em páginas de 10, então você esperaria ver 10 registros na página 1 e os 2 registros restantes no topo da próxima página, à frente dos registros inferiores. resultados classificados.
Mas como não existe uma ordem consistente, o PostgreSQL pode optar por reorganizar a ordem desses 12 registros entre diferentes instruções SQL. Você pode acabar obtendo alguns dos mesmos registros da página 1 na página 2 também e, da mesma forma, pode haver registros que nem aparecem.
pg_search corrige esse problema adicionando uma segunda expressão à cláusula ORDER BY, após a expressão :ranked_by explicada acima. Por padrão, a ordem de desempate é crescente por id.
ORDER BY [complicated :ranked_by expression...], id ASC
Isso pode não ser desejável para seu aplicativo, especialmente se você não deseja que os registros antigos superem os novos registros. Ao passar um :order_within_rank, você pode especificar uma expressão de desempate alternativa. Um exemplo comum seria decrescer por atualizado_at, para classificar primeiro os registros atualizados mais recentemente.
pg_search_scope :search_and_break_ties_by_latest_update ,
against : [ :title , :content ] ,
order_within_rank : "blog_posts.updated_at DESC"
Pode ser útil ou interessante ver a classificação de um registro específico. Isso pode ser útil para depurar por que um registro supera outro. Você também pode usá-lo para mostrar algum tipo de valor de relevância para os usuários finais de um aplicativo.
Para recuperar a classificação, chame .with_pg_search_rank
em um escopo e, em seguida, chame .pg_search_rank
em um registro retornado.
shirt_brands = ShirtBrand . search_by_name ( "Penguin" ) . with_pg_search_rank
shirt_brands [ 0 ] . pg_search_rank #=> 0.0759909
shirt_brands [ 1 ] . pg_search_rank #=> 0.0607927
Cada escopo PgSearch gera uma subconsulta nomeada para a classificação de pesquisa. Se você encadear vários escopos, o PgSearch gerará uma consulta de classificação para cada escopo, portanto, as consultas de classificação deverão ter nomes exclusivos. Se você precisar fazer referência à consulta de classificação (por exemplo, em uma cláusula GROUP BY), você pode gerar novamente o nome da subconsulta com o método PgScope::Configuration.alias
, passando o nome da tabela consultada.
shirt_brands = ShirtBrand . search_by_name ( "Penguin" )
. joins ( :shirt_sizes )
. group ( "shirt_brands.id, #{ PgSearch :: Configuration . alias ( 'shirt_brands' ) } .rank" )
O PgSearch não teria sido possível sem a inspiração do texto (agora renomeado como textacular). Obrigado a Aaron Patterson pela versão original e ao Casebook PBC (https://www.casebook.net) por presentear a comunidade com ela!
Por favor, leia nosso guia de CONTRIBUIÇÃO.
Também temos um Grupo do Google para discutir o pg_search e outros projetos de código aberto do Casebook PBC.
Copyright © 2010–2022 Casebook PBC. Licenciado sob a licença MIT, consulte o arquivo LICENSE.