À mesure que les bases de données vectorielles passent d'un simple appariement de similarité à des pipelines complexes de Génération Augmentée par Récupération (RAG), la capacité à filtrer efficacement les vecteurs par métadonnées est devenue un goulot d'étranglement critique. De nombreux développeurs rencontrent une dégradation des performances lors de l'interrogation de Qdrant avec des filtres stricts sur des champs à haute cardinalité, tels que les identifiants d'utilisateur, les horodatages ou des balises de documents spécifiques. Bien que Qdrant soit conçu pour la vitesse, la recherche vectorielle brute combinée à un filtrage non optimisé peut entraîner des réponses de requête lentes et une surcharge de calcul inutile. La clé pour débloquer une recherche filtrée à haut débit et faible latence réside dans la compréhension et l'exploitation de l'Indexation de charge utile.
Pourquoi le filtrage à haute cardinalité ralentit la recherche
Par défaut, Qdrant effectue d'abord la recherche de similarité vectorielle, puis applique les filtres de charge utile aux candidats résultants. Cette approche « rechercher-ensuite-filtrer » est efficace pour les champs à faible cardinalité (comme les drapeaux booléens ou de petits ensembles de catégories) car le filtre élimine une petite partie des résultats. Cependant, pour les champs à haute cardinalité — où un filtre pourrait ne correspondre qu'à une infime fraction de l'ensemble du jeu de données, ou où le champ contient des millions de valeurs uniques — le système doit examiner plus de vecteurs pour en trouver suffisamment qui correspondent aux critères.
Sans index, Qdrant doit inspecter la charge utile de chaque vecteur qui entre dans l'ensemble des candidats top-K pour déterminer s'il remplit les conditions du filtre. Si votre filtre est restrictif (par ex., user_id == 12345 dans un jeu de données de 10 millions de vecteurs), le système peut avoir besoin de récupérer des milliers de vecteurs juste pour en trouver quelques-uns qui sont à la fois similaires et pertinents. Cela entraîne des coûts d'E/S élevés et une latence accrue.
La puissance des index de charge utile
Les index de charge utile permettent à Qdrant de précalculer et de stocker des structures de données efficaces pour des champs de métadonnées spécifiques. Lorsqu'un index est créé pour un champ, Qdrant peut inverser la logique de recherche. Au lieu de « trouver des vecteurs similaires, puis vérifier s'ils correspondent au filtre », il peut efficacement « trouver les vecteurs qui correspondent au filtre, puis vérifier la similarité au sein de ce sous-ensemble ».
Pour les champs à haute cardinalité, Qdrant utilise les types d'index INTEGER, KEYWORD ou DATETIME. Ces index permettent au moteur de localiser rapidement le sous-ensemble de vecteurs qui satisfont la condition, réduisant considérablement l'espace de recherche avant le début du travail lourd de calcul de similarité vectorielle.
Création d'un index dans Qdrant
La création d'un index de charge utile est simple via l'API Qdrant. Voici comment vous pouvez indexer un champ user_id à haute cardinalité dans une collection nommée docs :
import qdrant_client
client = qdrant_client.QdrantClient(host="localhost", port=6333)
# Créer un index pour le champ 'user_id'
# Utiliser 'integer' pour les ID numériques, 'keyword' pour les chaînes
client.create_payload_index(
collection_name="docs",
field_name="user_id",
field_schema="integer"
)
# Pour les champs à haute cardinalité basés sur des chaînes, utiliser keyword
client.create_payload_index(
collection_name="docs",
field_name="document_hash",
field_schema="keyword"
)
Meilleures pratiques pour les champs à haute cardinalité
Tous les champs ne nécessitent pas la même stratégie d'indexation. Tenez compte des meilleures pratiques suivantes :
- Identifier la cardinalité : Avant l'indexation, estimez le nombre de valeurs uniques. Si un champ a une faible cardinalité (par ex., "status": ["active", "inactive"]), un filtre de charge utile standard est souvent suffisant et plus rapide en raison des coûts de maintenance d'index plus faibles. Réservez les index de charge utile pour les champs avec des milliers ou des millions de valeurs uniques.
- Utiliser le bon type de schéma : Spécifiez toujours le
field_schemacorrect. L'utilisation dekeywordpour du texte long ou des chaînes non structurées entraînera de mauvaises performances. Utilisezintegeroufloatpour les données numériques etdatetimepour le filtrage des séries temporelles. - Surveiller la taille de l'index : Les index de charge utile consomment un stockage supplémentaire. Pour les très grandes collections avec de nombreux champs à haute cardinalité, surveillez la taille totale de l'index pour vous assurer qu'elle reste gérable par rapport à votre infrastructure.
- Combiner avec des vecteurs creux : Dans les systèmes RAG complexes, la combinaison de la recherche vectorielle dense avec la recherche vectorielle creuse (pour l'appariement de mots-clés) et le filtrage de charge utile peut parfois surpasser les approches purement denses + filtre, en particulier lorsque le filtre est extrêmement restrictif.
Exemple pratique : Filtrage par utilisateur et par temps
Imaginez une IA de support client qui stocke les vecteurs de l'historique des discussions. Vous devez récupérer les 5 interactions passées les plus similaires pour l'user_id 999 qui se sont produites au cours des 7 derniers jours.
Sans index, Qdrant chercherait des vecteurs similaires globalement et les filtrerait un par un. Avec des index sur user_id (entier) et timestamp (date-temps), Qdrant peut d'abord restreindre la portée de la recherche aux seuls vecteurs appartenant à l'utilisateur 999 qui tombent dans l'intervalle de temps, puis effectuer le classement de similarité uniquement sur cet ensemble réduit.
from qdrant_client import models
import time
# Définir la condition de filtre
filter_query = models.Filter(
must=[
models.FieldCondition(
key="user_id",
match=models.MatchValue(value=999)
),
models.FieldCondition(
key="timestamp",
range=models.Range(
gte=time.time() - (7 * 24 * 60 * 60), # Il y a 7 jours
lte=time.time()
)
)
]
)
# Exécuter la recherche
results = client.search(
collection_name="chat_history",
query_vector=[0.1, 0.2, 0.3, ...], # Votre embedding
query_filter=filter_query,
limit=5
)
# Les résultats sont désormais garantis d'être à la fois similaires et filtrés
for point in results:
print(f"Score: {point.score}, ID: {point.id}")
Conclusion
L'optimisation de la recherche vectorielle filtrée dans Qdrant ne consiste pas seulement à écrire la bonne requête ; il s'agit de préparer la structure de données pour soutenir efficacement cette requête. En identifiant les champs de métadonnées à haute cardinalité et en appliquant les index de charge utile appropriés, vous transformez des scans exhaustifs potentiellement lents en recherches ciblées et rapides. Pour les développeurs intermédiaires à avancés construisant des systèmes RAG de qualité production, la maîtrise de l'indexation de charge utile est essentielle pour offrir des expériences de récupération à faible latence et précises à grande échelle. Effectuez toujours le profilage de vos requêtes avec et sans index pour comprendre l'impact, et rappelez-vous que l'objectif est de minimiser l'espace de recherche avant le début du calcul de similarité vectorielle.