🔝 Retour au Sommaire
Imaginez que vous avez une table avec 10 millions d'articles de blog. Sans index, une recherche textuelle pourrait prendre plusieurs minutes car PostgreSQL doit scanner chaque ligne pour vérifier si elle correspond à votre requête. Avec un index GIN bien configuré, la même recherche s'exécute en quelques millisecondes.
Les index GIN (Generalized Inverted Index) sont indispensables pour obtenir de bonnes performances avec le Full-Text Search. Ils transforment une opération O(n) en O(log n), soit un gain de performance de 100× à 10 000× selon la taille des données.
Exemple concret :
-- SANS index GIN : 45 secondes sur 10M d'articles
SELECT titre FROM articles
WHERE to_tsvector('french', contenu) @@ plainto_tsquery('french', 'postgresql');
-- Plan : Seq Scan (balayage séquentiel)
-- AVEC index GIN : 15 millisecondes
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
SELECT titre FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql');
-- Plan : Bitmap Index Scan (utilise l'index)Gain : 3000× plus rapide !
Un index inversé (inverted index) fonctionne à l'opposé d'un index normal :
Index normal (B-Tree) : Document → Mots
Document 1 → ["postgresql", "base", "données"]
Document 2 → ["mysql", "base", "données"]
Document 3 → ["postgresql", "performance"]
Index inversé (GIN) : Mot → Documents
"postgresql" → [Document 1, Document 3]
"base" → [Document 1, Document 2]
"données" → [Document 1, Document 2]
"mysql" → [Document 2]
"performance"→ [Document 3]
Avec un index inversé, pour trouver tous les documents contenant "postgresql", il suffit de faire une seule recherche dans l'index !
Index GIN sur search_vector
│
├── Lexème: "base"
│ ├── Document ID: 1
│ ├── Document ID: 2
│ ├── Document ID: 5
│ └── Document ID: 8
│
├── Lexème: "donn"
│ ├── Document ID: 1
│ ├── Document ID: 2
│ └── Document ID: 9
│
├── Lexème: "postgresql"
│ ├── Document ID: 1
│ ├── Document ID: 3
│ ├── Document ID: 7
│ └── Document ID: 10
│
└── ...
- Recherche rapide : O(log n) au lieu de O(n)
- Opérateurs complexes : AND, OR, NOT nativement supportés
- Compression efficace : Les listes de documents sont compressées
- Scalabilité : Performances constantes même avec des milliards de mots
CREATE INDEX nom_index ON nom_table USING GIN(colonne_tsvector);Les index GIN ne peuvent être créés que sur des colonnes de type tsvector :
-- ❌ IMPOSSIBLE : Index GIN sur colonne TEXT
CREATE INDEX idx_bad ON articles USING GIN(contenu);
-- Erreur : data type text has no default operator class for access method "gin"
-- ✅ BON : Index GIN sur colonne tsvector
ALTER TABLE articles ADD COLUMN search_vector tsvector;
UPDATE articles SET search_vector = to_tsvector('french', contenu);
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector); -- 1. Créer la table
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
titre VARCHAR(200),
contenu TEXT,
auteur VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
-- 2. Insérer des données (simuler 100k articles)
INSERT INTO articles (titre, contenu, auteur)
SELECT
'Article ' || i,
'Contenu de l''article numéro ' || i || '. ' ||
CASE WHEN i % 100 = 0 THEN 'PostgreSQL est mentionné ici.' ELSE '' END,
'Auteur ' || (i % 50)
FROM generate_series(1, 100000) AS i;
-- 3. Ajouter la colonne tsvector
ALTER TABLE articles ADD COLUMN search_vector tsvector;
-- 4. Remplir la colonne tsvector
UPDATE articles
SET search_vector =
setweight(to_tsvector('french', COALESCE(titre, '')), 'A') ||
setweight(to_tsvector('french', COALESCE(contenu, '')), 'B');
-- 5. Créer l'index GIN
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- 6. Analyser la table (mettre à jour les statistiques)
ANALYZE articles;Si vous n'avez pas de colonne tsvector dédiée, vous pouvez créer un index sur une expression :
-- Index GIN sur expression (sans colonne dédiée)
CREATE INDEX idx_articles_content_search
ON articles
USING GIN(to_tsvector('french', contenu));
-- Requête qui utilise l'index
SELECT titre FROM articles
WHERE to_tsvector('french', contenu) @@ plainto_tsquery('french', 'postgresql'); Recommandation : Toujours utiliser une colonne tsvector dédiée pour de meilleures performances.
Testons sur une table de 100 000 articles :
-- Désactiver l'utilisation des index (pour tester sans)
SET enable_indexscan = off;
SET enable_bitmapscan = off;
-- Recherche sans index
EXPLAIN (ANALYZE, BUFFERS)
SELECT titre, auteur
FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql'); Résultat :
Seq Scan on articles (cost=0.00..5234.00 rows=50 width=68)
(actual time=0.045..245.678 rows=1000 loops=1)
Filter: (search_vector @@ plainto_tsquery('french'::regconfig, 'postgresql'::text))
Rows Removed by Filter: 99000
Buffers: shared hit=2234
Planning Time: 0.234 ms
Execution Time: 245.912 ms ← 246 ms
Interprétation :
- Seq Scan : Balayage séquentiel de toute la table
- 99 000 lignes filtrées : Scan de 100% de la table
- 246 ms : Temps d'exécution
-- Réactiver les index
RESET enable_indexscan;
RESET enable_bitmapscan;
-- Même recherche avec index
EXPLAIN (ANALYZE, BUFFERS)
SELECT titre, auteur
FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql'); Résultat :
Bitmap Heap Scan on articles (cost=50.45..234.67 rows=50 width=68)
(actual time=1.234..3.456 rows=1000 loops=1)
Recheck Cond: (search_vector @@ plainto_tsquery('french'::regconfig, 'postgresql'::text))
Heap Blocks: exact=987
Buffers: shared hit=1012
-> Bitmap Index Scan on idx_articles_search (cost=0.00..50.44 rows=50 width=0)
(actual time=1.123..1.123 rows=1000 loops=1)
Index Cond: (search_vector @@ plainto_tsquery('french'::regconfig, 'postgresql'::text))
Buffers: shared hit=25
Planning Time: 0.189 ms
Execution Time: 3.678 ms ← 3.7 ms
Interprétation :
- Bitmap Index Scan : Utilise l'index GIN
- 25 blocs d'index : Seulement l'index consulté
- 3.7 ms : Temps d'exécution
Gain : 67× plus rapide (246ms → 3.7ms) !
Sans Index GIN :
[██████████████████████████████████████████████████] 246 ms
Scan de 100 000 lignes
Avec Index GIN :
[███] 3.7 ms
Scan de 1 000 lignes (celles qui matchent)
Les index GIN accélèrent l'opérateur @@ et ses variantes :
-- ✅ Utilise l'index GIN
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('french', 'postgresql');
-- ✅ Utilise l'index GIN
SELECT * FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql base données');
-- ✅ Utilise l'index GIN
SELECT * FROM articles
WHERE search_vector @@ websearch_to_tsquery('french', 'postgresql "base données"'); -- ✅ AND : Utilise l'index
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('french', 'postgresql & base');
-- ✅ OR : Utilise l'index
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('french', 'postgresql | mysql');
-- ✅ NOT : Utilise l'index
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('french', 'base & !oracle');
-- ✅ Phrases : Utilise l'index
SELECT * FROM articles
WHERE search_vector @@ phraseto_tsquery('french', 'base de données'); -- ❌ N'utilise PAS l'index (fonction sur tsvector)
SELECT * FROM articles
WHERE length(search_vector) > 10;
-- ❌ N'utilise PAS l'index (opérateur non supporté)
SELECT * FROM articles
WHERE search_vector > to_tsquery('french', 'postgresql'); Par défaut, les insertions/mises à jour sont immédiatement appliquées à l'index GIN, ce qui peut être lent.
L'option fastupdate permet de mettre en attente les mises à jour dans une liste temporaire :
-- Créer un index GIN avec fastupdate activé
CREATE INDEX idx_articles_search
ON articles
USING GIN(search_vector)
WITH (fastupdate = on); Comment ça marche :
- Les nouvelles entrées sont stockées dans une pending list (liste d'attente)
- Périodiquement, la pending list est fusionnée avec l'index principal
- Les recherches consultent index + pending list
Avantages :
- ✅ Insertions/mises à jour plus rapides (10-50%)
- ✅ Moins de fragmentation de l'index
Inconvénients :
⚠️ Recherches légèrement plus lentes si la pending list est grande⚠️ Consommation mémoire supplémentaire
Recommandation :
- ON : Pour tables avec insertions/mises à jour fréquentes
- OFF : Pour tables principalement en lecture
Définit la taille maximale de la pending list (par index) :
-- Par défaut : 4 MB
SHOW gin_pending_list_limit;
-- Résultat : 4MB
-- Augmenter pour tables avec beaucoup d'écritures
SET gin_pending_list_limit = '16MB';
-- Ou au niveau de l'index
CREATE INDEX idx_articles_search
ON articles
USING GIN(search_vector)
WITH (fastupdate = on, gin_pending_list_limit = 16384); -- 16 MB Impact :
- Petite valeur (1-4 MB) : Fusions fréquentes, écritures plus lentes mais recherches rapides
- Grande valeur (16-64 MB) : Fusions moins fréquentes, écritures rapides mais recherches un peu plus lentes
Pour éviter de bloquer la table pendant la création de l'index :
-- Index créé sans bloquer les écritures/lectures
CREATE INDEX CONCURRENTLY idx_articles_search
ON articles
USING GIN(search_vector); Avantages :
- ✅ Aucun verrou exclusif sur la table
- ✅ Applications continuent de fonctionner
Inconvénients :
⚠️ Création 2-3× plus lente⚠️ Utilise plus de ressources (CPU, I/O)
Recommandation : Toujours utiliser CONCURRENTLY en production.
Avec le temps, les index GIN peuvent :
- Gonfler (bloat) après de nombreuses suppressions/mises à jour
- Accumuler des entrées dans la pending list
- Devenir fragmentés
Symptômes d'un index dégradé :
- Recherches de plus en plus lentes
- Taille de l'index anormalement grande
- EXPLAIN montre des coûts élevés
-- Vacuum standard (nettoie la pending list)
VACUUM articles;
-- Vacuum verbose (avec détails)
VACUUM VERBOSE articles;Sortie :
INFO: vacuuming "public.articles"
INFO: index "idx_articles_search" now contains 100000 row versions in 2345 pages
DETAIL: 0 index row versions were removed.
500 index pages have been deleted, 0 are currently reusable.
Pour reconstruire complètement un index :
-- Reindex un index spécifique (bloque les écritures)
REINDEX INDEX idx_articles_search;
-- Reindex tous les index d'une table
REINDEX TABLE articles;
-- Reindex sans bloquer (PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_articles_search;Quand reindexer ?
- Après des suppressions massives (>50% de la table)
- Si l'index a 2× la taille attendue (bloat important)
- Si les performances se dégradent malgré VACUUM
- Migration de version majeure PostgreSQL
-- Taille des index GIN
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS taille_index,
idx_scan AS nombre_scans,
idx_tup_read AS tuples_lus
FROM pg_stat_user_indexes
WHERE indexrelname LIKE 'idx_%search%'
ORDER BY pg_relation_size(indexrelid) DESC; Résultat exemple :
schemaname | tablename | indexname | taille_index | nombre_scans | tuples_lus
-----------+-----------+---------------------+--------------+--------------+------------
public | articles | idx_articles_search | 45 MB | 12543 | 2345678
-- Extension pgstattuple pour analyser le bloat
CREATE EXTENSION IF NOT EXISTS pgstattuple;
-- Analyser un index GIN
SELECT
indexname,
leaf_pages,
deleted_pages,
ROUND(100.0 * deleted_pages / NULLIF(leaf_pages, 0), 2) AS bloat_percent
FROM pgstatginindex('idx_articles_search');Interprétation :
- bloat_percent < 10% : Index sain
- bloat_percent 10-30% : Bloat modéré, VACUUM suffit
- bloat_percent > 30% : Bloat important, REINDEX recommandé
Créer un index uniquement sur un sous-ensemble des lignes :
-- Index uniquement sur les articles publiés
CREATE INDEX idx_articles_published_search
ON articles
USING GIN(search_vector)
WHERE statut = 'publié';
-- Requête qui utilise l'index
SELECT titre FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql')
AND statut = 'publié'; -- ← Condition de l'indexAvantages :
- ✅ Index plus petit (moins d'espace)
- ✅ Maintenance plus rapide
- ✅ Recherches plus rapides sur le sous-ensemble
Cas d'usage :
- Articles publiés vs brouillons
- Produits en stock vs rupture
- Documents actifs vs archivés
Pour des requêtes avec plusieurs filtres :
-- Index GIN pour la recherche textuelle
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- Index B-Tree pour les filtres classiques
CREATE INDEX idx_articles_date ON articles(created_at);
CREATE INDEX idx_articles_auteur ON articles(auteur);
-- Requête combinée (PostgreSQL choisit les index optimaux)
SELECT titre, created_at
FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql')
AND created_at > NOW() - INTERVAL '1 month'
AND auteur = 'Jean Dupont';PostgreSQL peut :
- Utiliser l'index GIN pour filtrer par texte
- Puis l'index B-Tree pour filtrer par date
- Ou vice versa, selon les statistiques
Ajuster les paramètres globaux pour optimiser les index GIN :
-- Mémoire pour la construction d'index
SET maintenance_work_mem = '256MB'; -- Défaut : 64MB
-- Impact : Index créés plus rapidement
-- Mémoire pour la pending list (par session)
SET gin_pending_list_limit = '16MB'; -- Défaut : 4MB
-- Impact : Moins de fusions, écritures plus rapides
-- Afficher les paramètres GIN actuels
SHOW maintenance_work_mem;
SHOW gin_pending_list_limit; Recommandations :
- maintenance_work_mem : 5-10% de la RAM pour CREATE INDEX
- gin_pending_list_limit : 16-32 MB pour tables avec beaucoup d'écritures
-- Structure
CREATE TABLE blog_articles (
id SERIAL PRIMARY KEY,
titre VARCHAR(200),
contenu TEXT,
auteur VARCHAR(100),
categorie VARCHAR(50),
statut VARCHAR(20) DEFAULT 'brouillon', -- 'brouillon', 'publié', 'archivé'
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
search_vector tsvector
);
-- Trigger pour maintenir search_vector
CREATE OR REPLACE FUNCTION blog_search_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('french', COALESCE(NEW.titre, '')), 'A') ||
setweight(to_tsvector('french', COALESCE(NEW.contenu, '')), 'B') ||
setweight(to_tsvector('french', COALESCE(NEW.auteur, '')), 'D');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvector_update
BEFORE INSERT OR UPDATE ON blog_articles
FOR EACH ROW EXECUTE FUNCTION blog_search_trigger();
-- Index GIN partiel (uniquement articles publiés)
CREATE INDEX idx_blog_published_search
ON blog_articles
USING GIN(search_vector)
WHERE statut = 'publié'
WITH (fastupdate = on, gin_pending_list_limit = 16384);
-- Index B-Tree complémentaires
CREATE INDEX idx_blog_date ON blog_articles(created_at DESC);
CREATE INDEX idx_blog_categorie ON blog_articles(categorie);
-- Requête optimisée
SELECT
id,
titre,
LEFT(contenu, 200) AS extrait,
created_at,
ts_rank(search_vector, query) AS score
FROM
blog_articles,
websearch_to_tsquery('french', 'postgresql base données') query
WHERE
statut = 'publié' -- Utilise l'index partiel
AND search_vector @@ query -- Utilise l'index GIN
AND created_at > NOW() - INTERVAL '1 year' -- Peut utiliser idx_blog_date
ORDER BY score DESC
LIMIT 20; -- Structure
CREATE TABLE produits (
id SERIAL PRIMARY KEY,
nom VARCHAR(200),
description TEXT,
marque VARCHAR(100),
categorie VARCHAR(50),
prix NUMERIC(10, 2),
stock INTEGER,
actif BOOLEAN DEFAULT true,
search_vector tsvector
);
-- Trigger avec poids adaptés e-commerce
CREATE OR REPLACE FUNCTION produits_search_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
-- Nom produit : très important (doublé)
setweight(to_tsvector('french', COALESCE(NEW.nom, '')), 'A') ||
setweight(to_tsvector('french', COALESCE(NEW.nom, '')), 'A') ||
-- Marque : important
setweight(to_tsvector('french', COALESCE(NEW.marque, '')), 'A') ||
-- Description : normal
setweight(to_tsvector('french', COALESCE(NEW.description, '')), 'B') ||
-- Catégorie : faible
setweight(to_tsvector('french', COALESCE(NEW.categorie, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvector_update
BEFORE INSERT OR UPDATE ON produits
FOR EACH ROW EXECUTE FUNCTION produits_search_trigger();
-- Index GIN partiel (uniquement produits actifs et en stock)
CREATE INDEX idx_produits_available_search
ON produits
USING GIN(search_vector)
WHERE actif = true AND stock > 0
WITH (fastupdate = on);
-- Index complémentaires
CREATE INDEX idx_produits_prix ON produits(prix);
CREATE INDEX idx_produits_categorie ON produits(categorie);
-- Recherche optimisée
SELECT
nom,
marque,
prix,
stock,
ts_rank(
ARRAY[0.05, 0.2, 0.4, 2.0], -- Boost nom/marque
search_vector,
query,
1 -- Normalisation
) AS score
FROM
produits,
websearch_to_tsquery('french', 'chaussure running nike') query
WHERE
actif = true
AND stock > 0 -- Conditions de l'index partiel
AND search_vector @@ query
AND prix BETWEEN 50 AND 200 -- Peut utiliser idx_produits_prix
ORDER BY score DESC
LIMIT 50; -- Structure
CREATE TABLE documentation (
id SERIAL PRIMARY KEY,
titre VARCHAR(200),
contenu TEXT,
langue VARCHAR(10), -- 'fr', 'en', 'es'
version VARCHAR(20),
categorie VARCHAR(50),
search_vector tsvector
);
-- Trigger intelligent multilingue
CREATE OR REPLACE FUNCTION doc_search_trigger() RETURNS trigger AS $$
DECLARE
config regconfig;
BEGIN
-- Choisir la configuration selon la langue
config := CASE NEW.langue
WHEN 'fr' THEN 'french'::regconfig
WHEN 'en' THEN 'english'::regconfig
WHEN 'es' THEN 'spanish'::regconfig
ELSE 'simple'::regconfig
END;
NEW.search_vector :=
setweight(to_tsvector(config, COALESCE(NEW.titre, '')), 'A') ||
setweight(to_tsvector(config, COALESCE(NEW.contenu, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvector_update
BEFORE INSERT OR UPDATE ON documentation
FOR EACH ROW EXECUTE FUNCTION doc_search_trigger();
-- Index GIN par langue (plus performant que index global)
CREATE INDEX idx_doc_fr_search
ON documentation
USING GIN(search_vector)
WHERE langue = 'fr';
CREATE INDEX idx_doc_en_search
ON documentation
USING GIN(search_vector)
WHERE langue = 'en';
CREATE INDEX idx_doc_es_search
ON documentation
USING GIN(search_vector)
WHERE langue = 'es';
-- Recherche par langue
SELECT
titre,
version,
ts_rank(search_vector, query) AS score
FROM
documentation,
plainto_tsquery('french', 'connexion base données') query
WHERE
langue = 'fr' -- Utilise idx_doc_fr_search
AND search_vector @@ query
ORDER BY score DESC;-- EXPLAIN montre si l'index est utilisé
EXPLAIN (ANALYZE, BUFFERS)
SELECT titre FROM articles
WHERE search_vector @@ plainto_tsquery('french', 'postgresql'); Bon signe : "Bitmap Index Scan" ou "Index Scan"
Bitmap Index Scan on idx_articles_search (cost=...)
Index Cond: (search_vector @@ ...)
Mauvais signe : "Seq Scan"
Seq Scan on articles (cost=...)
Filter: (search_vector @@ ...)
-- Mettre à jour les statistiques
ANALYZE articles;
-- Vérifier la date de dernière analyse
SELECT
schemaname,
tablename,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE tablename = 'articles'; Pour de petites tables (< 1000 lignes), un scan séquentiel peut être plus rapide qu'un index.
-- Forcer l'utilisation de l'index (debug uniquement)
SET enable_seqscan = off;
-- Tester la requête
EXPLAIN SELECT * FROM articles WHERE search_vector @@ query;
-- Remettre à normal
RESET enable_seqscan;Si la requête retourne > 10-20% de la table, PostgreSQL préfère un scan séquentiel.
-- Vérifier la sélectivité
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE search_vector @@ plainto_tsquery('french', 'le')) AS matches,
ROUND(100.0 * COUNT(*) FILTER (WHERE search_vector @@ plainto_tsquery('french', 'le')) / COUNT(*), 2) AS pourcentage
FROM articles;
-- Si pourcentage > 20%, un scan séquentiel est normal-- ❌ N'utilise PAS l'index partiel (condition manquante)
CREATE INDEX idx_partial ON articles USING GIN(search_vector) WHERE statut = 'publié';
SELECT * FROM articles WHERE search_vector @@ query;
-- Manque : AND statut = 'publié'
-- ✅ Utilise l'index partiel
SELECT * FROM articles
WHERE search_vector @@ query AND statut = 'publié'; -- Voir combien de fois chaque index est utilisé
SELECT
schemaname,
tablename,
indexname,
idx_scan AS scans,
idx_tup_read AS tuples_lus,
idx_tup_fetch AS tuples_recuperes,
pg_size_pretty(pg_relation_size(indexrelid)) AS taille
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC; Interprétation :
- idx_scan = 0 : Index jamais utilisé (candidat à la suppression)
- idx_scan > 0 : Index utilisé (pertinent)
- Ratio tuples_lus/idx_scan : Nombre moyen de résultats par recherche
-- ✅ BON : Colonne dédiée
ALTER TABLE articles ADD COLUMN search_vector tsvector;
UPDATE articles SET search_vector = to_tsvector('french', contenu);
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- ❌ ÉVITER : Index sur expression
CREATE INDEX idx_bad ON articles USING GIN(to_tsvector('french', contenu));
-- Plus lent à l'insertion/mise à jour-- ✅ BON : Pas de blocage
CREATE INDEX CONCURRENTLY idx_articles_search ON articles USING GIN(search_vector);
-- ❌ RISQUÉ : Bloque la table (peut durer plusieurs minutes)
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);-- Tables avec insertions/mises à jour fréquentes
CREATE INDEX idx_articles_search
ON articles
USING GIN(search_vector)
WITH (fastupdate = on, gin_pending_list_limit = 16384); -- Si 90% des recherches concernent les articles publiés
CREATE INDEX idx_published_search
ON articles
USING GIN(search_vector)
WHERE statut = 'publié';
-- Index 10× plus petit !CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
ANALYZE articles; -- ← IMPORTANT : Met à jour les statistiques -- Script de maintenance hebdomadaire
VACUUM ANALYZE articles;
-- Reindex annuel (ou après opérations massives)
REINDEX INDEX CONCURRENTLY idx_articles_search;-- Avant création de l'index
\timing on
SELECT COUNT(*) FROM articles WHERE search_vector @@ query;
-- Temps : 2450 ms
-- Créer l'index
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
ANALYZE articles;
-- Après création de l'index
SELECT COUNT(*) FROM articles WHERE search_vector @@ query;
-- Temps : 15 ms
-- Gain : 163× plus rapide| Critère | GIN | GiST |
|---|---|---|
| Performance recherche | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Très bon |
| Performance écriture | ⭐⭐⭐ Moyen | ⭐⭐⭐⭐ Bon |
| Taille index | Plus grand | Plus petit |
| Full-Text Search | ✅ Recommandé | |
| Données spatiales | ❌ Non | ✅ Oui |
| Arrays | ✅ Oui | |
| JSONB | ✅ Oui |
Verdict Full-Text Search : GIN est le choix optimal
- ❌ Très petites tables (< 1000 lignes) : Overhead pas justifié
- ❌ Tables avec écritures ultra-fréquentes et peu de lectures : BRIN peut être mieux
- ❌ Colonnes avec très faible cardinalité : B-Tree suffit
- Créer une colonne tsvector dédiée
- Ajouter un trigger pour maintenir la colonne automatiquement
- Créer l'index GIN (avec CONCURRENTLY en production)
- Exécuter ANALYZE après création
- Vérifier avec EXPLAIN que l'index est utilisé
- Mesurer les performances avant/après
- Planifier la maintenance (VACUUM hebdomadaire)
- Surveiller la taille et l'utilisation de l'index
-- Création complète
ALTER TABLE ma_table ADD COLUMN search_vector tsvector;
CREATE INDEX CONCURRENTLY idx_search ON ma_table USING GIN(search_vector);
ANALYZE ma_table;
-- Maintenance
VACUUM ANALYZE ma_table;
REINDEX INDEX CONCURRENTLY idx_search;
-- Surveillance
SELECT pg_size_pretty(pg_relation_size('idx_search'));
SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelname = 'idx_search';
-- Debug
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM ma_table WHERE search_vector @@ query;-- Pour la création d'index
SET maintenance_work_mem = '256MB'; -- Ou plus selon RAM
-- Pour les écritures fréquentes
CREATE INDEX ... WITH (fastupdate = on, gin_pending_list_limit = 16384);
-- Index partiel si applicable
CREATE INDEX ... WHERE condition_frequente;Les index GIN sont absolument essentiels pour obtenir de bonnes performances avec le Full-Text Search dans PostgreSQL. Ils transforment des requêtes de plusieurs secondes en quelques millisecondes.
Points clés à retenir :
-
Index GIN = Obligatoire pour Full-Text Search à grande échelle
- Gain de performance : 100× à 10 000×
- Structure d'index inversé optimale pour la recherche textuelle
-
Colonne tsvector dédiée recommandée
- Plus performant que les index sur expressions
- Maintenance automatique avec triggers
-
Options d'optimisation :
fastupdate = onpour écritures fréquentesgin_pending_list_limitadapté à votre charge- Index partiels pour réduire la taille
-
Maintenance régulière :
- VACUUM pour nettoyer la pending list
- REINDEX en cas de bloat important
- ANALYZE pour mettre à jour les statistiques
-
Surveillance :
- Vérifier l'utilisation avec pg_stat_user_indexes
- Mesurer la taille avec pg_relation_size
- Valider les plans avec EXPLAIN ANALYZE
Sans index GIN, le Full-Text Search PostgreSQL est inutilisable en production. Avec un index GIN bien configuré et maintenu, vous obtenez un moteur de recherche performant et scalable, directement intégré à votre base de données.
Prochaine étape : Explorez les techniques avancées de recherche, les highlights, et l'intégration avec des interfaces utilisateur pour créer des expériences de recherche complètes !
Ressources complémentaires :
- Documentation GIN : https://www.postgresql.org/docs/current/gin.html
- GIN Implementation : https://www.postgresql.org/docs/current/gin-implementation.html
- Index Types Comparison : https://www.postgresql.org/docs/current/indexes-types.html
- Performance Tuning : https://www.postgresql.org/docs/current/performance-tips.html
⏭️ Foreign Data Wrappers (FDW) : PostgreSQL comme hub de données