Recherche plein texte avec Postgres

Découvrir le FTS avec Postgres

Le champ de recherche unique, voilà le graal du formulaire de recherche d'une application.

Pour l'utilisateur, c'est avoir la recherche accessible à toutes les pages, s'épargner le temps de comprendre dans quel champs rentrer un terme recherché, c'est passer de la recherche d'un simple mot à la requête complexe sans changer d'interface.

Pour le développeur, c'est la simplification de l'évolution de l'interface à mesure que les données de l'application s'enrichissent et tout un tas de fonctionnalités avancées : surlignage des résultats, calcul de pertinence, performances, etc.

Pour le DBA, c'est éviter d'avoir pleins de requêtes inutiles parce que l'utilisateur s'est trompé de champs.

Quelques généralités

Postgres est livré avec une recherche plein texte performante et bien conçue. Dans cet article, nous allons tester le FTS Postgres sans même toucher au schéma !

La recherche plein texte n'est pas une technique d'indexation. C'est une technique de normalisation de texte en vue d'une recherche pertinente et efficace. Un texte normalisé est très facile à indexer, nous verrons cela plus tard.

La normalisation

Avant de jouer, assurons-nous d'avoir un Postgres récent, et quelques données:

$ docker run --detach --rm --publish 5432:5432 postgres:10-alpine
$ psql -h localhost -U postgres
psql (10.1)
Saisissez « help » pour l'aide.

postgres=# CREATE TABLE textes AS VALUES
('Chuck Norris a gagné la guerre du Golf, en 18 trous.'),
('Google, c''est le seul endroit où tu peux taper Chuck Norris...'),
('Chuck Norris mange ses oranges tout rond: Chuck Norris fait pas de quartier.');
SELECT 3
postgres=#

La table n'a aucun index ni aucune notion de FTS et pourtant nous allons faire de la recherche plein texte dessus, sans la modifier.

Dans Postgres, un texte normalisé est un tsvector. la fonction to_tsvector(configuration, texte) normalise un texte. Le paramètre configuration est important, il détermine comment Postgres doit analyser le texte. Postgres dispose par défaut d'une configuration rudimentaire pour le français : french. Nous verrons plus tard comment faire notre propre configuration. Voyons déjà ce que ça donne !

postgres=# SELECT to_tsvector('french', 'Chuck Norris a gagné la guerre du Golf, en 18 trous.');
                               to_tsvector
-------------------------------------------------------------------------
 '18':10 'a':3 'chuck':1 'gagn':4 'golf':8 'guerr':6 'norr':2 'trous':11
(1 ligne)

postgres=#

Un vecteur est une liste de mots plus ou moins tronqués à leur racine, associés à un nombre. Le processus pour passer d'un mot à sa racine est appelé lemmatisation. Ainsi gagné devient gagn. Le nombre indique la place du mot dans le texte, cela permettra de connaître la distance entre les mots par exemple. Les mots vides la, du et en ont disparus. Malheureusement, trous n'a pas perdu son s, c'est une erreur de lemmatisation.

Comparons avec la configuration par défaut :

postgres=# select to_tsvector('Chuck Norris a gagné la guerre du Golf, en 18 trous.');
                                       to_tsvector
------------------------------------------------------------------------------------------
 '18':10 'chuck':1 'du':7 'en':9 'gagné':4 'golf':8 'guerr':6 'la':5 'norri':2 'trous':11
(1 ligne)

Le résultat est très différent. Les mots vides sont toujours là. gagné n'est pas lemmatisé. La configuration est donc importante et doit faire l'objet d'un soin particulier.

La recherche

Regardons maintenant la seconde fonction importante pour le FTS. plainto_tsquery(configuration, text) normalise une requête FTS au format tsquery.

postgres=# select plainto_tsquery('french', 'Chuck NORRIS gagne');
      plainto_tsquery
---------------------------
 'chuck' & 'norr' & 'gagn'
(1 ligne)

postgres=#

On retrouve des mots lemmatisés, avec des contraintes booléennes. Cette recherche signifie contient les trois mots chuck, norris et gagne. La recherche est appliquée avec l'opérateur @@:

postgres=# SELECT column1 FROM textes WHERE plainto_tsquery('french', 'Chuck NORRIS gagner') @@ to_tsvector('french', column1);
                       column1
------------------------------------------------------
 Chuck Norris a gagné la guerre du Golf, en 18 trous.
(1 ligne)

postgres=#

Bingo ! Notez que la requête contient gagner à l'infinitif mais Postgres trouve la correspondance avec le mot gagné. Bravo, vous avez fait de la recherche plein texte !

Chuck Norris levant son pouce.

Côté performance, le code to_tsvector('french', column1) provoque la normalisation de toute la colonne. C'est une très mauvaise idée de normaliser toute la table à chaque recherche ! C'est à l'écriture dans la base qu'il faut normaliser le texte à chercher. Nous verrons les performances dans un prochain article !

En conclusion, la recherche plein texte de Postgres est simple. Un développeur peut prototyper une recherche très rapidement ! N'hésitez pas à commenter pour orienter le sujet du prochain article !

Références