La recherche plein texte (ou plus simplement la recherche de
texte) permet de sélectionner des
documents en langage naturel qui satisfont une
requête et, en option, de les trier par intérêt
suivant cette requête. Le type le plus fréquent de recherche concerne la
récupération de tous les documents contenant les termes de
recherche indiqués et de les renvoyer dans un ordre dépendant
de leur similarité par rapport à la requête. Les
notions de
requête
et de similarité
peuvent
beaucoup varier et dépendent de l'application réelle. La recherche
la plus simple considère une requête
comme un
ensemble de mots et la similarité
comme la fréquence
des mots de la requête dans le document.
Les opérateurs de recherche plein texte existent depuis longtemps dans
les bases de données. PostgreSQL dispose des
opérateurs ~
, ~*
,
LIKE
et ILIKE
pour les types de
données texte, mais il lui manque un grand nombre de propriétés essentielles
requises par les systèmes d'information modernes :
Aucun support linguistique, même pour l'anglais. Les expressions
rationnelles ne sont pas suffisantes, car elles ne peuvent pas gérer
facilement les mots dérivés, par exemple satisfait
et
satisfaire
. Vous pouvez laisser passer des documents
qui contiennent satisfait
bien que vous souhaitiez
quand même les trouver avec une recherche sur
satisfaire
.
Il est possible d'utiliser OR
pour rechercher plusieurs
formes dérivées, mais cela devient complexe et augmente le risque d'erreur
(certains mots peuvent avoir des centaines de variantes).
Ils ne fournissent aucun classement (score) des résultats de la recherche, ce qui les rend inefficaces quand des centaines de documents correspondants sont trouvés.
Ils ont tendance à être lents, car les index sont peu supportés, donc ils doivent traiter tous les documents à chaque recherche.
L'indexage pour la recherche plein texte permet au document d'être prétraité et qu'un index de ce prétraitement soit sauvegardé pour une recherche ultérieure plus rapide. Le prétraitement inclut :
Analyse des documents en jetons. Il est utile d'identifier les différentes classes de jetons, c'est-à-dire nombres, mots, mots complexes, adresses email, pour qu'ils puissent être traités différemment. En principe, les classes de jetons dépendent de l'application, mais, dans la plupart des cas, utiliser un ensemble prédéfini de classes est adéquat. PostgreSQL utilise un analyseur pour réaliser cette étape. Un analyseur standard est fourni, mais des analyseurs personnalisés peuvent être écrits pour des besoins spécifiques.
Conversion des jetons en lexèmes.
Un lexème est une chaîne, identique à un jeton, mais elle a été
normalisée pour que différentes formes du même mot
soient découvertes. Par exemple, la normalisation inclut pratiquement toujours
le remplacement des majuscules par des minuscules, ainsi que la suppression
des suffixes (comme s
ou es
en anglais).
Ceci permet aux recherches de trouver les variantes du même mot, sans avoir
besoin de saisir toutes les variantes possibles. De plus, cette étape
élimine typiquement les termes courants, qui sont
des mots si courants qu'il est inutile de les rechercher. Donc, les
jetons sont des fragments bruts du document alors que les lexèmes sont des
mots supposés utiles pour l'indexage et la recherche.
PostgreSQL utilise des
dictionnaires pour réaliser cette étape.
Différents dictionnaires standards sont fournis et des dictionnaires
personnalisés peuvent être créés pour des besoins spécifiques.
Stockage des documents prétraités pour optimiser la recherche . Chaque document peut être représenté comme un tableau trié de lexèmes normalisés. Avec ces lexèmes, il est souvent souhaitable de stocker des informations de position à utiliser pour obtenir un score de proximité, pour qu'un document qui contient une région plus « dense » des mots de la requête se voit affecter un score plus important qu'un document qui en a moins.
Les dictionnaires autorisent un contrôle fin de la normalisation des jetons. Avec des dictionnaires appropriés, vous pouvez :
Définir les termes courants qui ne doivent pas être indexés.
Établir une liste des synonymes pour un simple mot en utilisant Ispell.
Établir une correspondance entre des phrases et un simple mot en utilisant un thésaurus.
Établir une correspondance entre différentes variations d'un mot et une forme canonique en utilisant un dictionnaire Ispell.
Établir une correspondance entre différentes variations d'un mot et une forme canonique en utilisant les règles du « stemmer » Snowball.
Un type de données tsvector
est fourni pour stocker les documents
prétraités, avec un type tsquery
pour représenter les requêtes
traitées (Section 8.11). Il existe beaucoup de
fonctions et d'opérateurs disponibles pour ces types de données
(Section 9.13), le plus important étant l'opérateur
de correspondance @@
, dont nous parlons dans la
Section 12.1.2. Les recherches plein texte peuvent
être accélérées en utilisant des index (Section 12.9).
Un document est l'unité de recherche dans un système de recherche plein texte, par exemple un article de magazine ou un message email. Le moteur de recherche plein texte doit être capable d'analyser des documents et de stocker les associations de lexèmes (mots-clés) avec les documents parents. Ensuite, ces associations seront utilisées pour rechercher les documents contenant des mots de la requête.
Pour les recherches dans PostgreSQL, un document est habituellement un champ texte à l'intérieur d'une ligne d'une table de la base ou une combinaison (concaténation) de champs, parfois stockés dans différentes tables ou obtenus dynamiquement. En d'autres termes, un document peut être construit à partir de différentes parties pour l'indexage et il peut ne pas être stocké quelque part. Par exemple :
SELECT titre || ' ' || auteur || ' ' || resume || ' ' || corps AS document FROM messages WHERE mid = 12; SELECT m.titre || ' ' || m.auteur || ' ' || m.resume || ' ' || d.corps AS document FROM messages m, docs d WHERE m.mid = did AND mid = 12;
En fait, dans ces exemples de requêtes, coalesce
devrait être utilisée pour empêcher un résultat NULL
pour
le document entier à cause d'une seule colonne NULL
.
Une autre possibilité est de stocker les documents dans de simples fichiers texte du système de fichiers. Dans ce cas, la base est utilisée pour stocker l'index de recherche plein texte et pour exécuter les recherches, et un identifiant unique est utilisé pour retrouver le document sur le système de fichiers. Néanmoins, retrouver les fichiers en dehors de la base demande les droits d'un super-utilisateur ou le support de fonctions spéciales, donc c'est habituellement moins facile que de conserver les données dans PostgreSQL. De plus, tout conserver dans la base permet un accès simple aux métadonnées du document pour aider l'indexage et l'affichage.
Dans le but de la recherche plein texte, chaque document doit être réduit
au format de prétraitement, tsvector
. La recherche et le
calcul du score sont réalisés entièrement à partir de la représentation
tsvector
d'un document -- le texte original n'a besoin d'être retrouvé que
lorsque le document a été sélectionné pour être montré à l'utilisateur.
Nous utilisons souvent tsvector
pour le document, mais, bien
sûr, il ne s'agit que d'une représentation compacte du document complet.
La recherche plein texte dans PostgreSQL est
basée sur l'opérateur de correspondance @@
, qui renvoie
true
si un tsvector
(document) correspond
à un tsquery
(requête). Peu importe le type de données indiqué
en premier :
SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery; ?column? ---------- t SELECT 'fat & cow'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector; ?column? ---------- f
Comme le suggère l'exemple ci-dessus, un tsquery
n'est pas un
simple texte brut, pas plus qu'un tsvector
ne l'est. Un
tsquery
contient des termes de recherche qui doivent déjà être
des lexèmes normalisés, et peut combiner plusieurs termes en utilisant les
opérateurs AND, OR, NOT et FOLLOWED BY. (Pour les détails sur la syntaxe, voir la Section 8.11.2.) Les fonctions to_tsquery
,
plainto_tsquery
et phraseto_tsquery
sont utiles pour convertir un texte
écrit par un utilisateur dans un tsquery
correct, principalement
en normalisant les mots apparaissant dans le texte. De façon similaire,
to_tsvector
est utilisée pour analyser et normaliser un
document. Donc, en pratique, une correspondance de recherche ressemblerait
plutôt à ceci :
SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat'); ?column? ---------- t
Observez que cette correspondance ne réussit pas si elle est écrite ainsi :
SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat'); ?column? ---------- f
car ici, aucune normalisation du mot rats
n'interviendra.
Les éléments d'un tsvector
sont des lexèmes, qui sont
supposés déjà normalisés, donc rats
ne correspond pas à
rat
.
L'opérateur @@
supporte aussi une entrée de type
text
, permettant l'oubli de conversions explicites de text vers
tsvector
ou tsquery
dans les cas simples. Les
variantes disponibles sont :
tsvector @@ tsquery tsquery @@ tsvector text @@ tsquery text @@ text
Nous avons déjà vu les deux premières. La forme
text
@@
tsquery
est
équivalente à to_tsvector(x) @@ y
.
La forme text
@@
text
est équivalente à to_tsvector(x) @@ plainto_tsquery(y)
.
Dans un tsquery
, l'opérateur &
(AND)
spécifie que ses deux arguments doivent être présents dans le document pour
qu'il y ait correspondance. De même, l'opérateur |
(OR)
spécifie qu'au moins un de ses arguments doit être présent dans le
document, alors que l'opérateur !
(NOT) spécifie que son
argument ne doit pas être présent pour qu'il y ait une
correspondance. Par exemple, la requête fat & ! rat
correspond aux documents contenant fat
, mais pas
rat
.
Chercher des phrases est possible à l'aide de l'opérateur
<->
(FOLLOWED BY) tsquery
, qui
établit la correspondance seulement si tous ses arguments sont adjacents et
dans l'ordre indiqué. Par exemple :
SELECT to_tsvector('fatal error') @@ to_tsquery('fatal <-> error'); ?column? ---------- t SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error'); ?column? ---------- f
Il existe une version plus générale de l'opérateur FOLLOWED BY qui s'écrit
<
, où
N
>N
est un entier représentant la différence entre
les positions des lexèmes correspondants. L'opérateur
<1>
est identique à <->
,
tandis que l'opérateur <2>
n'établit la
correspondance que si exactement un lexème différent apparaît entre les
deux lexèmes en argument, et ainsi de suite. La fonction
phraseto_tsquery
exploite cet opérateur pour construire
un tsquery
permettant de reconnaître une phrase quand
certains des mots sont des termes courants. Par exemple :
SELECT phraseto_tsquery('cats ate rats'); phraseto_tsquery ------------------------------- 'cat' <-> 'ate' <-> 'rat' SELECT phraseto_tsquery('the cats ate the rats'); phraseto_tsquery ------------------------------- 'cat' <-> 'ate' <2> 'rat'
Un cas particulier potentiellement utile est <0>
qui peut être utilisé pour vérifier que deux motifs correspondent à un même
mot.
On peut utiliser des parenthèses pour contrôler l'imbrication des
opérateurs tsquery
. En l'absence de parenthèses, l'opérateur
|
a une priorité moindre que &
,
puis <->
, et finalement !
.
Il est important de noter que les opérateurs AND/OR/NOT ont une
signification légèrement différente quand ils sont les arguments d'un
opérateur FOLLOWED BY que quand ils ne le sont pas. La raison en est que,
dans un FOLLOWED BY, la position exacte de la correspondance a une
importance. Par exemple, habituellement, !x
ne fait une
correspondance qu'avec les documents qui ne contiennent pas
x
quelque part. Mais !x <-> y
correspond à y
s'il n'est pas immédiatement après un
x
; un occurrence de x
quelque
part dans le document n'empêche pas une correspondance. Un autre exemple
est que x & y
nécessite seulement que
x
et y
apparaissent quelque part dans
le document, mais (x & y) <-> z
nécessite que
x
et y
réalisent une correspondance
immédiatement avant un z
. De ce fait, cette requête se
comporte différemment de x <-> z & y <->
z
, qui correspondra à un document contenant deux séquences
séparées x z
et y z
. (Cette requête
spécifique est inutile quand elle est écrite ainsi, car x
et y
ne peuvent pas être exactement à la même
place ; mais avec des situations plus complexes comme les motifs de
correspondance avec préfixe, une requête de cette forme pourrait être
utile.)
Les exemples ci-dessus ne sont que des exemples simples de recherche plein
texte. Comme mentionné précédemment, la recherche plein texte permet de
faire beaucoup plus : ignorer l'indexation de certains mots (termes
courants), traiter les synonymes et utiliser une analyse sophistiquée,
c'est-à-dire une analyse basée sur plus qu'un espace blanc. Ces
fonctionnalités sont contrôlées par les configurations de
recherche plein texte. PostgreSQL
arrive avec des configurations prédéfinies pour de nombreux langages et
vous pouvez facilement créer vos propres configurations (la commande
\dF
de psql affiche toutes
les configurations disponibles).
Lors de l'installation, une configuration appropriée est sélectionnée et
default_text_search_config est configuré dans
postgresql.conf
pour qu'elle soit utilisée par défaut.
Si vous utilisez la même configuration de recherche plein texte pour le
cluster entier, vous pouvez utiliser la valeur de
postgresql.conf
. Pour utiliser différentes configurations
dans le cluster, mais avec la même configuration pour une base, utilisez
ALTER DATABASE ... SET
. Sinon, vous pouvez configurer
default_text_search_config
dans chaque session.
Chaque fonction de recherche plein texte qui dépend d'une configuration a
un argument regconfig
en option, pour que la configuration
utilisée puisse être précisée explicitement.
default_text_search_config
est seulement utilisé quand
cet argument est omis.
Pour rendre plus facile la construction de configurations de recherche plein texte, une configuration est construite à partir d'objets de la base de données. La recherche plein texte de PostgreSQL fournit quatre types d'objets relatifs à la configuration :
Les analyseurs de recherche plein texte cassent les documents en jetons et classifient chaque jeton (par exemple, un mot ou un nombre).
Les dictionnaires de recherche plein texte convertissent les jetons en une forme normalisée et rejettent les termes courants.
Les modèles de recherche plein texte fournissent les fonctions nécessaires aux dictionnaires. (Un dictionnaire spécifie uniquement un modèle et un ensemble de paramètres pour ce modèle.)
Les configurations de recherche plein texte sélectionnent un analyseur et un ensemble de dictionnaires à utiliser pour normaliser les jetons produits par l'analyseur.
Les analyseurs de recherche plein texte et les modèles sont construits à
partir de fonctions bas niveau écrites en C ; du coup, le
développement de nouveaux analyseurs ou modèles nécessite des connaissances
en langage C, et les droits super-utilisateur pour les installer dans une
base de données. (Il y a des exemples d'analyseurs et de modèles en addon
dans la partie contrib/
de la distribution
PostgreSQL.) Comme les dictionnaires et les
configurations utilisent des paramètres et se connectent aux analyseurs et
modèles, aucun droit spécial n'est nécessaire pour créer un nouveau
dictionnaire ou une nouvelle configuration. Les exemples personnalisés de création de
dictionnaires et de configurations seront présentés plus tard
dans ce chapitre.