Skip to main content
Dans ce guide, vous allez :
  • Découvrir brièvement la recherche vectorielle
  • Découvrir les plus proches voisins approximatifs (ANN) et Hierarchical Navigable Small World (HNSW)
  • Découvrir Quantised Bit (QBit)
  • Utiliser QBit pour effectuer une recherche vectorielle à l’aide du jeu de données DBPedia

Introduction à la recherche vectorielle

En mathématiques et en physique, un vecteur est formellement défini comme un objet qui possède à la fois une norme et une direction. Il prend souvent la forme d’un segment de droite ou d’une flèche dans l’espace et peut servir à représenter des grandeurs telles que la vitesse, la force ou l’accélération. En informatique, un vecteur est une séquence finie de nombres. Autrement dit, c’est une structure de données utilisée pour stocker des valeurs numériques. En apprentissage automatique, les vecteurs sont les mêmes structures de données que celles dont on parle en informatique, mais les valeurs numériques qu’ils contiennent ont une signification particulière. Lorsque nous prenons un bloc de texte ou une image pour en extraire les concepts clés qu’il représente, ce processus s’appelle l’encodage. Le résultat obtenu est une représentation numérique, par la machine, de ces concepts clés. C’est un embedding, stocké dans un vecteur. Autrement dit, lorsque ce sens contextuel est encapsulé dans un vecteur, on peut parler d’embedding. La recherche vectorielle est aujourd’hui partout. Elle alimente les recommandations musicales, la génération augmentée par récupération (RAG) pour les grands modèles de langage, où des connaissances externes sont récupérées pour améliorer les réponses, et même la recherche sur Google repose dans une certaine mesure sur la recherche vectorielle. Les utilisateurs préfèrent souvent les bases de données généralistes dotées de capacités vectorielles ad hoc aux bases vectorielles entièrement spécialisées, malgré les avantages de ces dernières. ClickHouse prend en charge la recherche vectorielle brute-force ainsi que des méthodes de recherche approximative des plus proches voisins (ANN), notamment HNSW — la norme actuelle pour la recherche rapide de vecteurs.

Comprendre les embeddings

Examinons un exemple simple pour comprendre le fonctionnement de la recherche vectorielle. Considérons les embeddings (représentations vectorielles) de mots : Créez la table ci-dessous avec quelques exemples d’embeddings :
Vous pouvez rechercher les mots les plus proches d’un embedding donné :
L’embedding de la requête est le plus proche de “apple” (distance la plus faible), ce qui est logique si l’on compare les deux embeddings côte à côte :

Plus proches voisins approximatifs (ANN)

Pour les grands jeux de données, la recherche exhaustive devient trop lente. C’est là qu’interviennent les méthodes de recherche des plus proches voisins approximatifs.

Quantisation

La quantisation consiste à convertir des valeurs vers des types numériques plus petits. Des nombres plus petits signifient moins de données, et moins de données signifient des calculs de distance plus rapides. Le moteur d’exécution vectorisée des requêtes de ClickHouse peut faire tenir davantage de valeurs dans les registres du processeur à chaque opération, ce qui augmente directement le débit. Vous avez deux options :
  1. Conserver la copie quantifiée à côté de la colonne d’origine - Cela double l’espace de stockage, mais c’est sans risque, car nous pouvons toujours revenir à la pleine précision
  2. Remplacer entièrement les valeurs d’origine (en les convertissant vers un type inférieur lors de l’insertion) - Cela permet d’économiser de l’espace et des E/S, mais c’est irréversible

Hierarchical Navigable Small World (HNSW)

HNSW est constitué de plusieurs couches de nœuds (vecteurs). Chaque nœud est attribué aléatoirement à une ou plusieurs couches, avec une probabilité d’apparaître dans les couches supérieures qui décroît de façon exponentielle. Lors d’une recherche, nous partons d’un nœud de la couche supérieure et progressons de façon gloutonne vers les voisins les plus proches. Lorsqu’il n’est plus possible de trouver de nœud plus proche, nous descendons à la couche suivante, plus dense. Grâce à cette structure en couches, HNSW atteint une complexité de recherche logarithmique par rapport au nombre de nœuds.
Limitation de HNSWLe principal goulot d’étranglement est la mémoire. ClickHouse utilise l’implémentation HNSW de usearch, une structure de données en mémoire qui ne peut pas être scindée. Par conséquent, les jeux de données les plus volumineux nécessitent proportionnellement plus de RAM.

Comparaison des approches

QBit en profondeur

Quantised Bit (QBit)

QBit est une nouvelle structure de données capable de stocker des valeurs BFloat16, Float32 et Float64 en tirant parti de la manière dont les nombres à virgule flottante sont représentés : sous forme de bits. Au lieu de stocker chaque nombre dans son intégralité, QBit répartit les valeurs en plans de bits : tous les premiers bits, tous les deuxièmes bits, tous les troisièmes bits, et ainsi de suite. Cette approche résout la principale limite de la quantification traditionnelle. Il n’est pas nécessaire de stocker des données dupliquées ni de risquer de rendre les valeurs dénuées de sens. Elle évite également les goulots d’étranglement liés à la RAM de HNSW, puisque QBit fonctionne directement sur les données stockées au lieu de maintenir un index en mémoire.
AvantageSurtout, aucune décision préalable n’est nécessaire. La précision et les performances peuvent être ajustées dynamiquement à l’exécution de la requête, ce qui permet aux utilisateurs d’explorer le compromis entre exactitude et rapidité avec un minimum d’effort.
LimitationBien que QBit accélère la recherche vectorielle, sa complexité de calcul reste O(n). En d’autres termes : si votre jeu de données est suffisamment petit pour qu’un index HNSW tienne facilement en RAM, cela reste le choix le plus rapide.

Le type de données

Voici comment créer une colonne QBit :
Lorsque des données sont insérées dans une colonne QBit, elles sont transposées de manière à ce que tous les premiers bits soient alignés ensemble, puis tous les deuxièmes bits, et ainsi de suite. On appelle cela des groupes. Chaque groupe est stocké dans une colonne FixedString(N) distincte : des chaînes de longueur fixe de N octets, stockées consécutivement en mémoire, sans séparateur entre elles. Tous ces groupes sont ensuite réunis dans un unique Tuple, qui constitue la structure sous-jacente de QBit. Exemple : Si l’on part d’un vecteur de 8 éléments Float64, chaque groupe contiendra 8 bits. Comme un Float64 comporte 64 bits, on obtient au final 64 groupes (un pour chaque bit). L’organisation interne de QBit(Float64, 8) ressemble donc à un Tuple de 64 colonnes FixedString(1).
Si la longueur du vecteur d’origine n’est pas divisible par 8, la structure est complétée par des éléments invisibles afin de l’aligner sur 8. Cela garantit la compatibilité avec FixedString, qui fonctionne strictement sur des octets complets.

Calcul de la distance

Pour exécuter une query avec QBit, utilisez la fonction L2DistanceTransposed avec un paramètre de précision :
Le troisième paramètre (16) indique le niveau de précision en bits.

Optimisation des E/S

Avant de pouvoir calculer les distances, les données nécessaires doivent être lues depuis le disque, puis détransposées (reconverties de la représentation groupée des bits en vecteurs complets). Comme QBit stocke les valeurs transposées au niveau des bits par niveau de précision, ClickHouse peut ne lire que les plans de bits de poids fort nécessaires pour reconstruire les nombres jusqu’à la précision souhaitée. Dans la requête ci-dessus, nous utilisons un niveau de précision de 16. Comme un Float64 comporte 64 bits, nous ne lisons que les 16 premiers plans de bits, en évitant la lecture de 75 % des données. Après la lecture, nous ne reconstruisons que la partie de poids fort de chaque nombre à partir des plans de bits chargés, en laissant les bits non lus à zéro.

Optimisation des calculs

On peut se demander si une conversion vers un type plus petit, comme Float32 ou BFloat16, pourrait éliminer cette partie inutilisée. C’est possible, mais les conversions explicites coûtent cher lorsqu’elles sont appliquées à chaque ligne. À la place, nous pouvons réduire uniquement le type du vecteur de référence et traiter les données QBit comme si elles contenaient des valeurs codées sur moins de bits (« en faisant abstraction » de certaines colonnes), puisque leur agencement correspond souvent à une version tronquée de ces types.

Optimisation de BFloat16

BFloat16 est un Float32 tronqué de moitié. Il conserve le même bit de signe et le même exposant sur 8 bits, mais seulement les 7 bits de poids fort de la mantisse sur 23 bits. Ainsi, la lecture des 16 premiers plans de bits d’une colonne QBit reproduit effectivement la représentation des valeurs BFloat16. Dans ce cas, nous pouvons donc convertir le vecteur de référence en BFloat16 en toute sécurité, et c’est ce que nous faisons.

Complexité de Float64

Float64 est toutefois une tout autre histoire. Il utilise un exposant sur 11 bits et une mantisse sur 52 bits, ce qui signifie qu’il ne s’agit pas simplement d’un Float32 avec deux fois plus de bits. Sa structure et son biais d’exposant sont complètement différents. Convertir un Float64 vers un format plus petit comme Float32 nécessite une véritable conversion IEEE-754, dans laquelle chaque valeur est arrondie vers la valeur Float32 représentable la plus proche. Cette étape d’arrondi est coûteuse en calcul.
Si vous souhaitez approfondir les aspects de performance de QBit, consultez “Let’s vectorize”

Exemple avec DBpedia

Voyons QBit à l’œuvre à travers un exemple concret utilisant le jeu de données DBpedia, qui contient 1 million d’articles de Wikipédia représentés par des embeddings Float32.

Configuration

Commencez par créer la table
Insérez les données en ligne de commande :
L’insertion des données peut prendre un certain temps. C’est le moment de faire une pause café !
Vous pouvez également exécuter les instructions SQL individuellement, comme indiqué ci-dessous, pour charger chacun des 25 fichiers Parquet :
Vérifiez qu’un million de lignes sont présentes dans la table dbpedia :
Ensuite, ajoutez une colonne QBit :

Requête de recherche

Nous allons rechercher les concepts les plus étroitement liés à l’ensemble des termes de recherche relatifs à l’espace : Moon, Apollo 11, Space Shuttle, Astronaut, Rocket :
La requête recherche les 1 000 entrées les plus proches sémantiquement pour chacun des cinq concepts. Elle renvoie les entrées qui apparaissent dans au moins trois de ces résultats, classées selon le nombre de concepts auxquels elles correspondent et leur distance minimale à l’un d’eux (hors originaux). En utilisant seulement 5 bits (1 bit de signe + 4 bits d’exposant, mantisse nulle) :
Performances : 10 lignes dans le résultat. Temps écoulé : 0.271 s. 8.46 millions de lignes traitées, 4.54 GB (31.19 millions de lignes/s., 16.75 GB/s.) Utilisation mémoire maximale : 739.82 MiB.
Performance : 10 rows in set. Elapsed : 1.157 sec. Processed 10.00 million rows, 32.76 GB (8.64 million rows/s., 28.32 GB/s.) Peak memory usage : 6.05 GiB.

Idée clé

Les résultats ? Non seulement bons, mais étonnamment bons. Il n’est pas évident que des nombres à virgule flottante privés de toute leur mantisse et de la moitié de leur exposant conservent encore des informations significatives. L’idée clé derrière QBit, c’est que la recherche vectorielle fonctionne toujours, même si l’on ignore les bits insignifiants. L’utilisation mémoire a été réduite de 6.05 GB à 740 MB tout en conservant une excellente qualité de recherche sémantique !

Conclusion

QBit est un type de colonne qui stocke les nombres à virgule flottante sous forme de plans de bits. Il vous permet de choisir combien de bits lire lors d’une recherche vectorielle, afin d’ajuster le rappel et les performances sans modifier les données. Chaque méthode de recherche vectorielle a ses propres paramètres, qui déterminent les compromis entre rappel, précision et performances. Normalement, ils doivent être définis à l’avance. Si vous vous trompez, beaucoup de temps et de ressources sont gaspillés, et il devient difficile de changer d’approche par la suite. Avec QBit, il n’est pas nécessaire de prendre ces décisions en amont. Vous pouvez ajuster directement, au moment de la requête, le compromis entre précision et vitesse, et trouver progressivement le bon équilibre.
Adapté de l’article de blog de Raufs Dunamalijevs, publié le 28 octobre 2025
Dernière modification le 3 juillet 2026