- La recherche vectorielle exacte calcule la distance entre le point donné et tous les points de l’espace vectoriel. Cela garantit la meilleure précision possible, c’est-à-dire que les points renvoyés sont effectivement les véritables plus proches voisins. Comme l’espace vectoriel est exploré de façon exhaustive, la recherche vectorielle exacte peut être trop lente pour un usage réel.
- La recherche vectorielle approximative désigne un ensemble de techniques (par exemple, des structures de données spécialisées comme les graphes et les forêts aléatoires) qui calculent les résultats bien plus rapidement que la recherche vectorielle exacte. La précision obtenue est généralement “suffisamment bonne” pour un usage pratique. Bon nombre de techniques approximatives proposent des paramètres permettant d’ajuster le compromis entre la précision des résultats et le temps de recherche.
vectors de type tableau, par exemple Array(Float64), Array(Float32) ou Array(BFloat16).
Le vecteur de référence est un tableau constant, défini comme une expression de table commune.
<DistanceFunction> calcule la distance entre le point de référence et tous les points stockés.
N’importe laquelle des fonctions de distance disponibles peut être utilisée à cette fin.
<N> indique le nombre de voisins à renvoyer.
Recherche vectorielle exacte
Exemple
recherche vectorielle approximative
Index de similarité vectorielle
Les index de similarité vectorielle sont disponibles dans ClickHouse version 25.8 et les versions ultérieures.
Si vous rencontrez des problèmes, veuillez ouvrir une issue dans le dépôt ClickHouse.
Création d’un index de similarité vectorielle
ALTER TABLE ci-dessus ne construit l’index que pour les nouvelles données qui seront insérées dans la table.
Pour construire également l’index pour les données existantes, vous devez le matérialiser :
<distance_function> doit être
L2Distance, la distance euclidienne, qui représente la longueur du segment entre deux points dans l’espace euclidien,cosineDistance, la distance cosinus, qui représente l’angle entre deux vecteurs non nuls, oudotProduct, le produit scalaire (produit intérieur), qui représente la somme des produits élément par élément de deux vecteurs. Équivalent àcosineDistancesur des données normalisées.
L2Distance est généralement le meilleur choix ; sinon, cosineDistance est recommandé pour compenser les différences d’échelle.
Pour les fonctions de distance
L2Distance et cosineDistance, une valeur plus faible indique une similarité plus élevée, tandis que pour dotProduct, une valeur plus élevée indique une similarité plus élevée.
Par conséquent, les index vectoriels avec L2Distance et cosineDistance ne peuvent être utilisés que par des requêtes SELECT [...] ORDER BY [...] ASC (ASC est la valeur par défaut de ORDER BY), tandis que les index vectoriels construits pour dotProduct ne peuvent être utilisés que par des requêtes SELECT [...] ORDER BY [...] DESC.<dimensions> spécifie la cardinalité du tableau (nombre d’éléments) dans la colonne sous-jacente.
Si ClickHouse trouve un tableau avec une cardinalité différente lors de la création de l’index, l’index est abandonné et une erreur est renvoyée.
Le paramètre facultatif GRANULARITY <N> fait référence à la taille des granules d’index (voir ici).
Contrairement aux index de saut classiques, qui utilisent une granularité d’index par défaut de 1, les index de similarité vectorielle utilisent 100 millions comme granularité d’index par défaut.
Cette valeur garantit que seul un petit nombre d’index sont construits en interne, même pour de grandes parts.
Nous recommandons de ne modifier la granularité d’index qu’aux utilisateurs avancés qui comprennent les implications de ce qu’ils font (voir ci-dessous).
Les index de similarité vectorielle sont génériques, dans le sens où ils peuvent prendre en charge différentes méthodes de recherche approximative.
La méthode effectivement utilisée est spécifiée par le paramètre <type>.
À ce jour, la seule méthode disponible est HNSW (article académique), une technique populaire et de pointe de recherche vectorielle approximative basée sur des graphes de proximité hiérarchiques.
Si HNSW est utilisé comme type, les utilisateurs peuvent éventuellement spécifier des paramètres supplémentaires propres à HNSW :
<quantization>contrôle la quantification des vecteurs dans le graphe de proximité. Les valeurs possibles sontf64,f32,f16,bf16,i8oub1. La valeur par défaut estbf16. Notez que ce paramètre n’affecte pas la représentation des vecteurs dans la colonne source.<hnsw_max_connections_per_layer>contrôle le nombre de voisins par nœud du graphe, également appelé hyperparamètre HNSWM. La valeur par défaut est32. La valeur0signifie que la valeur par défaut est utilisée.<hnsw_candidate_list_size_for_construction>contrôle la taille de la liste dynamique de candidats lors de la construction du graphe HNSW, également appelée hyperparamètre HNSWef_construction. La valeur par défaut est128. La valeur0signifie que la valeur par défaut est utilisée.
- Les index de similarité vectorielle ne peuvent être construits que sur des colonnes de type Array(Float32), Array(Float64) ou Array(BFloat16). Les tableaux de flottants nullable ou à faible cardinalité, tels que
Array(Nullable(Float32))etArray(LowCardinality(Float32)), ne sont pas autorisés. - Les index de similarité vectorielle doivent être construits sur une seule colonne.
- Les index de similarité vectorielle peuvent être construits sur des expressions calculées (par exemple,
INDEX index_name arraySort(vectors) TYPE vector_similarity([...])), mais ces index ne pourront pas être utilisés ensuite pour la recherche approximative de voisins. - Les index de similarité vectorielle exigent que tous les tableaux de la colonne source contiennent
<dimension>éléments ; cela est vérifié lors de la création de l’index. Pour détecter les violations de cette exigence le plus tôt possible, les utilisateurs peuvent ajouter une contrainte sur la colonne vectorielle, par exempleCONSTRAINT same_length CHECK length(vectors) = 256. - De même, les valeurs de tableau dans la colonne source ne doivent pas être vides (
[]) ni avoir la valeur par défaut (également[]).
Utilisation d’un index de similarité vectorielle
Pour utiliser les index de similarité vectorielle, le paramètre compatibility doit être défini sur
'' (la valeur par défaut), '25.1' ou une version ultérieure.SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>).
La valeur par défaut du paramètre, 256, convient à la grande majorité des cas d’usage.
Des valeurs plus élevées améliorent la précision au détriment des performances.
Si la requête peut utiliser un index de similarité vectorielle, ClickHouse vérifie que la valeur LIMIT <N> fournie dans les requêtes SELECT est dans des limites raisonnables.
Plus précisément, une erreur est renvoyée si <N> est supérieur à la valeur du paramètre max_limit_for_vector_search_queries, dont la valeur par défaut est 100.
Des valeurs LIMIT trop élevées peuvent ralentir les recherches et indiquent généralement une erreur d’utilisation.
Pour vérifier si une requête SELECT utilise un index de similarité vectorielle, vous pouvez la préfixer avec EXPLAIN indexes = 1.
Par exemple, interrogez
Skip ainsi que le nom et le type de l’index vectoriel (dans l’exemple, idx et vector_similarity).
Dans ce cas, l’index de similarité vectorielle a éliminé deux des quatre granules, soit 50 % des données.
Plus le nombre de granules pouvant être éliminés est important, plus l’utilisation de l’index est efficace.
Post-filtrage et pré-filtrage
Les utilisateurs peuvent éventuellement spécifier une clause WHERE avec des conditions de filtre supplémentaires pour la requête SELECT.
ClickHouse évaluera ces conditions de filtre selon une stratégie de post-filtrage ou de pré-filtrage.
En résumé, les deux stratégies déterminent l’ordre dans lequel les filtres sont évalués :
- Le post-filtrage signifie que l’index de similarité vectorielle est évalué en premier, puis que ClickHouse évalue le ou les filtres supplémentaires spécifiés dans la clause
WHERE. - Le pré-filtrage signifie que l’ordre d’évaluation des filtres est inverse.
- Le post-filtrage présente un problème général : il peut renvoyer moins de lignes que le nombre demandé dans la clause
LIMIT <N>. Cette situation se produit lorsqu’une ou plusieurs lignes de résultat renvoyées par l’index de similarité vectorielle ne satisfont pas les filtres supplémentaires. - Le pré-filtrage est généralement un problème non résolu. Certaines bases de données vectorielles spécialisées proposent des algorithmes de pré-filtrage, mais la plupart des bases de données relationnelles (y compris ClickHouse) reviennent à une recherche exacte des voisins, c’est-à-dire à un balayage brute-force sans index.
year et la requête suivante est exécutée :
- la condition de filtre élimine au moins une ligne dans une partie, ClickHouse basculera vers le préfiltrage pour les plages « survivantes » au sein de la partie,
- la condition de filtre n’élimine aucune ligne dans une partie, ClickHouse effectuera un post-filtrage pour la partie.
auto, qui implémente les heuristiques ci-dessus) peut être défini sur prefilter.
Cela est utile pour forcer le préfiltrage lorsque les conditions de filtre supplémentaires sont extrêmement sélectives.
Par exemple, la requête suivante peut bénéficier du préfiltrage :
SETTINGS vector_search_filter_strategy = 'prefilter' à la requête), ClickHouse trouve d’abord tous les livres dont le prix est inférieur à 2 dollars, puis exécute une recherche vectorielle brute-force sur les livres trouvés.
Autre approche pour résoudre le problème ci-dessus : configurer le paramètre vector_search_index_fetch_multiplier (par défaut : 1.0, maximum : 1000.0) sur une valeur > 1.0 (par exemple, 2.0).
Le nombre de plus proches voisins récupérés depuis l’index vectoriel est multiplié par la valeur du paramètre, puis le filtre supplémentaire est appliqué à ces lignes afin de renvoyer jusqu’à LIMIT lignes.
Par exemple, nous pouvons exécuter à nouveau la requête, mais avec le multiplicateur 3.0 :
vector_search_index_fetch_multiplier peut atténuer ce problème, mais dans des cas extrêmes (condition WHERE très sélective), il reste possible que moins de N lignes demandées soient renvoyées.
Réévaluation du score
Les skip indexes dans ClickHouse filtrent généralement au niveau de la granule, c.-à-d. qu’une recherche dans un skip index renvoie (en interne) une liste de granules potentiellement correspondantes, ce qui réduit la quantité de données lues lors de l’analyse qui suit.
Cela fonctionne bien pour les skip indexes en général, mais dans le cas des index de similarité vectorielle, cela crée un “décalage de granularité”.
Plus précisément, l’index de similarité vectorielle détermine les numéros de ligne des N vecteurs les plus similaires pour un vecteur de référence donné, mais il doit ensuite extrapoler ces numéros de ligne en numéros de granule.
ClickHouse charge alors ces granules depuis le disque, puis répète le calcul de distance pour tous les vecteurs qu’elles contiennent.
Cette étape est appelée réévaluation et, bien qu’elle puisse théoriquement améliorer la précision — rappelez-vous que l’index de similarité vectorielle ne renvoie qu’un résultat approximatif —, elle n’est évidemment pas optimale en termes de performances.
ClickHouse propose donc une optimisation qui désactive la réévaluation et renvoie directement depuis l’index les vecteurs les plus similaires ainsi que leurs distances.
Cette optimisation est activée par défaut, voir le paramètre vector_search_with_rescoring.
Dans les grandes lignes, son fonctionnement est le suivant : ClickHouse met à disposition les vecteurs les plus similaires et leurs distances sous la forme d’une colonne virtuelle _distances.
Pour le constater, exécutez une requête de recherche vectorielle avec EXPLAIN header = 1 :
Une requête exécutée sans réévaluation (
vector_search_with_rescoring = 0) et avec les réplicas parallèles activés peut revenir à la réévaluation.Optimisation des performances
CODEC(NONE) pour la colonne vectorielle comme ceci :
system.text_log) indiquent que l’index de similarité vectorielle est en cours de chargement.
Si de tels messages apparaissent de façon répétée pour différentes requêtes de recherche vectorielle, cela indique que la taille du cache est trop faible.
Le cache de l’index de similarité vectorielle stocke des granules d’index vectoriel.
Si la taille de chaque granule d’index vectoriel dépasse celle du cache, elle ne sera pas mise en cache.
Veillez donc à calculer la taille de l’index vectoriel (à partir de la formule indiquée dans “Estimation de la consommation du stockage et de la mémoire” ou system.data_skipping_indices) et à dimensionner le cache en conséquence.
La quantification réduit la précision des recherches vectorielles par rapport à une recherche sur les valeurs d’origine en virgule flottante en pleine précision (
f32).
Cependant, sur la plupart des jeux de données, la quantification en brain float demi-précision (bf16) entraîne une perte de précision négligeable ; c’est pourquoi les index de similarité vectorielle utilisent cette technique par défaut.
La quantification en quart de précision (i8) et la quantification binaire (b1) entraînent une perte de précision notable dans les recherches vectorielles.
Nous ne recommandons ces deux quantifications que si la taille de l’index de similarité vectorielle dépasse nettement la taille de DRAM disponible.
Dans ce cas, nous suggérons également d’activer le rescoring (vector_search_index_fetch_multiplier, vector_search_with_rescoring) afin d’améliorer la précision.
La quantification binaire n’est recommandée que 1) pour des embeddings normalisés (c.-à-d. longueur du vecteur = 1, les modèles OpenAI sont généralement normalisés), et 2) si la distance cosinus est utilisée comme fonction de distance.
En interne, la quantification binaire utilise la distance de Hamming pour construire et parcourir le graphe de proximité.
L’étape de rescoring utilise les vecteurs d’origine en pleine précision stockés dans la table pour identifier les plus proches voisins via la distance cosinus.
Réglage du transfert de données
Le vecteur de référence dans une requête de recherche vectorielle est fourni par l’utilisateur et est généralement obtenu via un appel à un Large Language Model (LLM).
Voici à quoi pourrait ressembler un code Python typique exécutant une recherche vectorielle dans ClickHouse
search_v dans l’extrait ci-dessus) peuvent avoir un très grand nombre de dimensions.
Par exemple, OpenAI fournit des modèles qui génèrent des vecteurs d’embedding à 1536, voire 3072 dimensions.
Dans le code ci-dessus, le driver Python de ClickHouse remplace le vecteur d’embedding par une chaîne lisible, puis envoie la requête SELECT entièrement sous forme de chaîne.
En supposant que le vecteur d’embedding se compose de 1536 valeurs en virgule flottante simple précision, la chaîne envoyée atteint une longueur de 20 kB.
Cela entraîne une forte utilisation du CPU pour la tokenisation, l’analyse syntaxique et l’exécution de milliers de conversions de chaînes en nombres à virgule flottante.
En outre, un espace important est requis dans le fichier journal du serveur ClickHouse, ce qui entraîne également un gonflement de system.query_log.
Notez que la plupart des modèles de LLM renvoient un vecteur d’embedding sous la forme d’une liste ou d’un tableau NumPy de flottants natifs.
Nous recommandons donc aux applications Python de lier le paramètre du vecteur de référence sous forme binaire en utilisant le style suivant :
system.query_log.
Administration et surveillance
Différences par rapport aux index de saut classiques
GRANULARITY = [N] granules ([N] = 1 par défaut pour les index de saut classiques).
Par exemple, si la granularité de l’index primaire de la table est de 8192 (paramètre index_granularity = 8192) et que GRANULARITY = 2, alors chaque bloc indexé contiendra 16384 lignes.
Cependant, les structures de données et les algorithmes de recherche approximative de voisins sont intrinsèquement orientés lignes.
Ils stockent une représentation compacte d’un ensemble de lignes et renvoient également des lignes pour les requêtes de recherche vectorielle.
Cela entraîne des différences parfois peu intuitives dans le comportement des index de similarité vectorielle par rapport aux index de saut classiques.
Lorsqu’un utilisateur définit un index de similarité vectorielle sur une colonne, ClickHouse crée en interne un « sous-index » de similarité vectorielle pour chaque bloc d’index.
Le sous-index est « local » en ce sens qu’il ne connaît que les lignes du bloc d’index auquel il appartient.
Dans l’exemple précédent, en supposant qu’une colonne comporte 65536 lignes, on obtient quatre blocs d’index (couvrant huit granules) et un sous-index de similarité vectorielle pour chaque bloc d’index.
En théorie, un sous-index peut renvoyer directement les lignes contenant les N points les plus proches dans son bloc d’index.
Cependant, comme ClickHouse charge les données du disque en mémoire à la granularité des granules, les sous-index extrapolent les lignes correspondantes à cette granularité.
Cela diffère des index de saut classiques, qui sautent des données à la granularité des blocs d’index.
Le paramètre GRANULARITY détermine combien de sous-index de similarité vectorielle sont créés.
Des valeurs GRANULARITY plus élevées signifient des sous-index de similarité vectorielle moins nombreux, mais plus grands, jusqu’au point où une colonne (ou une data part de colonne) ne possède plus qu’un seul sous-index.
Dans ce cas, le sous-index a une vue « globale » de toutes les lignes de la colonne et peut renvoyer directement tous les granules de la colonne (part) contenant des lignes pertinentes (il y a au plus LIMIT [N] granules de ce type).
Dans un second temps, ClickHouse chargera ces granules et identifiera les meilleures lignes réelles en effectuant un calcul de distance en brute-force sur toutes les lignes de ces granules.
Avec une petite valeur de GRANULARITY, chacun des sous-index renvoie jusqu’à LIMIT N granules.
Par conséquent, davantage de granules doivent être chargés puis post-filtrés.
Notez que, dans les deux cas, la précision de la recherche est équivalente ; seule la performance de traitement diffère.
Il est généralement recommandé d’utiliser une valeur élevée de GRANULARITY pour les index de similarité vectorielle et de revenir à des valeurs plus faibles uniquement en cas de problèmes, comme une consommation mémoire excessive des structures de similarité vectorielle.
Si aucune valeur de GRANULARITY n’a été spécifiée pour les index de similarité vectorielle, la valeur par défaut est de 100 millions.
Exemple
Query
Response
Quantized Bit (QBit)
Array(BFloat16) au lieu de Array(Float32), la taille des données est réduite de moitié, et le temps d’exécution des requêtes devrait diminuer dans les mêmes proportions.
Cette méthode est appelée quantification. Bien qu’elle accélère les calculs, elle peut réduire la précision des résultats malgré un balayage exhaustif de tous les vecteurs.
Avec la quantification traditionnelle, on perd en précision à la fois lors de la recherche et lors du stockage des données. Dans l’exemple ci-dessus, on stockerait BFloat16 au lieu de Float32, ce qui signifie qu’il ne serait ensuite plus possible d’effectuer une recherche plus précise, même si on le souhaitait. Une autre approche consiste à stocker deux copies des données : une quantifiée et une en pleine précision. Bien que cela fonctionne, cela nécessite un stockage redondant. Prenons un scénario où Float64 est le format de données d’origine et où l’on souhaite exécuter des recherches avec différents niveaux de précision (16 bits, 32 bits ou 64 bits complets). Il faudrait alors stocker trois copies distinctes des données.
ClickHouse propose le type de données Quantized Bit (QBit), qui répond à ces limites en :
- Stockant les données d’origine en pleine précision.
- Permettant de spécifier la précision de quantification au moment de la requête.
QBit, utilisez la syntaxe suivante :
element_type– le type de chaque élément du vecteur. Les types pris en charge sontBFloat16,Float32etFloat64dimension– le nombre d’éléments de chaque vecteur
Création d’une table QBit et ajout de données
Recherche vectorielle avec QBit
QBit.
Recherche à pleine précision (64 bits) :
Considérations relatives aux performances
QBit vient de la réduction des opérations d’E/S, car moins de données doivent être lues depuis le stockage lorsqu’on utilise une précision plus faible. De plus, lorsque QBit contient des données Float32, si le paramètre de précision est inférieur ou égal à 16, la réduction des calculs apporte aussi des gains supplémentaires. Le paramètre de précision contrôle directement le compromis entre précision et vitesse :
- Précision plus élevée (plus proche de la largeur des données d’origine) : résultats plus précis, requêtes plus lentes
- Précision plus faible : requêtes plus rapides avec des résultats approximatifs, utilisation de la mémoire réduite