Quand la similarité vectorielle rencontre le filtre métier : le vrai défi de la recherche en production
Dans la plupart des applications RAG ou de recherche sémantique que nous déployons chez MulerTech, la requête "retourne-moi les 10 documents les plus proches" n'existe quasiment jamais seule. En production, le besoin ressemble plutôt à : trouve les 10 documents les plus similaires dans la catégorie legal, publiés ces 30 derniers jours. C'est ce qu'on appelle la recherche hybride : un ranking par similarité vectorielle combiné à des filtres scalaires classiques.
Cet article s'appuie sur les travaux publiés par Christopher Winslett sur le blog Crunchy Data pour explorer concrètement les patterns d'optimisation disponibles avec PostgreSQL et pgvector.
Le piège de la requête naïve
Voici la requête que tout le monde écrit en premier :
SELECT id, content
FROM docs
WHERE category = 'legal'
ORDER BY emb <=> '[0.031, ...]'
LIMIT 10;
Elle semble anodine. Avec un index B-tree classique, WHERE + ORDER BY est un problème résolu : le planificateur choisit un index, applique le filtre, trie ce qui reste. Mais avec un index vectoriel, la combinaison force PostgreSQL à sacrifier soit le recall (la qualité des résultats), soit la performance.
Pourquoi ne pas simplement croiser les deux index ? PostgreSQL sait déjà faire des bitmap index scans sur plusieurs index B-tree. Le problème est structurel : un index HNSW stocke les vecteurs dans un graphe de proximité optimisé pour la navigation rapide. Ce graphe est construit sur l'ensemble des données, sans connaissance du filtre category = 'legal'. Quand PostgreSQL traverse le graphe pour trouver les 10 voisins les plus proches, il peut traverser des centaines de nœuds avant d'en trouver 10 qui passent le filtre — ou pire, s'arrêter trop tôt et manquer des résultats pertinents.
Les trois stratégies disponibles avec pgvector
1. Le scan séquentiel filtré (Sequential Scan)
La solution la plus simple : ignorer l'index vectoriel, filtrer d'abord avec le B-tree sur category, puis calculer la similarité sur le sous-ensemble retenu.
SET enable_indexscan = off; -- forcer le seq scan sur le vecteur
SELECT id, content
FROM docs
WHERE category = 'legal'
ORDER BY emb <=> '[0.031, ...]'
LIMIT 10;
Avantage : recall parfait, résultats exacts. Inconvénient : si le filtre retourne 500 000 lignes, vous calculez 500 000 distances. Acceptable sur de petits sous-ensembles, catastrophique à l'échelle.
2. Les index partiels
Si vos filtres sont connus et stables (par exemple, un ensemble fixe de catégories), vous pouvez créer un index HNSW par sous-ensemble :
CREATE INDEX ON docs USING hnsw (emb vector_cosine_ops)
WHERE category = 'legal';
Le planificateur utilisera cet index uniquement quand category = 'legal' est présent dans la requête. Résultat : recall maximal, performance maximale.
Limite : vous multipliez les index. Avec 50 catégories dynamiques, cette approche devient ingérable. Elle est réservée aux filtres à faible cardinalité et haute fréquence.
3. Les scans itératifs de pgvector
C'est le comportement par défaut de pgvector depuis les versions récentes. Plutôt que de chercher les K voisins globaux puis filtrer, l'index est parcouru itérativement : pgvector élargit progressivement la recherche dans le graphe HNSW jusqu'à accumuler suffisamment de résultats qui passent le filtre.
Deux paramètres clés pilotent ce comportement :
-- Nombre de candidats explorés par itération
SET hnsw.ef_search = 100;
-- Nombre maximum d'itérations avant de basculer en seq scan
SET hnsw.iterative_scan = relaxed_order;
Le mode relaxed_order autorise pgvector à retourner des résultats légèrement hors-ordre si nécessaire pour éviter une explosion du temps de scan. Le mode strict_order garantit l'ordre exact au prix d'une exploration plus coûteuse.
Le bon réglage dépend de la sélectivité de votre filtre. Un filtre très sélectif (1% des lignes) nécessite d'explorer beaucoup plus de candidats qu'un filtre large (50% des lignes). Un ef_search trop bas dégrade le recall ; trop haut, et on approche du coût d'un seq scan.
Recommandations pratiques pour vos applications Symfony/PHP
Dans le contexte d'une application Symfony avec Doctrine, voici ce que nous appliquons chez MulerTech :
Mesurer avant d'optimiser. Utilisez EXPLAIN (ANALYZE, BUFFERS) pour identifier si PostgreSQL utilise l'index vectoriel ou bascule en seq scan. Le plan d'exécution vous dira tout.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content
FROM docs
WHERE category = 'legal'
ORDER BY emb <=> '[0.031, ...]'
LIMIT 10;
Adapter la stratégie à la cardinalité du filtre :
- Filtre sur une colonne à faible cardinalité et usage intensif → index partiel
- Filtre dynamique sur colonne à haute cardinalité → scan itératif avec
ef_searchajusté - Petit dataset ou filtre très sélectif → seq scan assumé, plus simple à maintenir
Exposer les paramètres pgvector par requête. Depuis PHP, vous pouvez SET des paramètres de session avant votre requête vectorielle :
$conn = $entityManager->getConnection();
$conn->executeStatement('SET hnsw.ef_search = 200');
$results = $conn->executeQuery($hybridQuery, $params)->fetchAllAssociative();
Cela permet d'adapter dynamiquement le comportement selon le contexte métier, sans toucher à la configuration globale du serveur.
Surveiller le recall en production. La dégradation du recall est silencieuse : votre requête retourne 10 résultats, mais pas forcément les 10 meilleurs. Mettez en place des métriques de qualité (évaluation humaine sur un échantillon, comparaison seq scan vs index scan sur des requêtes de référence).
Conclusion
La recherche hybride avec pgvector n'est pas un problème insolvable, mais elle demande de sortir du pilotage automatique. Le planificateur PostgreSQL fait de son mieux, mais la nature même des index vectoriels impose des compromis que vous devez piloter consciemment : recall vs performance, index multiples vs configuration dynamique, exactitude vs latence.
L'article original de Christopher Winslett sur le blog Crunchy Data explore ces patterns en profondeur et constitue une référence solide pour aller plus loin. Pour vos projets Symfony intégrant de la recherche sémantique ou du RAG, une revue de vos stratégies d'indexation pgvector est souvent le levier d'optimisation le plus impactant — bien avant de toucher à votre modèle d'embedding.