PostgreSQLLa base de données la plus sophistiquée au monde.

12.3. ContrĂŽler la recherche plein texte

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.

12.3.1. Analyser des documents

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.)

12.3.2. Analyser des requĂȘtes

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.

12.3.3. Ajouter un score aux résultats d'une recherche

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.

12.3.4. Surligner les résultats

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;