Le type de données QBit réorganise le stockage des vecteurs pour accélérer les recherches approximatives. Au lieu de stocker ensemble les éléments de chaque vecteur, il regroupe les mêmes positions de bits binaires pour l’ensemble des vecteurs.
Ce type stocke les vecteurs à pleine précision tout en vous permettant de choisir le niveau de quantification fine au moment de la recherche : lisez moins de bits pour réduire les E/S et accélérer les calculs, ou davantage de bits pour une précision accrue. Vous bénéficiez ainsi des gains de vitesse liés à la réduction des transferts de données et des calculs grâce à la quantification, tout en conservant l’accès aux données d’origine lorsque nécessaire.
Pour déclarer une colonne de type QBit, utilisez la syntaxe suivante :
column_name QBit(element_type, dimension[, stride])element_type– le type de chaque élément du vecteur. Les types autorisés sontInt8,BFloat16,Float32etFloat64dimension– le nombre d’éléments de chaque vecteurstride– facultatif. Le nombre de dimensions stockées ensemble dans un groupe de flux. S’il est omis, la valeur par défaut estdimension(un seul groupe). Lorsqu’il est spécifié,dimensiondoit être un multiple destrideet, lorsquestrideest inférieur àdimension,stridedoit être un multiple de 8. Lesdimensiondimensions sont réparties endimension / stridegroupes contigus, et les plans de bits de chaque groupe sont stockés dans des flux distincts. Cela permet, lors d’une recherche sur lesDpremières dimensions (oùDest un multiple destride), de ne lire que les flux des groupes couvrant ces dimensions, ce qui est utile pour les Matryoshka embeddings.
Création de QBit
Utilisation du type QBit dans la définition d’une colonne de la table :
CREATE TABLE test (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO test VALUES (1, [1, 2, 3, 4, 5, 6, 7, 8]), (2, [9, 10, 11, 12, 13, 14, 15, 16]);
SELECT vec FROM test ORDER BY id;┌─vec──────────────────────┐
│ [1,2,3,4,5,6,7,8] │
│ [9,10,11,12,13,14,15,16] │
└──────────────────────────┘Conversion de tableaux en QBit
Les tableaux sont convertis en QBit lorsque leur longueur correspond à la dimension de QBit. Le type des éléments du tableau n’a pas besoin de correspondre à celui de QBit. Tout type d’élément numérique y est converti automatiquement. Cela vous permet de transférer directement une colonne d’embeddings existante vers une colonne QBit :
CREATE TABLE embeddings (id UInt32, embedding Array(Float32)) ENGINE = Memory;
INSERT INTO embeddings VALUES (1, [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]), (2, [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1]);
CREATE TABLE vectors (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO vectors SELECT id, embedding FROM embeddings;
SELECT * FROM vectors ORDER BY id;┌─id─┬─vec───────────────────────────────┐
│ 1 │ [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8] │
│ 2 │ [0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1] │
└────┴───────────────────────────────────┘La conversion fonctionne également explicitement avec CAST, par exemple CAST(embedding AS QBit(Float32, 8)).
Conversion de QBit en tableaux
La conversion inverse reconstitue le vecteur d’origine à partir de la représentation transposée bit à bit. Ainsi, convertir un QBit en Array renvoie les valeurs stockées. C’est l’inverse de la conversion de tableaux en QBit :
SELECT [1, 2, 3, 4]::QBit(Float32, 4)::Array(Float32) AS vec;┌─vec───────┐
│ [1,2,3,4] │
└───────────┘Le tableau reconstruit utilise le type d’élément du QBit, puis ses éléments sont convertis dans le type d’élément du tableau demandé. Une conversion de type qui modifie également le type d’élément, comme de QBit(Float32, N) vers Array(Float64), fonctionne donc elle aussi.
Un aller-retour Array -> QBit -> Array est sans perte pour Int8, Float32 et Float64. Pour BFloat16, il correspond à une conversion directe en BFloat16 — la seule précision perdue est celle propre à BFloat16.
Lorsque la dimension n’est pas un multiple de 8, les éléments de remplissage de fin présents dans la représentation interne sont supprimés, de sorte que le résultat comporte toujours exactement dimension éléments.
Conversion entre les types QBit
Un QBit peut être converti dans un autre QBit tant que la dimension (le nombre d’éléments du vecteur) reste identique. Le element_type et le stride peuvent tous deux changer ; en revanche, une conversion vers un QBit ayant une dimension différente lève une exception, car cela modifierait le vecteur lui-même.
Modifier le element_type reconstruit le vecteur et convertit chaque élément vers le nouveau type, exactement comme pour la conversion Array correspondante : l’élargissement (par exemple de QBit(Float32, N) vers QBit(Float64, N)) est exact, tandis que le rétrécissement entraîne une perte de précision, comme avec une conversion Array restrictive.
SELECT [1, 2, 3, 4]::QBit(Float32, 4)::QBit(Float64, 4) AS vec;┌─vec───────┐
│ [1,2,3,4] │
└───────────┘Modifier uniquement le stride (en conservant le même element_type) regroupe différemment les plans de bits stockés sans toucher aux valeurs ; l'opération est donc toujours sans perte :
SELECT range(16)::Array(Float32)::QBit(Float32, 16)::QBit(Float32, 16, 8)::Array(Float32)
= range(16)::Array(Float32) AS is_lossless;┌─is_lossless─┐
│ 1 │
└─────────────┘Sous-colonnes QBit
QBit implémente un mécanisme d’accès par sous-colonnes qui vous permet d’accéder à chaque plan de bits des vecteurs stockés. Chaque position de bit est accessible à l’aide de la syntaxe .N, où N correspond à la position du bit :
CREATE TABLE test (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO test VALUES (1, [0, 0, 0, 0, 0, 0, 0, 0]);
INSERT INTO test VALUES (1, [-0, -0, -0, -0, -0, -0, -0, -0]);
SELECT bin(vec.1) FROM test;┌─bin(tupleElement(vec, 1))─┐
│ 00000000 │
│ 11111111 │
└───────────────────────────┘Le nombre de sous-colonnes accessibles dépend du type d’élément (et, en cas de découpage en strides, du nombre de groupes de stride) :
Int8: 8 sous-colonnes par groupe de stride (1-8)BFloat16: 16 sous-colonnes par groupe de stride (1-16)Float32: 32 sous-colonnes par groupe de stride (1-32)Float64: 64 sous-colonnes par groupe de stride (1-64)
Les sous-colonnes suivent un ordre par groupe : en général, vec.N lit le plan de bits (N-1) % element_size du groupe de stride (N-1) / element_size. Par exemple, avec QBit(BFloat16, 4096, 1024), les 4096 dimensions sont réparties en 4 groupes de 1024, il y a donc 64 sous-colonnes : vec.1 … vec.16 sont les plans de bits du premier groupe de stride (dimensions 1–1024), vec.17 … vec.32 appartiennent au deuxième groupe (dimensions 1025–2048), et ainsi de suite.
Strides
Par défaut, un QBit stocke chaque plan de bits dans un flux unique couvrant l’ensemble des dimensions dimension, de sorte qu’une recherche lit toujours les plans de bits complets sur tout le vecteur. Le paramètre facultatif stride partitionne les dimensions dimension en dimension / stride groupes contigus et stocke les plans de bits de chaque groupe dans des flux distincts. Cela permet à une recherche portant uniquement sur les D premières dimensions (où D est un multiple de stride) de ne lire que les flux des groupes couvrant ces dimensions — ce qui est utile pour les Matryoshka embeddings, où les premières dimensions forment un embedding exploitable de dimension inférieure.
CREATE TABLE test (id UInt32, vec QBit(BFloat16, 4096, 1024)) ENGINE = MergeTree ORDER BY id;Ici, les 4096 dimensions sont réparties en 4 groupes de 1024. Les sous-colonnes suivent un ordre par groupe : avec BFloat16 (16 plans de bits), vec.1 … vec.16 correspondent aux 16 plans de bits du premier groupe de stride (dimensions 1–1024), vec.17 … vec.32 appartiennent au deuxième groupe (dimensions 1025–2048), et ainsi de suite. De manière générale, vec.N lit le plan de bits (N-1) % element_size du groupe de stride (N-1) / element_size.
Pour exécuter une recherche à dimension réduite, indiquez le nombre de dimensions à lire comme quatrième argument des fonctions de distance transposées (voir ci-dessous). Le vecteur de référence doit contenir au moins ce nombre d’éléments (tout élément supplémentaire en fin de vecteur est ignoré), et cette valeur doit être un multiple de stride.
Accès aux éléments et découpage
arrayElement (et l’opérateur vec[n]) renvoie le n-ième élément du vecteur, reconstruit avec toute la pleine précision du type d’élément. Seuls les plans de bits du groupe de stride contenant l’élément sont lus. arraySlice renvoie un QBit sur les dimensions sélectionnées (une projection sur un sous-ensemble de dimensions) ; le décalage et la longueur doivent être des constantes, car la dimension d’un QBit fait partie de son type. Une tranche alignée sur les limites des groupes de stride conserve le stride et réutilise les flux de plans de bits stockés sans les copier :
CREATE TABLE test (id UInt32, vec QBit(Float32, 8)) ENGINE = Memory;
INSERT INTO test VALUES (1, [1, 2, 3, 4, 5, 6, 7, 8]);
SELECT vec[3], arraySlice(vec, 2, 3) FROM test;┌─arrayElement(vec, 3)─┬─arraySlice(vec, 2, 3)─┐
│ 3 │ [2,3,4] │
└──────────────────────┴───────────────────────┘Fonctions de recherche vectorielle
Voici les fonctions de distance pour la recherche de similarité vectorielle qui utilisent le type de données QBit :
Pour un QBit avec stride, ces fonctions acceptent un quatrième argument facultatif, used_dims — le nombre de premières dimensions à lire —, qui lit uniquement les groupes de stride couvrant ces dimensions. Le vecteur de référence doit comporter au moins used_dims éléments (tout élément supplémentaire en fin de vecteur est ignoré, de sorte qu'un vecteur de requête de taille complète peut être réutilisé pour une recherche à dimension réduite sans devoir le tronquer au préalable), et used_dims doit être un multiple de stride.