DanLevy.net

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.

Carte des outils de recherche PostgresUne comparaison de pg_trgm, full-text search, pgvector et hybrid search par forme d’entrée et intention de requête.Choisissez le primitif de recherche par forme d’entréeLa même table Postgres peut supporter les quatre. L’astuce est d’associer la requête au texte.Les mots exacts comptentLe sens compteTexte court / structuréProse longue / blocsfloupg_trgmNoms, adresses, titres, fautes,autocomplétion, chaînes partielles.Similarité orthographique : distance d’écriture.similairepgvectorÉléments liés, tickets dupliqués,recommandations depuis de courtes descriptions.Similarité d’embedding : distance de sens.lexicalRecherche plein texteArticles, docs, logs, contenu supportoù les mots de la requête doivent apparaître.Lexèmes, racinisation, classement, filtres booléens.hybrideFTS + pgvectorDocs techniques et RAG où les utilisateurs posentdes questions conceptuelles plus des symboles exacts.Exécutez les deux, fusionnez les rangs avec RRF.Partez de l’intention de requête, puis vérifiez la forme du texte
Les quatre primitifs de recherche Postgres cartographiés par intention de requête (exacte vs. sémantique) et forme du texte (structuré vs. prose). La même table peut porter les quatre index — le choix se fait par requête, pas par table.

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 posts
WHERE 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 automatiquement
ALTER 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ête
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 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


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 :

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 score
FROM users
WHERE name % $1 -- opérateur % = seuil de similarité (0.3 par défaut)
ORDER BY score DESC
LIMIT 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 users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 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 score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 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énarioUtiliser
Recherche de nom personne/entreprise avec fautespg_trgm
Autocomplétion / recherche par préfixepg_trgm (ou FTS avec requêtes préfixe)
Chaînes courtes, identifiants, codespg_trgm
Articles en prose, documentation, ticketsFTS
Messages de log pour mots-clésFTS
Recherche de noms multilinguespg_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 corps
SELECT id, title,
ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank,
similarity(title, $1) AS trgm_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 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_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR 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_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR 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, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 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 floue
CREATE 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 rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 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 recherchezRecommandé
Articles en prose, docs, ticketsFTS
Noms personne/entreprise avec fautespg_trgm
Autocomplétion, recherche par préfixepg_trgm
Codes courts, identifiantspg_trgm
Messages de log pour mots-clésFTS
Noms internationauxpg_trgm + unaccent
Contenu volumineux, meilleur classementpg_search (ParadeDB BM25)
Clés primaires, e-mails exacts, identifiantsIndex B-tree
Dates, plages, listes triéesIndex B-tree
Permissions, catégories, filtresClause WHERE classique
Questions, paraphrases, conceptspgvector (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 avec Reciprocal Rank FusionUne requête se répartit sur la recherche plein texte et la recherche vectorielle, chacune produit des rangs, et Reciprocal Rank Fusion les combine en une seule liste de résultats.La recherche hybride, c’est deux signaux honnêtes, puis un seul classement fusionnéNe faites pas la moyenne des scores bruts. Le rang FTS et la distance cosinus sont des devises différentes.Requête utilisateur”withRetrytimeout errors”FTS / BM25Symboles et mots exacts1. Référence API2. Guide RetrypgvectorVoisins conceptuels1. Échecs réseau2. Guide RetryFusion RRFDonnez à chaque résultat un crédit poursa position dans chaque liste.1 / (60 + rank)Résultats finauxLe premier résultat est là où les termesexacts et le sens sémantique sont d’accord.
Une requête se répartit sur FTS et pgvector en parallèle. Chacun produit sa propre liste classée. RRF score chaque document par sa position dans chaque liste et somme les scores — le résultat fait émerger les documents sur lesquels les deux signaux sont d’accord.
-- Recherche hybride : FTS + pgvector fusionnés avec RRF
WITH 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_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 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.