Guide de recherche textuelle Postgres 2026
Les outils de recherche déjà dans votre base de données, et quand chacun justifie sa place.
La plupart des équipes n’utilisent qu’un seul outil de recherche Postgres. Les équipes qui connaissent les trois livrent une meilleure recherche avec moins de complexité — et évitent le détour coûteux vers un service de recherche dédié dont elles n’avaient pas encore besoin.
Ce guide couvre l’ensemble des options natives de Postgres : ce que chacune fait, quand elle est la bonne solution, et comment les combiner.
Les trois outils
La recherche plein texte (tsvector / index GIN) est lexicale. Elle découpe le texte en lexèmes, les racinise et compare les requêtes à l’index. « Running » et « runs » se réduisent au même lexème. « Dog » et « dogs » aussi. La fonction de classement (ts_rank) valorise les documents où les termes de la requête apparaissent fréquemment ou en position proéminente.
Les trigrammes (pg_trgm) découpent les chaînes en tranches de 3 caractères superposées et mesurent combien deux chaînes en partagent. « Dan » → " da", "dan", "an ". « Micheal » et « Michael » partagent la plupart de leurs trigrammes, donc la similarité est élevée. Cela rend pg_trgm excellent pour la recherche floue de noms, la tolérance aux fautes de frappe et l’autocomplétion — là où la recherche plein texte échoue.
Les index de correspondance exacte (B-tree, hash) gèrent les clés primaires, les adresses e-mail, les identifiants, les SKU et tout ce qui est binaire : ça correspond ou pas. Ce ne sont pas à proprement parler des outils de « recherche », mais ils appartiennent à cette conversation car le pire schéma consiste à utiliser une recherche floue ou sémantique pour des problèmes qui ont des réponses correctes.
Le choix ne porte pas sur la sophistication. Il s’agit d’adapter l’outil à la forme de la requête.
Quand la recherche plein texte l’emporte
Rechercher des mots-clés dans de la prose. Articles de blog, documentation, descriptions de produits, tickets de support, documents juridiques. La recherche plein texte a été conçue pour cette forme de contenu : une récupération indexée et classée sur du texte en langage naturel.
Requêtes utilisateur basées sur des mots-clés. Les utilisateurs tapent un terme de recherche, filtrent par tag ou naviguent par mot-clé. La recherche plein texte gère cette intention nativement sans aucune infrastructure d’embedding.
Résultats classés sans dépendances externes. Les index de recherche plein texte sont rapides, déterministes et ne nécessitent aucun appel API. Le signal de pertinence vient de la fréquence des termes pondérée par la position du champ.
Filtrage booléen en parallèle de la recherche. La recherche plein texte se compose naturellement avec votre logique de requête existante :
SELECT * FROM postsWHERE search_vector @@ to_tsquery('english', 'postgres & performance') AND category = 'tutorial' AND published_at > NOW() - INTERVAL '6 months';Configuration de la recherche plein texte
-- La colonne générée maintient l'index à jour automatiquementALTER TABLE posts ADD COLUMN search_vector tsvector GENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(body, '')), 'B') ) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- RequêteSELECT title, ts_rank(search_vector, query) AS rankFROM posts, to_tsquery('english', 'postgres & performance') queryWHERE search_vector @@ queryORDER BY rank DESCLIMIT 10;setweight assigne l’importance : A (titre) prime sur B (corps). C’est l’ensemble du modèle de pertinence pour la plupart des cas d’usage de recherche de contenu.
Ce que la recherche plein texte ne gère pas bien
- Fautes de frappe dans les requêtes — « javascipt » ne correspondra pas à « javascript »
- Noms de personnes, adresses, noms propres qui ne se racinisent pas de manière prévisible
- Autocomplétion / recherche par préfixe sans configuration spéciale
- Requêtes où l’utilisateur décrit un concept plutôt que de le nommer
Quand les trigrammes l’emportent (pg_trgm)
pg_trgm couvre le milieu inconfortable que la recherche plein texte rate systématiquement.
La recherche plein texte découpe le texte en lexèmes et les racinise. Pour la prose, c’est correct. Pour les noms et les identifiants courts, ce l’est souvent pas :
- Noms de personnes (« Dan Levy » → racinisé différemment selon le dictionnaire et la configuration de langue)
- Noms d’entreprises, adresses, titres de produits où l’orthographe exacte compte
- Requêtes avec des fautes de frappe — « Micheal Jordan », « Amaon », « javascipt »
- Autocomplétion / recherche par préfixe
- Correspondance de chaînes partielles (« son » correspondant à « Johnson », « Anderson »)
pg_trgm est également indépendant de la langue, ce qui compte pour les noms issus de contextes linguistiques divers. La recherche plein texte nécessite une configuration de dictionnaire par langue.
Recherche floue de noms
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- Trouve « Micheal Jordan » en cherchant « Michael Jordan »SELECT id, name, similarity(name, $1) AS scoreFROM usersWHERE name % $1 -- opérateur % = seuil de similarité (0.3 par défaut)ORDER BY score DESCLIMIT 10;L’opérateur % utilise pg_trgm.similarity_threshold (0,3 par défaut, plage 0–1). Pour la recherche de noms, 0,3–0,4 attrape les fautes tout en gardant le bruit bas.
Autocomplétion, recherche par préfixe et recherche par contenu
-- Correspondance par préfixe pour l'autocomplétion. Un index GIN trigramme peut aider,-- mais un index pattern B-tree peut être meilleur pour les préfixes purs ancrés à gauche.SELECT name FROM usersWHERE name ILIKE $1 || '%'ORDER BY nameLIMIT 10;
-- word_similarity pour les correspondances partielles dans des chaînes plus longues-- (« Johnson » dans « Andrew Johnson III »)SELECT id, name, word_similarity($1, name) AS scoreFROM usersWHERE $1 <% nameORDER BY score DESCLIMIT 10;L’index GIN trigramme est particulièrement utile pour les requêtes de contenu ILIKE '%pattern%' — des patterns qui sont habituellement des scans complets de table sans un index trigramme.
Quand utiliser pg_trgm plutôt que la recherche plein texte
| Scénario | Utiliser |
|---|---|
| Recherche de nom personne/entreprise avec fautes | pg_trgm |
| Autocomplétion / recherche par préfixe | pg_trgm (ou FTS avec requêtes préfixe) |
| Chaînes courtes, identifiants, codes | pg_trgm |
| Articles en prose, documentation, tickets | FTS |
| Messages de log pour mots-clés | FTS |
| Recherche de noms multilingues | pg_trgm (indépendant de la langue) |
Quand la correspondance exacte SQL l’emporte
Certains problèmes de « recherche » ne sont pas de la recherche du tout.
« Trouver l’utilisateur avec l’e-mail dan@example.com » est un test d’égalité. « Trouver la commande ORD-12345 » est une recherche de clé primaire. « Lister les posts dans la catégorie tutorial triés par date » est une requête filtrée. Ceux-ci appartiennent aux index B-tree ou hash.
Utiliser la recherche plein texte ou les trigrammes ici ajoute de la complexité sans améliorer la justesse — et pour les identifiants exacts, une correspondance approximative est pire qu’aucune correspondance.
CREATE INDEX users_email_idx ON users (email);
-- Recherche exacte : rapide et sans ambiguïtéSELECT id, name FROM users WHERE email = $1;La leçon plus large : la recherche approximative pour des problèmes qui ont des réponses correctes est une erreur de catégorie. Elle retourne quelque chose — qui peut être confiant mais faux.
Combiner ces outils
Ces outils se composent proprement. Vous n’en choisissez pas un seul.
FTS + pg_trgm pour une barre de recherche qui tolère les fautes dans les mots-clés :
-- La similarité trigramme sur le titre attrape les fautes ; ts_rank gère la pertinence du corpsSELECT id, title, ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank, similarity(title, $1) AS trgm_scoreFROM postsWHERE search_vector @@ to_tsquery('simple', $1) OR title % $1ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESCLIMIT 10;FTS + unaccent pour le contenu international :
-- Supprime les marques diacritiques pour que « José » corresponde à « Jose »CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refreshBEFORE INSERT OR UPDATE OF title, body ON postsFOR EACH ROW EXECUTE FUNCTION tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);unaccent + pg_trgm pour la recherche de noms internationaux :
ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()RETURNS trigger LANGUAGE plpgsql AS $$BEGIN NEW.name_search := unaccent(coalesce(NEW.name, '')); RETURN NEW;END;$$;
CREATE TRIGGER users_name_search_refreshBEFORE INSERT OR UPDATE OF name ON usersFOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx ON users USING GIN (name_search gin_trgm_ops);
SELECT id, nameFROM usersWHERE name_search % unaccent($1)ORDER BY similarity(name_search, unaccent($1)) DESCLIMIT 10;Les exemples de triggers évitent d’utiliser unaccent() dans des expressions de colonnes générées ou d’index, où les règles d’immuabilité de PostgreSQL comptent. Si vous enveloppez unaccent() dans votre propre fonction immuable, documentez que vous acceptez un risque de mise à niveau / configuration.
Extensions notables
pg_trgm est fourni avec la plupart des distributions Postgres mais nécessite une activation explicite. Le fondement de la correspondance floue de chaînes dans Postgres.
unaccent supprime les marques diacritiques avant l’indexation et la requête. Se marie bien avec pg_trgm et la recherche plein texte pour le contenu en langues européennes. Fourni avec Postgres.
pg_bigm étend l’approche trigramme aux bigrammes (tranches de 2 caractères), ce qui améliore significativement les résultats pour les langues CJK (chinois, japonais, coréen) où pg_trgm sous-performe. Doit être installé séparément ; non fourni.
pg_search (de ParadeDB) remplace la pile standard GIN / tsvector par un index BM25 basé sur Tantivy. Cela vous donne le scoring BM25 (souvent meilleur que ts_rank), la correspondance floue dans les requêtes FTS, la recherche facetée, et une indexation considérablement plus rapide sur les grandes tables. C’est un chemin de mise à niveau directe quand la recherche plein texte standard commence à montrer des limites de classement ou de performance.
-- pg_search : recherche plein texte BM25 avec correspondance floueCREATE INDEX posts_bm25_idx ON posts USING bm25 (id, title, body) WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- Requête avec scoring BM25 + correspondance floue (attrape « javascipt »)SELECT id, title, paradedb.score(id) AS rankFROM postsWHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')ORDER BY rank DESCLIMIT 10;pgvector ajoute le stockage de vecteurs denses et la recherche par similarité. C’est le bon outil quand les utilisateurs décrivent ce qu’ils veulent plutôt que de le nommer — recherche sémantique, RAG, recommandations de contenu lié, requêtes multilingues. Couvert en profondeur dans Recherche vectorielle sémantique et stratégies hybrides.
Tableau de décision
| Ce que vous recherchez | Recommandé |
|---|---|
| Articles en prose, docs, tickets | FTS |
| Noms personne/entreprise avec fautes | pg_trgm |
| Autocomplétion, recherche par préfixe | pg_trgm |
| Codes courts, identifiants | pg_trgm |
| Messages de log pour mots-clés | FTS |
| Noms internationaux | pg_trgm + unaccent |
| Contenu volumineux, meilleur classement | pg_search (ParadeDB BM25) |
| Clés primaires, e-mails exacts, identifiants | Index B-tree |
| Dates, plages, listes triées | Index B-tree |
| Permissions, catégories, filtres | Clause WHERE classique |
| Questions, paraphrases, concepts | pgvector (voir article suivant) |
En cas de doute : chaînes courtes avec variation d’orthographe → trigrammes. Prose longue pour des requêtes par mots-clés → FTS. Identifiants structurés → index classiques. Requêtes conceptuelles ou en langage naturel → pgvector.
Recherche hybride : deux signaux, un seul classement
Quand une requête comme "withRetry timeout errors" arrive dans une barre de recherche, elle porte deux types d’intention : des noms de symboles exacts que l’utilisateur connaît (withRetry) et une description conceptuelle (timeout errors). Aucun primitif unique ne couvre les deux. Exécuter la recherche plein texte et la recherche vectorielle en parallèle — puis fusionner leurs listes classées avec Reciprocal Rank Fusion — le fait.
RRF score chaque résultat comme 1 / (60 + rank) dans chaque liste et somme à travers les listes. La constante 60 amortit l’avantage des premiers rangs, de sorte qu’un résultat qui se place deuxième dans les deux listes peut battre un résultat qui gagne une liste et rate l’autre entièrement. De manière cruciale, RRF n’additionne jamais les scores bruts entre méthodes — le rang FTS et la distance cosinus sont des devises différentes et ne peuvent pas être combinés arithmétiquement.
-- Recherche hybride : FTS + pgvector fusionnés avec RRFWITH fts AS ( SELECT id, ts_rank(search_vector, query) AS score, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank FROM docs, to_tsquery('english', 'withRetry & timeout') query WHERE search_vector @@ query LIMIT 60),vec AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank FROM docs ORDER BY embedding <=> $embedding LIMIT 60)SELECT COALESCE(fts.id, vec.id) AS id, (COALESCE(1.0 / (60 + fts.rank), 0) + COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_scoreFROM fts FULL JOIN vec ON fts.id = vec.idORDER BY rrf_score DESCLIMIT 10;Le pool de 60 documents candidats par branche (LIMIT 60) est un point de départ courant. Élargissez-le si le rappel est faible ; réduisez-le pour la vitesse.
Et ensuite
La recherche textuelle Postgres couvre beaucoup de terrain, mais elle a un plafond. Quand les utilisateurs décrivent ce qu’ils veulent au lieu de le nommer — « quelque chose pour m’aider à dormir en avion », « des articles sur la confiance en débogage en tant qu’ingénieur junior » — la recherche lexicale et trigramme échouent toutes les deux.
C’est le territoire des embeddings vectoriels, de la recherche sémantique et des architectures hybrides. Couvert dans Recherche vectorielle sémantique et stratégies hybrides.