Tu as déjà lancé une requête WHERE title LIKE '%chat%' sur une table avec quelques millions de lignes ? Si oui, tu sais que l'expérience est… comment dire… lente. Et encore, le pire n'est pas la lenteur. C'est l'absence totale de pertinence : LIKE ne comprend ni la langue, ni les variantes d'un mot, ni les fautes de frappe. C'est là que la full-text search (ou recherche plein texte) entre en jeu.
La full-text search ne se contente pas de comparer des chaînes de caractères. Elle analyse le texte, le découpe en unités, le normalise, puis construit un index inversé qui associe chaque mot à la liste des documents qui le contiennent. Résultat : la recherche devient rapide, même sur des millions d'articles, et elle peut classer les résultats par pertinence.
Prenons un exemple. Avec SQL classique, pour trouver les articles parlant de chats, tu écris :
Ceci scanne toute la table et ne trouve que les occurrences exactes. À côté, voici ce que PostgreSQL full-text propose :
La différence ne saute pas aux yeux ? Détrompe-toi. to_tsvector transforme le texte en une liste de lexèmes normalisés. to_tsquery fait de même avec ta requête. Le moteur peut alors utiliser un index GIN pour retrouver les documents en un éclair. Et comme il travaille sur des racines de mots, il gère automatiquement les pluriels, les conjugaisons, et même les accords.
Ce qu'il faut retenir : la full-text search n'est pas une option. C'est une brique indispensable dès que tu construis une app où les utilisateurs cherchent du texte. Que ce soit un blog, un e-commerce ou un SaaS.
Dans ce tutoriel, je vais te montrer concrètement comment passer de LIKE à une vraie recherche plein texte. On commencera par les concepts qui se cachent derrière : l'index inversé, l'analyseur lexical, la tokenisation, le stemming et la lemmatisation. On n'aura pas peur des mots, promis.
Ensuite, on passera à la pratique avec PostgreSQL : on configurera les analyseurs français, on testera la pertinence des résultats, et je te donnerai mes astuces pour éviter les pièges classiques.
Enfin, on prendra du recul. Parce que PostgreSQL full-text, c'est déjà très bien. Mais quand ton produit grandit, tu auras peut-être besoin d'un moteur dédié comme Elasticsearch ou Meilisearch. Je te dirai quand ça vaut le coup de franchir le pas — et quand il vaut mieux rester sur ta base relationnelle.
La full-text search, c'est comme la différence entre chercher une aiguille dans une botte de foin avec une loupe (LIKE) et avoir un aimant géant (l'index inversé). Les deux trouvent l'aiguille, mais un seul le fait en un temps raisonnable.
Allez, on attaque.
Qu'est-ce que le full-text search ?
... Les concepts internes : index inversé et analyse du langage
Un index inversé, c'est le truc qui transforme une recherche de plusieurs secondes en moins de 10 millisecondes. Au lieu de parcourir chaque document ligne par ligne, tu construis une structure de données qui associe chaque mot à la liste des documents où il apparaît. C'est la pierre angulaire de tout moteur de recherche moderne, de Lucene à Elasticsearch en passant par PostgreSQL full-text.
Prenons un exemple concret. Tu as trois documents : « Le chat dort », « Le chien dort », « Le chat chasse la souris ». L'index inversé ressemble à ça :
Quand tu cherches « chat », le moteur va directement dans l'entrée chat et récupère doc1 et doc3. Pas besoin de scanner les trois documents. Plus tu as de documents, plus l'économie est énorme. C'est pour ça que la full-text search passe à l'échelle là où LIKE '%chat%' s'effondre.
Mais attention, le texte brut ne se laisse pas découper aussi facilement. C'est là que l'analyseur lexical entre en scène. Il découpe le texte en unités appelées tokens, en ignorant la ponctuation et la casse. "Chat !" devient le token chat. La tokenisation gère aussi les cas particuliers : les URL, les emails, les nombres. Sans elle, ton index serait pollué par des variantes inutiles.
Ensuite vient le stemming ou la lemmatisation. Le stemming réduit les mots à leur racine brute : dormons → dorm, dort → dorm. C'est brutal mais efficace. La lemmatisation, plus intelligente, utilise un dictionnaire pour ramener les mots à leur forme canonique : dormons → dormir, dort → dormir. PostgreSQL avec la configuration french fait de la lemmatisation. Résultat : ta recherche « je dors » trouve aussi « nous dormons » et « ils dorment ».
Mon avis : si tu veux une recherche qui déchire, ne néglige jamais l'analyse. Un bon analyseur fait plus pour la pertinence que n'importe quel algorithme de scoring. Configure-le correctement pour la langue de tes utilisateurs, et tu gagneras des heures de débogage. Pense à l'index inversé comme à un aimant géant : plus il est propre et précis, plus il attire les bonnes réponses.
Implémenter le full-text search avec PostgreSQL
Le plus dur avec PostgreSQL full-text, c'est de comprendre que la requête n'est que la partie visible. Le vrai travail se joue en amont : préparer une colonne tsvector et l'indexer. Dès que c'est fait, la recherche devient un jeu d'enfant.
La colonne générée s'occupe de tout. À chaque insert ou update, PostgreSQL met à jour le tsvector. Plus besoin de faire le calcul toi-même. Et l'index GIN te permet de retrouver les documents en un éclair, sans scanner la table. C'est le minimum syndical.
Pour interroger cette colonne, j'utilise une syntaxe que je trouve finalement très lisible :
Le double arobase ( @@ ) est l'opérateur de correspondance. À gauche, le tsvector. À droite, le tsquery. Le & dans tsquery signifie ET. Le | signifie OU, le ! est la négation. Tu peux aussi chercher une phrase avec l'opérateur de distance :
Attention, je te déconseille de construire cette requête en concaténant des entrées utilisateur directement dans to_tsquery. Passe par plainto_tsquery si tu veux une recherche libre : elle échappe la ponctuation et transforme la saisie en opérateur ET. Sinon, tu vas t'exposer à des erreurs de syntaxe, voire pire.
Côté pertinence, ts_rank fait le boulot. Il trie les résultats en fonction de la fréquence des termes et de leur rareté dans le document. Pour avoir un détail visuel, ajoute ts_headline :
Mon conseil : commence avec ts_rank, pas ts_rank_cd. Ce dernier est plus précis mais il réserve des surprises quand ton texte est long. Teste les deux, et choisis ce qui correspond à ton besoin.
Le piège le plus classique, c'est l'analyseur de langue. Si ta colonne est en 'french', ton tsquery doit être en 'french' aussi. Mélange un to_tsvector('french', ...) avec un to_tsquery('english', ...), et tu obtiens des résultats vides ou incohérents. Pour les accents, PostgreSQL ne les ignore pas par défaut. Installe l'extension unaccent et combine-la avec un dictionnaire :
Franchement, la recherche plein texte PostgreSQL couvre 80% des besoins d'une application. C'est rapide à mettre en place, ça gère le français, la pertinence, et même le surlignage. Mais si tu as besoin de typo tolérante, de suggestions ou de facettes, là il faudra sortir le gros moteur. On en parle dans la suite.
...
... Conclusion
Tu ne vas pas revenir en arrière. Une fois que tu as vu un index inversé répondre en quelques millisecondes, le bon vieux LIKE '%chat%' finit à la poubelle.
PostgreSQL full-text va te surprendre par sa robustesse. L'exemple to_tsvector de l'introduction scanne un index GIN, pas la table. Avec un analyseur français bien configuré et ts_rank pour classer, tu tiens des millions d'articles sans problème.
Mais ne te précipite pas sur Elasticsearch ou Meilisearch. Seuls des besoins avancés la justifient : fautes de frappe, facettes, recherche multi-langues à chaud. J'ai migré un projet quand PostgreSQL a commencé à ramer sur la pertinence, pas sur la vitesse.
Mon conseil : commence par PostgreSQL. Comprends la tokenisation, le stemming, la lemmatisation et l'analyseur lexical sur postgres. Ces concepts te suivront partout, même si tu bascules sur Apache Lucene.
La recherche plein texte n'est pas une option à cocher. C'est la promesse que tu fais à tes utilisateurs : "tu cherches, je trouve". Fais-la tenir.