PgSearch crée des étendues nommées qui tirent parti de la recherche en texte intégral de PostgreSQL.
Lisez le billet de blog présentant PgSearch sur 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 ajoutez cette ligne à votre Gemfile :
gem 'pg_search'
En plus d'installer et d'exiger la gemme, vous souhaiterez peut-être inclure les tâches de rake PgSearch dans votre Rakefile. Cela n'est pas nécessaire pour les projets Rails, qui obtiennent les tâches Rake via un Railtie.
load "pg_search/tasks.rb"
Pour ajouter PgSearch à un modèle Active Record, incluez simplement le module PgSearch.
class Shape < ActiveRecord :: Base
include PgSearch :: Model
end
multisearchable
pg_search_scope
:tsearch
(Recherche en texte intégral):prefix
(PostgreSQL 8.4 et versions ultérieures uniquement):negation
:dictionary
:normalization
:any_word
:sort_only
:highlight
:dmetaphone
(recherche sonore Double Metaphone):trigram
(Recherche de trigramme):threshold
:word_similarity
:ranked_by
(Choisir un algorithme de classement):order_within_rank
(Briser les égalités)PgSearch#pg_search_rank
(Lecture du classement d'un enregistrement en tant que Float)pg_search prend en charge deux techniques différentes pour la recherche, la recherche multiple et les étendues de recherche.
La première technique est la recherche multiple, dans laquelle les enregistrements de nombreuses classes d'enregistrements actifs différentes peuvent être mélangés dans un seul index de recherche global sur l'ensemble de votre application. La plupart des sites souhaitant prendre en charge une page de recherche générique souhaiteront utiliser cette fonctionnalité.
L’autre technique concerne les étendues de recherche, qui vous permettent d’effectuer des recherches plus avancées sur une seule classe Active Record. Ceci est plus utile pour créer des éléments tels que des compléteurs automatiques ou filtrer une liste d'éléments dans une recherche à facettes.
Avant d'utiliser la recherche multiple, vous devez générer et exécuter une migration pour créer la table de base de données pg_search_documents.
$ rails g pg_search:migration:multisearch
$ rake db:migrate
Pour ajouter un modèle à l'index de recherche global de votre application, appelez multisearchable dans sa définition 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
Si ce modèle contient déjà des enregistrements existants, vous devrez réindexer ce modèle pour obtenir les enregistrements existants dans la table pg_search_documents. Voir la tâche de reconstruction ci-dessous.
Chaque fois qu'un enregistrement est créé, mis à jour ou détruit, un rappel Active Record se déclenche, conduisant à la création d'un enregistrement PgSearch::Document correspondant dans la table pg_search_documents. L'option :contre peut être une ou plusieurs méthodes qui seront appelées sur l'enregistrement pour générer son texte de recherche.
Vous pouvez également transmettre un nom de procédure ou de méthode à appeler pour déterminer si un enregistrement particulier doit être inclus ou non.
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
Notez que le nom du Proc ou de la méthode est appelé dans un hook after_save. Cela signifie que vous devez être prudent lorsque vous utilisez Time ou d'autres objets. Dans l'exemple suivant, si l'enregistrement a été enregistré pour la dernière fois avant l'horodatage publié_at, il ne sera pas du tout répertorié dans la recherche globale jusqu'à ce qu'il soit à nouveau touché après l'horodatage.
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
Mettre à jour conditionnellement pg_search_documents
Vous pouvez également utiliser l'option :update_if
pour transmettre un nom de procédure ou de méthode à appeler pour déterminer si un enregistrement particulier doit ou non être mis à jour.
Notez que le nom du Proc ou de la méthode est appelé dans un hook after_save
, donc si vous comptez sur les indicateurs sales d'ActiveRecord, utilisez *_previously_changed?
.
class Message < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :body ] ,
update_if : :body_previously_changed?
end
Spécifiez les attributs supplémentaires à enregistrer sur la table pg_search_documents
Vous pouvez spécifier :additional_attributes
à enregistrer dans la table pg_search_documents
. Par exemple, vous indexez peut-être un modèle de livre et un modèle d'article et souhaitez inclure le author_id.
Tout d'abord, nous devons ajouter une référence à l'auteur à la migration créant notre table 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
Ensuite, nous pouvons envoyer cet attribut supplémentaire dans un lambda
multisearchable (
against : [ :title , :body ] ,
additional_attributes : -> ( article ) { { author_id : article . author_id } }
)
Cela permet des recherches beaucoup plus rapides sans jointures ultérieures en faisant quelque chose comme :
PgSearch . multisearch ( params [ 'search' ] ) . where ( author_id : 2 )
REMARQUE : Vous devez actuellement appeler manuellement record.update_pg_search_document
pour que l'attribut supplémentaire soit inclus dans la table pg_search_documents.
Deux associations sont construites automatiquement. Sur l'enregistrement d'origine, il y a une association has_one :pg_search_document pointant vers l'enregistrement PgSearch::Document, et sur l'enregistrement PgSearch::Document, il y a une association polymorphe appartiennent_to :searchable pointant vers l'enregistrement d'origine.
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">
Pour récupérer les entrées PgSearch::Document pour tous les enregistrements qui correspondent à une requête donnée, utilisez 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 renvoie un ActiveRecord::Relation, tout comme le font les scopes, afin que vous puissiez enchaîner les appels de scope jusqu'à la fin. Cela fonctionne avec des gemmes comme Kaminari qui ajoutent des méthodes de portée. Tout comme avec les étendues normales, la base de données ne recevra les requêtes SQL que lorsque cela est nécessaire.
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 peut être configuré en utilisant les mêmes options que pg_search_scope
(expliquées plus en détail ci-dessous). Définissez simplement PgSearch.multisearch_options dans un initialiseur :
PgSearch . multisearch_options = {
using : [ :tsearch , :trigram ] ,
ignoring : :accents
}
Si vous modifiez l'option :against sur une classe, ajoutez multisearchable à une classe qui a déjà des enregistrements dans la base de données, ou supprimez multisearchable d'une classe afin de la supprimer de l'index, vous constaterez que la table pg_search_documents pourrait devenir obsolète. de synchronisation avec les enregistrements réels de vos autres tables.
L'index peut également devenir désynchronisé si jamais vous modifiez des enregistrements d'une manière qui ne déclenche pas de rappels Active Record. Par exemple, la méthode d'instance #update_attribute et la méthode de classe .update_all ignorent toutes deux les rappels et modifient directement la base de données.
Pour supprimer tous les documents d'une classe donnée, vous pouvez simplement supprimer tous les enregistrements PgSearch::Document.
PgSearch :: Document . delete_by ( searchable_type : "Animal" )
Pour régénérer les documents d'une classe donnée, exécutez :
PgSearch :: Multisearch . rebuild ( Product )
La méthode rebuild
supprimera tous les documents de la classe donnée avant de les régénérer. Dans certaines situations, cela peut ne pas être souhaitable, par exemple lorsque vous utilisez l'héritage de table unique et searchable_type
est votre classe de base. Vous pouvez empêcher rebuild
de supprimer vos enregistrements comme suit :
PgSearch :: Multisearch . rebuild ( Product , clean_up : false )
rebuild
s'exécute en une seule transaction. Pour exécuter en dehors d'une transaction, vous pouvez passer transactional: false
comme ceci :
PgSearch :: Multisearch . rebuild ( Product , transactional : false )
La reconstruction est également disponible en tant que tâche Rake, pour plus de commodité.
$ rake pg_search:multisearch:rebuild[BlogPost]
Un deuxième argument facultatif peut être passé pour spécifier le chemin de recherche du schéma PostgreSQL à utiliser, pour les bases de données multi-tenant qui ont plusieurs tables pg_search_documents. Ce qui suit définira le chemin de recherche du schéma sur "my_schema" avant la réindexation.
$ rake pg_search:multisearch:rebuild[BlogPost,my_schema]
Pour les modèles multirecherchables :against
les méthodes qui correspondent directement aux attributs Active Record, une seule instruction SQL efficace est exécutée pour mettre à jour la table pg_search_documents
en une seule fois. Cependant, si vous appelez des méthodes dynamiques dans :against
, alors update_pg_search_document
sera appelé sur les enregistrements individuels indexés par lots.
Vous pouvez également fournir une implémentation personnalisée pour reconstruire les documents en ajoutant une méthode de classe appelée rebuild_pg_search_documents
à votre modèle.
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
Remarque : si vous utilisez PostgreSQL avant 9.1, remplacez l'appel de fonction CONCAT_WS()
par une concaténation double-pipe, par exemple. (movies.name || ' ' || directors.name)
. Cependant, sachez maintenant que si l'une des valeurs jointes est NULL, alors la valeur content
finale sera également NULL, tandis que CONCAT_WS()
ignorera sélectivement les valeurs NULL.
Si vous devez effectuer une opération en masse importante, telle que l'importation d'un grand nombre d'enregistrements à partir d'une source externe, vous souhaiterez peut-être accélérer les choses en désactivant temporairement l'indexation. Vous pouvez ensuite utiliser l'une des techniques ci-dessus pour reconstruire les documents de recherche hors ligne.
PgSearch . disable_multisearch do
Movie . import_from_xml_file ( File . open ( "movies.xml" ) )
end
Vous pouvez utiliser pg_search_scope pour créer une étendue de recherche. Le premier paramètre est un nom de portée et le deuxième paramètre est un hachage d'options. La seule option obligatoire est :against, qui indique à pg_search_scope la ou les colonnes sur lesquelles effectuer la recherche.
Pour effectuer une recherche dans une colonne, transmettez un symbole comme option :contre.
class BlogPost < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_title , against : :title
end
Nous avons maintenant une portée ActiveRecord nommée search_by_title sur notre modèle BlogPost. Il prend un paramètre, une chaîne de requête de recherche.
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">]
Transmettez simplement un tableau si vous souhaitez rechercher plusieurs colonnes.
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_full_name , against : [ :first_name , :last_name ]
end
Désormais, notre requête de recherche peut correspondre à l’une ou aux deux colonnes.
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]
Tout comme avec les étendues nommées Active Record, vous pouvez transmettre un objet Proc qui renvoie un hachage d'options. Par exemple, la portée suivante prend un paramètre qui choisit dynamiquement la colonne sur laquelle effectuer la recherche.
Important : Le hachage renvoyé doit inclure une clé :query. Sa valeur ne doit pas nécessairement être dynamique. Vous pouvez choisir de le coder en dur sur une valeur spécifique si vous le souhaitez.
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]
Il est possible de rechercher des colonnes sur des modèles associés. Notez que si vous faites cela, il sera impossible d'accélérer les recherches avec les index des bases de données. Cependant, il est pris en charge comme moyen rapide d’essayer la recherche multi-modèles.
Vous pouvez transmettre un hachage dans l'option :associated_against pour configurer la recherche via les associations. Les clés sont les noms des associations et la valeur fonctionne comme une option :contre pour l'autre modèle. À l’heure actuelle, la recherche à plus d’une association n’est pas prise en charge. Vous pouvez contourner ce problème en configurant une série d'associations :through pour pointer jusqu'au bout.
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]
Par défaut, pg_search_scope utilise la recherche de texte intégrée à PostgreSQL. Si vous transmettez l'option :using à pg_search_scope, vous pouvez choisir des techniques de recherche alternatives.
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name , against : :name , using : [ :tsearch , :trigram , :dmetaphone ]
end
Voici un exemple si vous transmettez plusieurs options :using avec des configurations supplémentaires.
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name ,
against : :name ,
using : {
:trigram => { } ,
:dmetaphone => { } ,
:tsearch => { :prefix => true }
}
end
Les fonctionnalités actuellement implémentées sont
La recherche en texte intégral intégrée de PostgreSQL prend en charge la pondération, les recherches de préfixes et la recherche radicale dans plusieurs langues.
Chaque colonne consultable peut recevoir un poids de "A", "B", "C" ou "D". Les colonnes contenant des lettres antérieures ont une pondération plus élevée que celles contenant des lettres ultérieures. Ainsi, dans l’exemple suivant, le titre est le plus important, suivi du sous-titre et enfin du contenu.
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : {
title : 'A' ,
subtitle : 'B' ,
content : 'C'
}
end
Vous pouvez également transmettre les poids sous forme de tableau de tableaux ou de toute autre structure qui répond à #each et génère soit un symbole unique, soit un symbole et un poids. Si vous omettez le poids, un poids par défaut sera utilisé.
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
La recherche en texte intégral de PostgreSQL correspond par défaut à des mots entiers. Toutefois, si vous souhaitez rechercher des mots partiels, vous pouvez définir :prefix sur true. Puisqu'il s'agit d'une option spécifique à :tsearch, vous devez la transmettre directement à :tsearch, comme le montre l'exemple suivant.
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]
La recherche en texte intégral de PostgreSQL correspond par défaut à tous les termes de recherche. Si vous souhaitez exclure certains mots, vous pouvez définir :negation sur true. Alors n'importe quel terme qui commence par un point d'exclamation !
seront exclus des résultats. Puisqu'il s'agit d'une option spécifique à :tsearch, vous devez la transmettre directement à :tsearch, comme le montre l'exemple suivant.
Notez que combiner cela avec d’autres fonctionnalités de recherche peut donner des résultats inattendus. Par exemple, les recherches :trigram n'ont pas de concept de termes exclus, et donc si vous utilisez à la fois :tsearch et :trigram en tandem, vous pouvez toujours trouver des résultats contenant le terme que vous tentiez d'exclure.
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]
La recherche en texte intégral PostgreSQL prend également en charge plusieurs dictionnaires pour la recherche de racines. Vous pouvez en savoir plus sur le fonctionnement des dictionnaires en lisant la documentation PostgreSQL. Si vous utilisez l'un des dictionnaires de langue, par exemple « anglais », les variantes de mots (par exemple « jumping » et « jumped ») se correspondent. Si vous ne voulez pas de radicalisation, vous devez choisir le dictionnaire "simple" qui ne fait aucune radicalisation. Si vous ne précisez pas de dictionnaire, le dictionnaire "simple" sera utilisé.
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 prend en charge plusieurs algorithmes pour classer les résultats par rapport aux requêtes. Par exemple, vous souhaiterez peut-être prendre en compte la taille globale du document ou la distance entre plusieurs termes de recherche dans le texte original. Cette option prend un entier, qui est transmis directement à PostgreSQL. Selon la dernière documentation PostgreSQL, les algorithmes pris en charge sont :
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
Cet entier est un masque de bits, donc si vous souhaitez combiner des algorithmes, vous pouvez additionner leurs nombres. (par exemple, pour utiliser les algorithmes 1, 8 et 32, vous passeriez 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]
Définir cet attribut sur true effectuera une recherche qui renverra tous les modèles contenant un mot dans les termes de recherche.
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' ) # => []
Définir cet attribut sur true rendra cette fonctionnalité disponible pour le tri, mais ne l'inclura pas dans la condition WHERE de la requête.
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]
En ajoutant .with_pg_search_highlight après pg_search_scope, vous pouvez accéder à l'attribut pg_highlight
pour chaque objet.
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."
L'option highlight accepte toutes les options prises en charge par ts_headline et utilise les valeurs par défaut de PostgreSQL.
Consultez la documentation pour plus de détails sur la signification de chaque option.
Double Metaphone est un algorithme permettant de faire correspondre des mots qui se ressemblent même s'ils sont orthographiés très différemment. Par exemple, « Geoff » et « Jeff » sonnent de manière identique et correspondent donc. Actuellement, il ne s’agit pas d’un véritable double métaphone, car seul le premier métaphone est utilisé pour la recherche.
La prise en charge de Double Metaphone est actuellement disponible dans le cadre de l'extension fuzzystrmatch qui doit être installée avant que cette fonctionnalité puisse être utilisée. En plus de l'extension, vous devez installer une fonction utilitaire dans votre base de données. Pour générer et exécuter une migration pour cela, exécutez :
$ rails g pg_search:migration:dmetaphone
$ rake db:migrate
L'exemple suivant montre comment utiliser :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]
La recherche de trigrammes fonctionne en comptant le nombre de sous-chaînes de trois lettres (ou « trigrammes ») qui correspondent entre la requête et le texte. Par exemple, la chaîne « Lorem ipsum » peut être divisée en trigrammes suivants :
[" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
La recherche par trigramme a une certaine capacité à fonctionner même avec des fautes de frappe et d'orthographe dans la requête ou le texte.
La prise en charge de Trigram est actuellement disponible dans le cadre de l'extension pg_trgm qui doit être installée avant que cette fonctionnalité puisse être utilisée.
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]
Par défaut, les recherches de trigrammes trouvent les enregistrements qui ont une similarité d'au moins 0,3 en utilisant les calculs de pg_trgm. Vous pouvez spécifier un seuil personnalisé si vous préférez. Des nombres plus élevés correspondent plus strictement et renvoient donc moins de résultats. Les nombres inférieurs correspondent de manière plus permissive, laissant entrer plus de résultats. Veuillez noter que la définition d'un seuil de trigramme forcera une analyse de table car la requête dérivée utilise la fonction similarity()
au lieu de l'opérateur %
.
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" ) # => []
Vous permet de faire correspondre des mots dans des chaînes plus longues. Par défaut, les recherches de trigrammes utilisent %
ou similarity()
comme valeur de similarité. Définissez word_similarity
sur true
pour opter pour <%
et word_similarity
à la place. Cela amène la recherche par trigramme à utiliser la similarité du terme de requête et le mot présentant la plus grande similarité.
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]
Parfois, lorsque vous effectuez des requêtes combinant différentes fonctionnalités, vous souhaiterez peut-être effectuer une recherche uniquement sur certains champs présentant certaines fonctionnalités. Par exemple, vous souhaitez peut-être effectuer une recherche de trigramme uniquement sur les champs les plus courts afin de ne pas avoir besoin de réduire excessivement le seuil. Vous pouvez spécifier quels champs en utilisant l'option « uniquement » :
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
Vous pouvez maintenant récupérer avec succès une image avec un nom de fichier : 'image_foo.jpg' et long_description : 'Cette description est si longue qu'elle ferait échouer une recherche de trigramme à tout seuil raisonnable' avec :
Image . combined_search ( 'reasonable' ) # found with tsearch
Image . combined_search ( 'foo' ) # found with trigram
La plupart du temps, vous souhaiterez ignorer les accents lors de la recherche. Cela permet de trouver des mots comme « piñata » lors d'une recherche avec la requête « pinata ». Si vous définissez un pg_search_scope pour ignorer les accents, il ignorera les accents à la fois dans le texte consultable et dans les termes de la requête.
Ignorer les accents utilise l'extension unaccent qui doit être installée avant que cette fonctionnalité puisse être utilisée.
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]
Les utilisateurs avancés souhaiteront peut-être ajouter des index pour les expressions générées par pg_search. Malheureusement, la fonction unaccent fournie par cette extension n'est pas indexable (à partir de PostgreSQL 9.1). Ainsi, vous souhaiterez peut-être écrire votre propre fonction wrapper et l’utiliser à la place. Cela peut être configuré en appelant le code suivant, peut-être dans un initialiseur.
PgSearch . unaccent_function = "my_unaccent"
PostgreSQL vous permet d'effectuer une recherche sur une colonne de type tsvector au lieu d'utiliser une expression ; cela accélère considérablement la recherche car cela décharge la création du vecteur ts par rapport auquel le tsquery est évalué.
Pour utiliser cette fonctionnalité, vous devrez effectuer plusieurs opérations :
Créez une colonne de type tsvector sur laquelle vous souhaitez effectuer une recherche. Si vous souhaitez effectuer une recherche en utilisant plusieurs méthodes de recherche, par exemple tsearch et dmetaphone, vous aurez besoin d'une colonne pour chacune.
Créez une fonction de déclenchement qui mettra à jour la ou les colonnes à l'aide de l'expression appropriée à ce type de recherche. Voir : la documentation PostgreSQL pour les déclencheurs de recherche de texte
Si vous avez des données préexistantes dans le tableau, mettez à jour les colonnes tsvector nouvellement créées avec l'expression utilisée par votre fonction de déclenchement.
Ajoutez l'option à pg_search_scope, par exemple :
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
}
Veuillez noter que la colonne :against n'est utilisée que lorsque le tsvector_column n'est pas présent pour le type de recherche.
Il est possible de rechercher plusieurs vecteurs ts à la fois. Cela peut être utile si vous souhaitez conserver plusieurs étendues de recherche mais ne souhaitez pas conserver des tsvectors distincts pour chaque étendue. Par exemple:
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" ]
}
}
Par défaut, pg_search classe les résultats en fonction de la similarité :tsearch entre le texte consultable et la requête. Pour utiliser un algorithme de classement différent, vous pouvez passer une option :ranked_by à pg_search_scope.
pg_search_scope :search_by_tsearch_but_rank_by_trigram ,
against : :title ,
using : [ :tsearch ] ,
ranked_by : ":trigram"
Notez que :ranked_by utilise une chaîne pour représenter l'expression de classement. Cela permet des possibilités plus complexes. Les chaînes telles que ":tsearch", ":trigram" et ":dmetaphone" sont automatiquement développées dans les expressions SQL appropriées.
# 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)"
PostgreSQL ne garantit pas un ordre cohérent lorsque plusieurs enregistrements ont la même valeur dans la clause ORDER BY. Cela peut entraîner des problèmes de pagination. Imaginez un cas où 12 enregistrements ont tous la même valeur de classement. Si vous utilisez une bibliothèque de pagination telle que kaminari ou will_paginate pour renvoyer des résultats dans des pages de 10, vous vous attendez à voir 10 des enregistrements sur la page 1 et les 2 enregistrements restants en haut de la page suivante, avant ceux du bas. résultats classés.
Mais comme il n'y a pas d'ordre cohérent, PostgreSQL peut choisir de réorganiser l'ordre de ces 12 enregistrements entre différentes instructions SQL. Vous pourriez finir par obtenir certains des mêmes enregistrements de la page 1 à la page 2 également, et de même, il se peut que certains enregistrements n'apparaissent pas du tout.
pg_search résout ce problème en ajoutant une deuxième expression à la clause ORDER BY, après l'expression :ranked_by expliquée ci-dessus. Par défaut, l'ordre de départage est croissant par identifiant.
ORDER BY [complicated :ranked_by expression...], id ASC
Cela n'est peut-être pas souhaitable pour votre application, surtout si vous ne souhaitez pas que les anciens enregistrements soient plus performants que les nouveaux. En passant un :order_within_rank, vous pouvez spécifier une autre expression de départage. Un exemple courant serait de descendre par update_at, pour classer en premier les enregistrements les plus récemment mis à jour.
pg_search_scope :search_and_break_ties_by_latest_update ,
against : [ :title , :content ] ,
order_within_rank : "blog_posts.updated_at DESC"
Il peut être utile ou intéressant de voir le classement d'un enregistrement particulier. Cela peut être utile pour déboguer pourquoi un enregistrement surpasse un autre. Vous pouvez également l'utiliser pour afficher une sorte de valeur de pertinence aux utilisateurs finaux d'une application.
Pour récupérer le classement, appelez .with_pg_search_rank
sur une étendue, puis appelez .pg_search_rank
sur un enregistrement renvoyé.
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
Chaque portée PgSearch génère une sous-requête nommée pour le classement de recherche. Si vous enchaînez plusieurs étendues, PgSearch générera une requête de classement pour chaque étendue, les requêtes de classement doivent donc avoir des noms uniques. Si vous devez référencer la requête de classement (par exemple dans une clause GROUP BY), vous pouvez régénérer le nom de la sous-requête avec la méthode PgScope::Configuration.alias
en passant le nom de la table interrogée.
shirt_brands = ShirtBrand . search_by_name ( "Penguin" )
. joins ( :shirt_sizes )
. group ( "shirt_brands.id, #{ PgSearch :: Configuration . alias ( 'shirt_brands' ) } .rank" )
PgSearch n'aurait pas été possible sans l'inspiration du texticle (maintenant renommé textacular). Merci à Aaron Patterson pour la version originale et à Casebook PBC (https://www.casebook.net) pour l'avoir offert à la communauté !
Veuillez lire notre guide CONTRIBUTION.
Nous avons également un groupe Google pour discuter de pg_search et d'autres projets open source Casebook PBC.
Copyright © 2010–2022 Recueil de cas PBC. Sous licence MIT, voir le fichier LICENSE.