Leçons tirées de la gestion d'une base de données de six milliards de vecteurs, y compris les défis, les insights et les conseils d'optimisation.
La recherche vectorielle représente une véritable révolution dans le domaine des moteurs de recherche. Grâce aux techniques d’intelligence artificielle les plus avancées, ces moteurs sont désormais capables de comprendre le sens des mots et d’effectuer des recherches plus précises, surpassant les limites de la recherche lexicale, basée uniquement sur des mots-clés.
Cependant, bien que la recherche vectorielle soit présentée comme une solution universelle aux failles et limites des moteurs de recherche traditionnels, comme la mise en œuvre fastidieuse de synonymes ou la gestion du multilinguisme, elle a un coût : celui de la création, de la gestion et de la recherche dans des vecteurs, en particulier lorsqu’il s’agit de dizaines ou de centaines de millions.
Dans cet article, nous allons explorer en profondeur la recherche vectorielle à grande échelle, en examinant les défis liés à la gestion d’environ six milliards de vecteurs. Mais d’abord, si vous voulez en savoir plus sur la recherche vectorielle et son fonctionnement, vous pouvez consulter cet article : Comprendre les différences entre les vecteurs sémantiques denses et épars !
Les vecteurs denses sont des représentations numériques d’entités hétérogènes comme du texte, des images ou de l’audio. Ils sont généralement générés à l’aide de modèles d’apprentissage automatique basés sur des architectures de type transformeur. Chaque dimension du vecteur peut nécessiter jusqu'à 8 octets, comme dans le cas d’un float64. Les modèles légers produisent souvent des vecteurs avec 324 dimensions, tandis que les modèles plus complexes peuvent aller jusqu'à 8192 dimensions.
Naturellement, plus le nombre de dimensions est élevé, mieux le vecteur peut capturer la sémantique. Cependant, cette amélioration se fait au prix d’une augmentation significative des besoins en mémoire.
Faisons un peu de calcul ici.
Supposons que nous souhaitions construire un moteur de recherche basé sur une base de données vectorielle composée de 1 milliard de vecteurs.
Pour simplifier, supposons que chaque vecteur corresponde à un document. En réalité, un seul document peut être représenté par plusieurs vecteurs, soit en raison de sa structure interne, soit à cause du découpage en morceaux (chunking).
Pour faciliter la recherche sémantique et l’optimiser pour notre cas d’utilisation, Elasticsearch utilise l’algorithme HNSW (Hierarchical Navigable Small World), qui offre un excellent équilibre entre vitesse d’exécution et qualité des résultats de recherche.
La RAM requise pour un type de données float
peut être calculée à l’aide de la formule suivante :
RAM = nombre de vecteurs * nombre de dimensions * 4
Si le nombre de dimensions est le standard 1024, la RAM requise sera :
RAM = 1,000,000,000 * 1024 * 4
Décomposons :
Ce qui donne :
RAM = 4,096,000,000,000 octets = 4000 Go
Nous devons également tenir compte de la mémoire nécessaire pour construire le graphe HNSW, qui peut être calculée à l’aide de la formule :
RAM = nombre de vecteurs * 4 * HNSW.m
Où HNSW.m
représente le nombre de connexions que chaque nœud peut avoir dans le graphe HNSW. Ce paramètre peut être configuré dans Elasticsearch lors de la création de l’index. Par défaut, sa valeur est fixée à 16.
Un nombre plus élevé de connexions entraîne un graphe plus dense et plus précis, mais il est plus lent à parcourir et nécessite plus de mémoire.
Par exemple, avec 1 milliard de vecteurs, la RAM requise pour le graphe HNSW est :
RAM = 1,000,000,000 * 4 * 16
Cela nécessite 64 Go de RAM pour charger l’intégralité du HNSW.
Pour résumer, pour effectuer une recherche sémantique sur une base de données vectorielle de 1 milliard de vecteurs avec 1024 dimensions et un type d’élément float32
, 4064 Go de RAM seraient nécessaires pour des performances optimales. Cela dit, il est important de se rappeler que la mémoire utilisée pour ce type d’opération est hors tas (off-heap).
En général, la configuration la plus courante pour un nœud de données est 64 Go de RAM, dont la moitié est dédiée à la JVM, afin de pouvoir utiliser des optimisations sur les OOPs (Optimized Object Pointers).
À ce stade, si nous devions calculer approximativement le nombre de nœuds nécessaires pour effectuer une recherche sémantique :
Nodes = 4064 / 32
Cela donnerait un cluster de 127 nœuds de données, auquel il faudrait ajouter un nœud maître, un nœud ML pour les calculs d’inférence intra-cluster, etc. Évidemment, un cluster de cette taille peut poser des défis significatifs en termes de coûts, en particulier lorsqu’il doit être répliqué sur plusieurs environnements (préproduction, développement, etc.).
Mais ne désespérez pas, la quantification vient à notre rescousse !
La quantification scalaire permet de minimiser l’utilisation de la mémoire en convertissant chaque type d'élément en une version plus “compacte”. La conversion peut se faire ainsi :
Float32 -> Float16 -> Int8 -> Int4 -> Bbq (dans ce cas, nous considérons la quantification automatique d’Elasticsearch, qui utilise Lucene).
Bien entendu, la quantification se fait au détriment de la qualité. Il est toujours important d'évaluer si la perte causée par la quantification est acceptable en termes de pertinence des résultats de recherche. Mais qu’est-ce que cela signifie en termes d'économie de mémoire ?
Prenons la quantification Int8 comme exemple. Dans ce cas, la RAM requise pour 1 milliard de vecteurs peut être calculée comme suit :
RAM = nombre de vecteurs * (nombre de dimensions + 4)
Ainsi, pour 1 milliard de vecteurs avec 1024 dimensions, la mémoire requise sera d’environ 1 Téraoctet.
Dans ce cas, la perte de qualité est minimale, et en même temps, nous n’aurions besoin que de 32 nœuds de données avec 64 Go de RAM chacun pour exécuter correctement la recherche sémantique. La quantification BBQ nous permet d’atteindre cet objectif avec encore moins de ressources.
Voici les formules pour calculer la mémoire nécessaire :
element_type: float :
num_vectors * num_dimensions * 4
element_type: float avec quantification int8 :
num_vectors * (num_dimensions + 4)
element_type: float avec quantification int4 :
num_vectors * (num_dimensions / 2 + 4)
element_type: float avec quantification BBQ :
num_vectors * (num_dimensions / 8 + 12)
N’oubliez pas d’ajouter la mémoire nécessaire pour HNSW, comme nous l’avons vu précédemment, afin de calculer la mémoire totale requise pour votre configuration de recherche sémantique.
La mémoire sur disque est une question plus complexe. Mais essayons de clarifier.
Le vecteur est enregistré dans sa version Float32 à l’intérieur d’une structure de données spéciale dans Lucene appelée knn_vector
. La version originale du vecteur est également stockée dans _source
. Cependant, pour économiser de l’espace, il est possible d’exclure cela de _source
.
Pour ce faire, ajoutez simplement l’exclusion à votre mapping comme suit :
"mappings": {
"_source": {
"excludes": [
"your vector field"
]
}
//Le reste de votre configuration de mapping
En procédant ainsi, pour 1 milliard de vecteurs, vous pouvez économiser environ 4 To. Tout semble parfait… mais pas tout à fait.
Par défaut, si nous choisissons la quantification automatique effectuée par le moteur Lucene, Elasticsearch stockera non seulement le vecteur quantifié dans l’objet knn_vector
, mais également la version Float32 du vecteur. À partir de la version 8.17, il n’est pas possible d’exclure complètement le vecteur non quantifié.
Une solution possible serait de quantifier le vecteur en dehors d’Elasticsearch et d’indexer la version Int8 en modifiant le element_type
.
Comme nous l’avons vu tout au long de cet article, bien que la recherche vectorielle offre des capacités puissantes, notamment en termes de recherche sémantique, elle comporte son lot de défis, en particulier lorsqu’il s’agit de gérer des milliards de vecteurs. De la compréhension des besoins en mémoire à la gestion efficace du stockage sur disque, nous avons exploré l’importance de bien configurer votre infrastructure pour garantir des performances optimales.
La taille des vecteurs, le nombre de dimensions et le choix des techniques de quantification impactent considérablement l’utilisation de la mémoire RAM et disque. En utilisant la quantification—telle que Int8 ou BBQ—vous pouvez réduire considérablement la consommation de mémoire tout en maintenant un niveau de qualité acceptable dans les résultats de recherche.
De plus, l’optimisation de la mémoire sur disque en excluant les vecteurs non quantifiés peut également contribuer à des économies d’espace, bien que certaines limitations du comportement par défaut d’Elasticsearch nécessitent des solutions de contournement. En termes d’infrastructure, comme le montre notre exemple, un système de recherche sémantique à grande échelle peut toujours être géré avec relativement moins de ressources si la quantification est appliquée efficacement.
En résumé, bien que la construction et la maintenance d’un moteur de recherche sémantique à grande échelle utilisant des vecteurs puissent sembler complexes en raison des besoins substantiels en ressources, la compréhension des subtilités de la gestion de la mémoire et l’utilisation des techniques d’optimisation telles que la quantification peuvent réduire drastiquement les coûts et rendre cela réalisable même pour des ensembles de données volumineux. Gardez à l’esprit que l’équilibre entre efficacité mémoire et qualité des résultats est essentiel pour construire une solution de recherche efficace à grande échelle.