La recherche en texte intégral PostgreSQL convient à de nombreuses applications. Utilisez une règle simple, une requête de départ et une checklist d'indexation pour savoir quand ajouter un moteur de recherche.

La plupart des gens ne demandent pas « recherche en texte intégral ». Ils veulent une boîte de recherche qui soit rapide et qui trouve ce qu'ils voulaient dès la première page. Si les résultats sont lents, bruités ou mal ordonnés, les utilisateurs ne se soucient pas que vous ayez utilisé PostgreSQL full-text search ou un moteur séparé. Ils perdent simplement confiance dans la recherche.
C'est une décision : garder la recherche dans Postgres, ou ajouter un moteur dédié. L'objectif n'est pas une pertinence parfaite. C'est une base solide, rapide à livrer, facile à faire fonctionner, et suffisante pour la manière dont votre app est réellement utilisée.
Pour beaucoup d'apps, PostgreSQL full-text search suffit longtemps. Si vous avez quelques champs texte (titre, description, notes), un classement de base et un ou deux filtres (statut, catégorie, tenant), Postgres peut s'en charger sans infrastructure supplémentaire. Moins de composants, des sauvegardes plus simples et moins d'incidents « pourquoi la recherche est en panne alors que l'app fonctionne ? ».
« Suffisant » signifie généralement atteindre trois cibles en même temps :
Un exemple concret : un dashboard SaaS où les utilisateurs recherchent des projets par nom et par notes. Si une requête comme « onboarding checklist » renvoie le bon projet dans le top 5, en moins d'une seconde, et que vous n'êtes pas sans cesse en train d'ajuster des analyseurs ou de relancer des réindexations, c'est « suffisant ». Quand vous ne pouvez plus atteindre ces cibles sans complexité accrue, la question « recherche intégrée vs moteur de recherche » devient pertinente.
Les équipes décrivent souvent la recherche en termes de fonctionnalités, pas de résultats. L'approche utile est de traduire chaque fonctionnalité en coût de construction, d'ajustement et de fiabilisation.
Les premières demandes ressemblent à : tolérance aux fautes, facettes et filtres, surlignage, classement « intelligent », et autocomplétion. Pour une première version, séparez les indispensables des agréables à avoir. Une boîte de recherche basique doit généralement trouver des éléments pertinents, gérer les formes courantes des mots (pluriel, temps), respecter des filtres simples et rester rapide à mesure que votre table grandit. C'est justement là que PostgreSQL full-text search s'intègre bien.
Postgres excelle quand votre contenu vit dans des champs texte normaux et que vous voulez la recherche proche de vos données : articles d'aide, articles de blog, tickets de support, docs internes, titres et descriptions de produits, ou notes sur des fiches clients. Ce sont surtout des problèmes « trouvez le bon enregistrement », pas « construire un produit de recherche ».
Les « agréables à avoir » sont ceux qui complexifient. La tolérance aux fautes et une autocomplétion riche poussent souvent vers des outils supplémentaires. Les facettes sont possibles dans Postgres, mais si vous voulez beaucoup de facettes, des analyses profondes et des comptes instantanés sur de très gros jeux, un moteur dédié devient attractif.
Le coût caché n'est rarement la licence. C'est le second système. Une fois que vous ajoutez un moteur, vous ajoutez synchronisation des données et backfills (et les bugs qui vont avec), monitoring et mises à jour, support « pourquoi la recherche montre des données anciennes ? », et deux ensembles de réglages de pertinence.
Si vous n'êtes pas sûr, commencez par Postgres, livrez quelque chose de simple, et n'ajoutez un autre moteur que lorsque vous avez une exigence claire qui ne peut pas être satisfaite.
Utilisez une règle en trois vérifications. Si vous passez les trois, restez avec PostgreSQL full-text search. Si vous échouez gravement sur une, envisagez un moteur dédié.
Besoins de pertinence : des résultats « assez bons » sont‑ils acceptables, ou avez‑vous besoin d'un classement quasi parfait sur de nombreux cas limites (fautes, synonymes, « les gens ont aussi cherché », résultats personnalisés) ? Si vous pouvez tolérer un ordre parfois imparfait, Postgres fonctionne généralement.
Volume de requêtes et latence : combien de recherches par seconde attendez‑vous au pic, et quel est votre budget réel de latence ? Si la recherche représente une petite part du trafic et que vous pouvez garder les requêtes rapides avec des index appropriés, Postgres suffit. Si la recherche devient une charge majeure et commence à concurrencer les lectures/écritures cœur, c'est un signal d'alerte.
Complexité : recherchez‑vous dans un ou deux champs texte, ou combinez‑vous de nombreux signaux (tags, filtres, décroissance temporelle, popularité, permissions) et plusieurs langues ? Plus la logique est complexe, plus vous ressentirez de friction dans SQL.
Un point de départ sûr : déployez une base dans Postgres, consignez les requêtes lentes et les recherches « sans résultat », puis décidez. Beaucoup d'apps ne le dépassent jamais et vous évitez d'exécuter et synchroniser un second système trop tôt.
Signaux rouges qui orientent vers un moteur dédié :
Signaux verts pour rester dans Postgres :
PostgreSQL full-text search est un moyen intégré de transformer du texte en quelque chose que la base peut rechercher rapidement, sans scanner chaque ligne. Il marche mieux quand votre contenu est déjà dans Postgres et que vous voulez une recherche rapide et correcte avec des opérations prévisibles.
Trois éléments à connaître :
ts_rank (ou ts_rank_cd) pour mettre les lignes les plus pertinentes en premier.La configuration linguistique compte car elle change la manière dont Postgres traite les mots. Avec la bonne config, « running » et « run » peuvent matcher (racinisation), et les mots vides peuvent être ignorés. Avec la mauvaise config, la recherche peut sembler cassée parce que la formulation normale des utilisateurs ne correspond plus à ce qui a été indexé.
Le préfixe (prefix matching) est utilisé quand on veut un comportement de type « suggestions pendant la frappe », par exemple faire matcher « dev » avec « developer ». Dans Postgres FTS, on le fait souvent avec un opérateur de préfixe (par exemple term:*). Cela peut améliorer la qualité perçue, mais augmente souvent le travail par requête, donc considérez‑le comme une option, pas par défaut.
Ce que Postgres n'essaie pas d'être : une plateforme de recherche complète avec toutes les fonctionnalités. Si vous avez besoin de correction orthographique floue, d'une autocomplétion avancée, de learning‑to‑rank, d'analyseurs complexes par champ, ou d'indexation distribuée sur de nombreux nœuds, vous êtes hors de la zone de confort intégrée. Pour beaucoup d'apps, toutefois, PostgreSQL full-text search fournit la plupart de ce que les utilisateurs attendent avec beaucoup moins de pièces mobiles.
Voici une petite structure réaliste pour le contenu que vous voulez rechercher :
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
Un bon point de départ pour PostgreSQL full-text search : construisez une requête à partir de ce que l'utilisateur a tapé, filtrez d'abord les lignes (quand vous le pouvez), puis classez les correspondances restantes.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at >= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Quelques détails qui font gagner du temps plus tard :
WHERE avant le classement (status, tenant_id, plages de dates). Vous classez moins de lignes, donc c'est plus rapide.ORDER BY (comme updated_at, puis id). Cela stabilise la pagination quand de nombreux résultats ont le même score.websearch_to_tsquery pour l'entrée utilisateur. Il gère les guillemets et les opérateurs simples de manière attendue.Une fois cette base en place, déplacez l'expression to_tsvector(...) dans une colonne stockée. Cela évite de la recalculer à chaque requête et facilite l'indexation.
La plupart des histoires « PostgreSQL full-text search est lent » reviennent à une chose : la base reconstruit le document de recherche à chaque requête. Corrigez cela d'abord en stockant un tsvector préconstruit et en l'indexant.
tsvector : colonne générée ou trigger ?Une colonne générée est l'option la plus simple quand votre document de recherche est construit à partir de colonnes de la même ligne. Elle reste correcte automatiquement et on l'oublie moins lors des mises à jour.
Utilisez un tsvector maintenu par trigger quand le document dépend de tables liées (par exemple en combinant une ligne produit avec le nom de sa catégorie), ou quand vous voulez une logique personnalisée difficile à exprimer comme une seule expression générée. Les triggers ajoutent des pièces mobiles, gardez‑les petites et testées.
Créez un index GIN sur la colonne tsvector. C'est la base qui rend PostgreSQL full-text search instantané pour une recherche d'app typique.
Une configuration qui marche pour beaucoup d'apps :
tsvector dans la même table que les lignes que vous recherchez le plus.tsvector.@@ contre le tsvector stocké, pas to_tsvector(...) recalculé à la volée.VACUUM (ANALYZE) après de gros backfills pour que le planner comprenne le nouvel index.Garder le vector dans la même table est généralement plus rapide et simple. Une table de recherche séparée peut avoir du sens si la table de base est très sujette aux écritures, ou si vous indexez un document combiné couvrant plusieurs tables et voulez le mettre à jour selon votre propre calendrier.
Les index partiels aident quand vous ne recherchez qu'un sous‑ensemble de lignes, comme status = 'active', un seul tenant dans une app multi‑tenant, ou une langue spécifique. Ils réduisent la taille de l'index et peuvent accélérer les recherches, mais seulement si vos requêtes incluent toujours le même filtre.
Vous pouvez obtenir des résultats étonnamment bons avec PostgreSQL full-text search si vous gardez des règles de pertinence simples et prévisibles.
Le gain le plus facile est le pondération des champs : les correspondances dans un titre doivent peser plus que dans le corps. Construisez un tsvector combiné où le titre a un poids plus élevé que la description, puis classez avec ts_rank ou ts_rank_cd.
Si vous voulez que les éléments « frais » ou « populaires » remontent, faites‑le prudemment. Un petit bonus est acceptable, mais ne laissez pas cela dépasser la pertinence textuelle. Un schéma pratique : classer d'abord par texte, puis départager par récence, ou ajouter un bonus plafonné pour qu'un élément récent non pertinent ne batte pas une correspondance parfaite plus ancienne.
Les synonymes et la correspondance de phrases font souvent diverger les attentes. Les synonymes ne sont pas automatiques ; vous devez ajouter un thésaurus ou un dictionnaire personnalisé, ou étendre les termes de la requête vous‑même (par exemple traiter « auth » comme « authentication »). La correspondance de phrase n'est pas non plus par défaut : les requêtes simples font matcher des mots n'importe où, pas une « phrase exacte ». Si les utilisateurs saisissent des phrases entre guillemets ou des questions longues, envisagez phraseto_tsquery ou websearch_to_tsquery pour mieux coller à la façon dont on recherche.
Le contenu multilingue nécessite une décision. Si vous connaissez la langue par document, stockez‑la et générez le tsvector avec la bonne configuration (English, Russian, etc.). Si vous ne la connaissez pas, une solution sûre est d'indexer avec la configuration simple (pas de racinisation), ou de garder deux vecteurs : un spécifique à la langue quand connue, un simple pour tout le reste.
Pour valider la pertinence, gardez‑le petit et concret :
Généralement, c'est assez pour la recherche d'apps comme « templates », « docs » ou « projects ».
La plupart des histoires « PostgreSQL full-text search est lent ou non pertinent » viennent de quelques erreurs évitables. Les corriger est souvent plus simple que d'ajouter un nouveau système.
Un piège courant est de traiter le tsvector comme une valeur calculée qui reste correcte toute seule. Si vous stockez un tsvector mais ne le mettez pas à jour à chaque insert/update, les résultats paraîtront aléatoires parce que l'index ne correspond plus au texte. Si vous calculez to_tsvector(...) à la volée dans la requête, les résultats peuvent être corrects mais lents, et vous perdez l'avantage d'un index dédié.
Une autre façon de nuire aux performances est de classer avant de réduire l'ensemble de candidats. ts_rank est utile, mais il devrait généralement s'exécuter après que Postgres ait utilisé l'index pour trouver les lignes correspondantes. Si vous calculez le score pour une grosse portion de la table (ou joignez d'abord d'autres tables), vous pouvez transformer une recherche rapide en un scan complet.
Les gens attendent parfois qu'une recherche « contains » se comporte comme LIKE '%term%'. Les wildcards en tête ne se mappent pas bien à la FTS parce que celle‑ci est basée sur des mots (lexèmes), pas sur des sous‑chaînes arbitraires. Si vous avez besoin de recherche par sous‑chaîne pour des codes produit ou des IDs partiels, utilisez un autre outil pour ce cas (par exemple l'index trigram) au lieu d'accuser la FTS.
Les problèmes de performance viennent souvent du traitement des résultats, pas du matching. Deux motifs à surveiller :
OFFSET, qui force Postgres à sauter de plus en plus de lignes.Les aspects opérationnels comptent aussi. La bloat des index peut s'accumuler après de nombreuses mises à jour, et un reindex peut être coûteux si vous attendez d'être déjà en crise. Mesurez les temps réels des requêtes (et vérifiez EXPLAIN ANALYZE) avant et après les changements. Sans chiffres, il est facile de « corriger » PostgreSQL full-text search en le rendant pire autrement.
Avant d'accuser PostgreSQL full-text search, exécutez ces vérifications. La plupart des bugs « Postgres search est lent ou non pertinent » viennent des bases manquantes, pas de la fonctionnalité elle‑même.
Construisez un vrai tsvector : stockez‑le dans une colonne générée ou maintenue, utilisez la bonne configuration linguistique (english, simple, etc.), et appliquez des poids si vous mélangez des champs (titre > sous‑titre > corps).
Normalisez ce que vous indexez : gardez les champs bruyants (IDs, boilerplate, texte de navigation) hors du tsvector, et tronquez les blobs énormes si les utilisateurs ne les recherchent jamais.
Créez le bon index : ajoutez un index GIN sur la colonne tsvector et confirmez qu'il est utilisé dans EXPLAIN. Si seule une sous‑partie est recherchable (par exemple status = 'published'), un index partiel peut réduire la taille et accélérer les lectures.
Gardez les tables saines : les tuples morts ralentissent les scans d'index. Un vacuum régulier compte, surtout sur du contenu fréquemment mis à jour.
Ayez un plan de reindex : de grosses migrations ou des index bloatés nécessitent parfois une fenêtre contrôlée de reindex.
Une fois les données et l'index corrects, concentrez‑vous sur la forme de la requête. PostgreSQL full-text search est rapide quand il peut réduire l'ensemble de candidats tôt.
Filtrez d'abord, puis classez : appliquez des filtres stricts (tenant, langue, publié, catégorie) avant le classement. Classer des milliers de lignes que vous allez ensuite éliminer est du travail gaspillé.
Utilisez un ordre stable : triez par score puis un tie‑breaker comme updated_at ou id pour que les résultats ne sautent pas entre les rafraîchissements.
Évitez la requête qui « fait tout » : si vous avez besoin de fuzzy matching ou tolérance aux fautes, faites‑le intentionnellement (et mesurez). Ne forcez pas des scans séquentiels par accident.
Testez des requêtes réelles : collectez les 20 recherches les plus fréquentes, vérifiez la pertinence à la main et gardez une petite liste de résultats attendus pour détecter les régressions.
Surveillez les chemins lents : consignez les requêtes lentes, révisez EXPLAIN (ANALYZE, BUFFERS) et surveillez la taille des index et le hit rate du cache pour repérer quand la croissance change le comportement.
Un centre d'aide SaaS est un bon départ car l'objectif est simple : aider les gens à trouver l'article qui répond à leur question. Vous avez quelques milliers d'articles, chacun avec un titre, un résumé court et un corps. La plupart des visiteurs tapent 2 à 5 mots comme « reset password » ou « billing invoice ».
Avec PostgreSQL full-text search, cela peut être réglé très vite. Vous stockez un tsvector pour les champs combinés, ajoutez un index GIN et classez par pertinence. Le succès ressemble à : résultats en moins de 100 ms, les 3 premiers résultats sont généralement corrects, et vous n'avez pas besoin de surveiller constamment le système.
Puis le produit grandit. Le support veut filtrer par zone produit, plateforme (web, iOS, Android) et plan (free, pro, business). Les rédacteurs veulent des synonymes, un « did you mean » et une meilleure gestion des fautes. Le marketing veut des analyses comme « recherches les plus fréquentes sans résultat ». Le trafic augmente et la recherche devient l'un des endpoints les plus sollicités.
Ce sont des signaux que l'ajout d'un moteur dédié peut valoir le coût :
Un chemin de migration pratique est de garder Postgres comme source de vérité, même après l'ajout d'un moteur. Commencez par consigner les requêtes et les cas sans résultat, puis exécutez une tâche de sync asynchrone qui copie uniquement les champs recherchables dans le nouvel index. Faites tourner les deux en parallèle un moment et basculez progressivement, plutôt que de tout parier dès le premier jour.
Si votre recherche consiste majoritairement à « trouver des documents contenant ces mots » et que votre jeu de données n'est pas massif, PostgreSQL full-text search suffit généralement. Commencez par ça, faites‑le fonctionner, et n'ajoutez un moteur dédié que quand vous pouvez nommer la fonctionnalité manquante ou la douleur de montée en charge.
Un récapitulatif pratique :
tsvector, ajouter un index GIN et vos besoins de classement sont basiques.Étape pratique : implémentez la requête de départ et l'index des sections précédentes, puis consignez quelques métriques simples pendant une semaine. Suivez le p95 du temps de requête, les requêtes lentes et un signal grossier de succès comme « recherche -> clic -> pas de rebond immédiat » (même un compteur d'événements basique aide). Vous verrez vite si vous avez besoin d'un meilleur classement ou juste d'une meilleure UX (filtres, surlignage, extraits plus clairs).
Commencez à planifier un moteur dédié quand l'une de ces fonctionnalités devient une exigence réelle (pas un « serait bien d'avoir »): autocomplétion forte ou recherche instantanée à chaque frappe à grande échelle, tolérance aux fautes et correction orthographique robustes, facettes et agrégations rapides avec comptes sur beaucoup de champs, outils avancés de pertinence (ensembles de synonymes, learning-to-rank, boosts par requête), ou une charge soutenue et de grands index difficiles à garder rapides.
Si vous voulez avancer vite côté app, Koder.ai (koder.ai) peut être utile pour prototyper l'UI et l'API de recherche via chat, puis itérer en toute sécurité en utilisant des snapshots et rollback pendant que vous mesurez le comportement réel des utilisateurs.
PostgreSQL full-text search est « suffisant » quand vous réalisez simultanément ces trois points :
Si vous pouvez atteindre cela avec un tsvector stocké + un index GIN, vous êtes généralement dans une bonne situation.
Par défaut, commencez par PostgreSQL full-text search. C'est plus rapide à mettre en place, garde vos données et la recherche au même endroit, et évite de construire et maintenir un pipeline d'indexation séparé.
Passez à un moteur dédié quand vous avez une exigence claire que Postgres ne gère pas bien (tolérance aux fautes d'orthographe de haute qualité, autocomplétion riche, facettes lourdes, ou charge de recherche qui concurrence le travail de la base de données principale).
Une règle simple : restez sur Postgres si vous validez ces trois points :
Si vous échouez gravement à l’un d’eux (surtout pertinence comme fautes/autocomplétion, ou trafic de recherche élevé), envisagez un moteur dédié.
Utilisez Postgres FTS quand la recherche consiste principalement à « trouver le bon enregistrement » sur quelques champs comme titre/corps/notes, avec des filtres simples (tenant, statut, catégorie).
C'est adapté aux centres d'aide, docs internes, tickets, recherche d'articles/blogs, et dashboards SaaS où les gens cherchent par nom de projet et notes.
Une bonne requête de base :
websearch_to_tsquery.Stockez un tsvector préconstruit et ajoutez un index GIN. Cela évite de recalculer to_tsvector(...) à chaque requête.
Configuration pratique :
Utilisez une colonne générée quand le document de recherche est construit à partir de colonnes de la même ligne (simple et robuste).
Utilisez une colonne maintenue par trigger quand le texte de recherche dépend de tables reliées ou d'une logique personnalisée.
Choix par défaut : colonne générée d'abord, triggers seulement si vous avez vraiment besoin de composer entre tables.
Commencez par des règles simples et prévisibles :
Validez ensuite avec une petite liste de requêtes réelles et résultats attendus.
FTS de Postgres est basé sur les mots, pas sur les sous‑chaînes. Il ne se comportera donc pas comme LIKE '%term%' pour des chaînes partielles arbitraires.
Si vous avez besoin de recherche par sous‑chaîne (codes produit, identifiants, fragments), traitez cela séparément (souvent avec un index trigram) au lieu de forcer la FTS à faire un travail pour lequel elle n'est pas conçue.
Signaux courants que vous avez dépassé Postgres FTS :
Chemin pratique : gardez Postgres comme source de vérité et ajoutez un index asynchrone quand l'exigence est claire.
@@ contre un tsvector stocké.ts_rank/ts_rank_cd plus un tie-breaker stable comme updated_at, id.Cela garde les résultats pertinents, rapides et stables pour la pagination.
tsvector sur la même table que vous interrogez.tsvector_column @@ tsquery.C'est le correctif le plus courant quand la recherche semble lente.