Pour implémenter la recherche plein texte, une fonction doit permettre la création d'un tsvector à partir d'un document et la création d'un tsquery à partir de la requête d'un utilisateur. De plus, nous avons besoin de renvoyer les résultats dans un ordre utile, donc nous avons besoin d'une fonction de comparaison des documents suivant leur adéquation à la recherche. Il est aussi important de pouvoir afficher joliment les résultats. PostgreSQL™ fournit un support pour toutes ces fonctions.
PostgreSQL™ fournit la fonction to_tsvector pour convertir un document vers le type de données tsvector.
to_tsvector([ config regconfig, ] document text) returns tsvector
to_tsvector analyse un document texte et le convertit en jetons, réduit les jetons en des lexemes et renvoie un tsvector qui liste les lexemes avec leur position dans le document. Ce dernier est traité suivant la configuration de recherche plein texte spécifiée ou celle par défaut. Voici un exemple simple :
SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats'); to_tsvector ----------------------------------------------------- 'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
Dans l'exemple ci-dessus, nous voyons que le tsvector résultant ne contient pas les mots a, on et it, le mot rats est devenu rat et le signe de ponctuation - a été ignoré.
En interne, la fonction to_tsvector appelle un analyseur qui casse le texte en jetons et affecte un type à chaque jeton. Pour chaque jeton, une liste de dictionnaires (Section 12.6, « Dictionnaires ») est consultée, liste pouvant varier suivant le type de jeton. Le premier dictionnaire qui reconnaît le jeton émet un ou plusieurs lexemes pour représenter le jeton. Par exemple, rats devient rat car un des dictionnaires sait que le mot rats est la forme pluriel de rat. Certains mots sont reconnus comme des termes courants (Section 12.6.1, « Termes courants »), ce qui fait qu'ils sont ignorés car ils surviennent trop fréquemment pour être utile dans une recherche. Dans notre exemple, il s'agissait de a, on et it. Si aucun dictionnaire de la liste ne reconnaît le jeton, il est aussi ignoré. Dans cet exemple, il s'agit du signe de ponctuation - car il n'existe aucun dictionnaire affecté à ce type de jeton (Space symbols), ce qui signifie que les jetons espace ne seront jamais indexés. Le choix de l'analyseur, des dictionnaires et des types de jetons à indexer est déterminé par la configuration de recherche plein texte sélectionné (Section 12.7, « Exemple de configuration »). Il est possible d'avoir plusieurs configurations pour la même base, et des configurations prédéfinies sont disponibles pour différentes langues. Dans notre exemple, nous avons utilisé la configuration par défaut, à savoir english pour l'anglais.
La fonction setweight peut être utilisé pour ajouter un label aux entrées d'un tsvector avec un poids donné. Ce poids consiste en une lettre : A, B, C ou D. Elle est utilisée typiquement pour marquer les entrées provenant de différentes parties d'un document, comme le titre et le corps. Plus tard, cette information peut être utilisée pour modifier le score des résultats.
Comme to_tsvector(NULL) renvoie NULL, il est recommandé d'utiliser coalesce quand un champ peut être NULL. Voici la méthode recommandée pour créer un tsvector à partir d'un document structuré :
UPDATE tt SET ti = setweight(to_tsvector(coalesce(title,'')), 'A') || setweight(to_tsvector(coalesce(keyword,'')), 'B') || setweight(to_tsvector(coalesce(abstract,'')), 'C') || setweight(to_tsvector(coalesce(body,'')), 'D');
Ici nous avons utilisé setweight pour ajouter un label au source de chaque lexeme dans le tsvector final, puis assemblé les valeurs tsvector en utilisant l'opérateur de concaténation des tsvector, ||. (La Section 12.4.1, « Manipuler des documents » donne des détails sur ces opérations.)
PostgreSQL™ fournit les fonctions to_tsquery et plainto_tsquery pour convertir une requête dans le type de données tsquery. to_tsquery offre un accès à d'autres fonctionnalités que plainto_tsquery mais est moins indulgent sur ses arguments.
to_tsquery([ config regconfig, ] querytext text) returns tsquery
to_tsquery crée une valeur tsquery à partir de querytext qui doit contenir un ensemble de jetons individuels séparés par les opérateurs booléens & (AND), | (OR) et ! (NOT). Ces opérateurs peuvent être groupés en utilisant des parenthèses. En d'autres termes, les arguments de to_tsquery doivent déjà suivre les règles générales pour un tsquery comme décrit dans la Section 8.11, « Types de recherche plein texte ». La différence est que, alors qu'un tsquery basique prend les jetons bruts, to_tsquery normalise chaque jeton en un lexeme en utilisant la configuration spécifiée ou par défaut, et annule tout jeton qui est un terme courant d'après la configuration. Par exemple :
SELECT to_tsquery('english', 'The & Fat & Rats'); to_tsquery --------------- 'fat' & 'rat'
Comme une entrée tsquery basique, des poids peuvent être attachés à chaque lexeme à restreindre pour établir une correspondance avec seulement des lexemes tsvector de ces poids. Par exemple :
SELECT to_tsquery('english', 'Fat | Rats:AB'); to_tsquery ------------------ 'fat' | 'rat':AB
De plus, * peut être attaché à un lexeme pour demander la correspondance d'un préfixe :
SELECT to_tsquery('supern:*A & star:A*B'); to_tsquery -------------------------- 'supern':*A & 'star':*AB
Un tel lexeme correspondra à tout mot dans un tsvector qui commence par la chaîne indiquée.
to_tsquery peut aussi accepter des phrases avec des guillemets simples. C'est utile quand la configuration inclut un dictionnaire thésaurus qui peut se déclencher sur de telles phrases. Dans l'exemple ci-dessous, un thésaurus contient la règle supernovae stars : sn :
SELECT to_tsquery('''supernovae stars'' & !crab'); to_tsquery --------------- 'sn' & !'crab'
sans guillemets, to_tsquery génère une erreur de syntaxe pour les jetons qui ne sont pas séparés par un opérateur AND ou OR.
plainto_tsquery([ config regconfig, ] querytext text) returns tsquery
plainto_tsquery transforme le texte non formaté querytext en tsquery. Le texte est analysé et normalisé un peu comme pour to_tsvector, ensuite l'opérateur booléen & (AND) est inséré entre les mots restants.
Exemple :
SELECT plainto_tsquery('english', 'The Fat Rats'); plainto_tsquery ----------------- 'fat' & 'rat'
Notez que plainto_tsquery ne peut pas reconnaître un opérateur booléen, des labels de poids en entrée ou des labels de correspondance de préfixe :
SELECT plainto_tsquery('english', 'The Fat & Rats:C'); plainto_tsquery --------------------- 'fat' & 'rat' & 'c'
Ici, tous les symboles de ponctuation ont été annulés car ce sont des symboles espace.
Les tentatives de score pour mesurer l'adéquation des documents se font par rapport à une certaine requête. Donc, quand il y a beaucoup de correspondances, les meilleurs doivent être montrés en premier. PostgreSQL™ fournit deux fonctions prédéfinies de score, prennant en compte l'information lexicale, la proximité et la structure ; en fait, elles considèrent le nombre de fois où les termes de la requête apparaissent dans le document, la proximité des termes de la recherche avec ceux de la requête et l'importance du passage du document où se trouvent les termes du document. Néanmoins, le concept d'adéquation pourrait demander plus d'informations pour calculer le score, par exemple la date et l'heure de modification du document. Les fonctions internes de calcul de score sont seulement des exemples. Vous pouvez écrire vos propres fonctions de score et/ou combiner leur résultats avec des facteurs supplémentaires pour remplir un besoin spécifique.
Les deux fonctions de score actuellement disponibles sont :
ts_rank([ weights float4[], ] vector tsvector,
query tsquery [, normalization integer ]) returns float4
Fonction de score standard.
ts_rank_cd([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4
Cette fonction calcule le score de la densité de couverture pour le vecteur du document et la requête donnés, comme décrit dans l'article de Clarke, Cormack et Tudhope, « Relevance Ranking for One to Three Term Queries », article paru dans le journal « Information Processing and Management » en 1999.
Cette fonction nécessite des informations de position. Du coup, elle ne fonctionne pas sur des valeurs tsvector « strippées » -- elle renvoie toujours zéro.
Pour ces deux fonctions, l'argument optionnel des poids offre la possibilité d'impacter certains mots plus ou moins suivant la façon dont ils sont marqués. Le tableau de poids indique à quel point chaque catégorie de mots est marquée. Dans l'ordre :
{poids-D, poids-C, poids-B, poids-A}
Si aucun poids n'est fourni, alors ces valeurs par défaut sont utilisées :
{0.1, 0.2, 0.4, 1.0}
Typiquement, les poids sont utilisés pour marquer les mots compris dans des aires spéciales du document, comme le titre ou le résumé initial, pour qu'ils puissent être traités avec plus ou moins d'importance que les mots dans le corps du document.
Comme un document plus long a plus de chance de contenir un terme de la requête, il est raisonnable de prendre en compte la taille du document, par exemple un document de cent mots contenant cinq fois un mot de la requête est probablement plus intéressant qu'un document de mille mots contenant lui-aussi cinq fois un mot de la requête. Les deux fonctions de score prennent une option normalization, de type integer, qui précise si la longueur du document doit impacter son score. L'option contrôle plusieurs comportements, donc il s'agit d'un masque de bits : vous pouvez spécifier un ou plusieurs comportements en utilisant | (par exemple, 2|4).
0 (valeur par défaut) ignore la longueur du document
1 divise le score par 1 + le logarithme de la longueur du document
2 divise le score par la longueur du document
4 divise le score par "mean harmonic distance between extents" (ceci est implémenté seulement par ts_rank_cd)
8 divise le score par le nombre de mots uniques dans le document
16 divise le score par 1 + le logarithme du nombre de mots uniques dans le document
32 divise le score par lui-même + 1
Si plus d'un bit de drapeau est indiqué, les transformations sont appliquées dans l'ordre indiqué.
Il est important de noter que les fonctions de score n'utilisent aucune information globale donc il est impossible de produire une normalisation de 1% ou 100%, comme c'est parfois demandé. L'option de normalisation 32 (score/(score+1)) peut s'appliquer pour échelonner tous les scores dans une échelle de zéro à un mais, bien sûr, c'est une petite modification cosmétique, donc l'ordre des résultats ne changera pas.
Voici un exemple qui sélectionne seulement les dix correspondances de meilleur score :
SELECT title, ts_rank_cd(textsearch, query) AS rank FROM apod, to_tsquery('neutrino|(dark & matter)') query WHERE query @@ textsearch ORDER BY rank DESC LIMIT 10; title | rank -----------------------------------------------+---------- Neutrinos in the Sun | 3.1 The Sudbury Neutrino Detector | 2.4 A MACHO View of Galactic Dark Matter | 2.01317 Hot Gas and Dark Matter | 1.91171 The Virgo Cluster: Hot Plasma and Dark Matter | 1.90953 Rafting for Solar Neutrinos | 1.9 NGC 4650A: Strange Galaxy and Dark Matter | 1.85774 Hot Gas and Dark Matter | 1.6123 Ice Fishing for Cosmic Neutrinos | 1.6 Weak Lensing Distorts the Universe | 0.818218
Voici le même exemple en utilisant un score normalisé :
SELECT title, ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank FROM apod, to_tsquery('neutrino|(dark & matter)') query WHERE query @@ textsearch ORDER BY rank DESC LIMIT 10; title | rank -----------------------------------------------+------------------- Neutrinos in the Sun | 0.756097569485493 The Sudbury Neutrino Detector | 0.705882361190954 A MACHO View of Galactic Dark Matter | 0.668123210574724 Hot Gas and Dark Matter | 0.65655958650282 The Virgo Cluster: Hot Plasma and Dark Matter | 0.656301290640973 Rafting for Solar Neutrinos | 0.655172410958162 NGC 4650A: Strange Galaxy and Dark Matter | 0.650072921219637 Hot Gas and Dark Matter | 0.617195790024749 Ice Fishing for Cosmic Neutrinos | 0.615384618911517 Weak Lensing Distorts the Universe | 0.450010798361481
Le calcul du score peut consommer beaucoup de ressources car il demande de consulter le tsvector de chaque document correspondant, ce qui est très consommateur en entrées/sorties et du coup lent. Malheureusement, c'est presque impossible à éviter car les requêtes intéressantes ont un grand nombre de correspondances.
Pour présenter les résultats d'une recherche, il est préférable d'afficher une partie de chaque document et en quoi cette partie concerne la requête. Habituellement, les moteurs de recherche affichent des fragments du document avec des marques pour les termes recherchés. PostgreSQL™ fournit une fonction ts_headline qui implémente cette fonctionnalité.
ts_headline([ config regconfig, ] document text, query tsquery [, options text ]) returns text
ts_headline accepte un document avec une requête et renvoie un résumé du document. Les termes de la requête sont surlignés dans les extractions. La configuration à utiliser pour analyser le document peut être précisée par config ; si config est omis, le paramètre default_text_search_config est utilisé.
Si une chaîne options est spécifiée, elle doit consister en une liste de une ou plusieurs paires option=valeur séparées par des virgules. Les options disponibles sont :
StartSel, StopSel : les chaînes qui permettent de délimiter les mots de la requête parmi le reste des mots. Vous devez mettre ces chaînes entre guillemets doubles si elles contiennent des espaces ou des virgules.
MaxWords, MinWords : ces nombres déterminent les limites minimum et maximum des résumés à afficher.
ShortWord : les mots de cette longueur et les mots plus petits seront supprimés au début et à la fin d'un résumé. La valeur par défaut est de trois pour éliminer les articles anglais communs.
HighlightAll : booléen ; si true, le document complet sera utilisé pour le surlignage, en ignorant les trois paramètres précédents.
MaxFragments : nombre maximum d'extraits ou de fragments de texte à afficher. La valeur par défaut, 0, sélectionne une méthode de génération d'extraits qui n'utilise pas les fragments. Une valeur positive et non nulle sélectionne la génération d'extraits basée sur les fragments. Cette méthode trouve les fragments de texte avec autant de mots de la requête que possible et restreint ces fragments autour des mots de la requête. Du coup, les mots de la requête se trouvent au milieu de chaque fragment et ont des mots de chaque côté. Chaque fragment sera au plus de MaxWords et les mots auront une longueur maximum de ShortWord. Si tous les mots de la requête ne sont pas trouvés dans le document, alors un seul fragment de MinWords sera affiché.
FragmentDelimiter : quand plus d'un fragment est affiché, alors les fragments seront séparés par ce délimiteur.
Toute option omise recevra une valeur par défaut :
StartSel=<b>, StopSel=</b>, MaxWords=35, MinWords=15, ShortWord=3, HighlightAll=FALSE, MaxFragments=0, FragmentDelimiter=" ... "
Par exemple :
SELECT ts_headline('english', 'The most common type of search is to find all documents containing given query terms and return them in order of their similarity to the query.', to_tsquery('query & similarity')); ts_headline ------------------------------------------------------------ containing given <b>query</b> terms and return them in order of their <b>similarity</b> to the <b>query</b>. SELECT ts_headline('english', 'The most common type of search is to find all documents containing given query terms and return them in order of their similarity to the query.', to_tsquery('query & similarity'), 'StartSel = <, StopSel = >'); ts_headline ------------------------------------------------------- containing given <query> terms and return them in order of their <similarity> to the <query>.
ts_headline utilise le document original, pas un résumé tsvector, donc elle peut être lente et doit être utilisée avec parcimonie et attention. Une erreur typique est d'appeler ts_headline pour chaque document correspondant quand seuls dix documents sont à afficher. Les sous-requêtes SQL peuvent aider ; voici un exemple :
SELECT id, ts_headline(body, q), rank FROM (SELECT id, body, q, ts_rank_cd(ti, q) AS rank FROM apod, to_tsquery('stars') q WHERE ti @@ q ORDER BY rank DESC LIMIT 10) AS foo;