Saltar al contenido principal
En esta guía, aprenderás a:
  • Obtener una breve introducción a la búsqueda vectorial
  • Conocer vecinos más cercanos aproximados (ANN) y Hierarchical Navigable Small World (HNSW)
  • Conocer bit cuantizado (QBit)
  • Usar QBit para realizar búsquedas vectoriales con el conjunto de datos DBPedia

Introducción a la búsqueda vectorial

En matemáticas y física, un vector se define formalmente como un objeto que tiene magnitud y dirección. A menudo adopta la forma de un segmento de recta o una flecha en el espacio, y puede usarse para representar magnitudes como la velocidad, la fuerza y la aceleración. En informática, un vector es una secuencia finita de números. En otras palabras, es una estructura de datos que se utiliza para almacenar valores numéricos. En aprendizaje automático, los vectores son las mismas estructuras de datos de las que hablamos en informática, pero los valores numéricos que almacenan tienen un significado especial. Cuando tomamos un bloque de texto o una imagen y lo destilamos hasta quedarnos con los conceptos clave que representa, este proceso se denomina codificación. El resultado es la representación numérica que una máquina hace de esos conceptos clave. Esto es un embedding y se almacena en un vector. Dicho de otro modo, cuando ese significado contextual está incorporado en un vector, podemos referirnos a él como un embedding. La búsqueda vectorial está hoy en todas partes. Impulsa las recomendaciones musicales, la generación aumentada por recuperación (RAG) para modelos de lenguaje de gran tamaño, donde se recupera conocimiento externo para mejorar las respuestas, e incluso las búsquedas en Google se basan en cierta medida en la búsqueda vectorial. Los usuarios suelen preferir las bases de datos convencionales con capacidades vectoriales ad hoc a los almacenes vectoriales totalmente especializados, a pesar de las ventajas de estas bases de datos especializadas. ClickHouse admite la búsqueda vectorial por fuerza bruta, así como métodos de búsqueda aproximada del vecino más cercano (ANN), incluido HNSW, el estándar actual para la recuperación rápida de vectores.

Comprender los embeddings

Veamos un ejemplo sencillo para entender cómo funciona la búsqueda vectorial. Consideremos embeddings (representaciones vectoriales) de palabras: Crea la siguiente table con algunos embeddings de ejemplo:
Puedes buscar las palabras más similares a partir de un embedding dado:
El embedding de la consulta es el que está más cerca de “apple” (la menor distancia), lo cual tiene sentido si observamos ambos embeddings lado a lado:

Vecinos más cercanos aproximados (ANN)

Para conjuntos de datos grandes, la búsqueda por fuerza bruta se vuelve demasiado lenta. Aquí es donde resultan útiles los métodos de vecinos más cercanos aproximados.

Cuantización

La cuantización consiste en convertir a tipos numéricos de menor tamaño. Números más pequeños implican menos datos, y menos datos implican cálculos de distancia más rápidos. El motor de ejecución vectorizada de consultas de ClickHouse puede alojar más valores en los registros del procesador en cada operación, lo que aumenta directamente el rendimiento. Tienes dos opciones:
  1. Mantener la copia cuantizada junto a la columna original - Esto duplica el almacenamiento, pero es una opción segura porque siempre podemos volver a la precisión completa
  2. Reemplazar por completo los valores originales (convirtiéndolos a un tipo menor durante la inserción) - Esto ahorra espacio y E/S, pero no tiene vuelta atrás

Hierarchical Navigable Small World (HNSW)

HNSW se construye a partir de múltiples capas de nodos (vectores). Cada nodo se asigna aleatoriamente a una o más capas, y la probabilidad de aparecer en capas superiores disminuye exponencialmente. Al realizar una búsqueda, comenzamos en un nodo de la capa superior y avanzamos de forma voraz hacia los vecinos más cercanos. Cuando ya no se puede encontrar un nodo más cercano, descendemos a la siguiente capa, que es más densa. Gracias a este diseño por capas, HNSW alcanza una complejidad de búsqueda logarítmica con respecto al número de nodos.
Limitación de HNSWEl principal cuello de botella es la memoria. ClickHouse usa la implementación usearch de HNSW, que es una estructura de datos en memoria y no admite particionamiento. Como resultado, los conjuntos de datos más grandes requieren proporcionalmente más RAM.

Comparación de enfoques

Profundización en QBit

Bit cuantizado (QBit)

QBit es una nueva estructura de datos que puede almacenar valores BFloat16, Float32 y Float64 aprovechando la forma en que se representan los números de coma flotante: como bits. En lugar de almacenar cada número completo, QBit divide los valores en planos de bits: el primer bit de cada valor, el segundo, el tercero, y así sucesivamente. Este enfoque resuelve la principal limitación de la cuantización tradicional. No hace falta almacenar datos duplicados ni arriesgarse a que los valores pierdan significado. Además, evita los cuellos de botella de RAM de HNSW, ya que QBit trabaja directamente con los datos almacenados en lugar de mantener un índice en memoria.
BeneficioY, lo más importante, no requiere tomar decisiones por adelantado. La precisión y el rendimiento pueden ajustarse dinámicamente en tiempo de consulta, lo que permite a los usuarios explorar el equilibrio entre exactitud y velocidad con muy poca fricción.
LimitaciónAunque QBit acelera la búsqueda vectorial, su complejidad computacional sigue siendo O(n). En otras palabras: si tu conjunto de datos es lo bastante pequeño como para que un índice HNSW quepa cómodamente en RAM, esa sigue siendo la opción más rápida.

El tipo de dato

A continuación se muestra cómo crear una columna QBit:
Cuando los datos se insertan en una columna QBit, se transponen de modo que todos los primeros bits queden alineados, todos los segundos bits queden alineados, y así sucesivamente. A esto lo llamamos grupos. Cada grupo se almacena en una columna FixedString(N) independiente: cadenas de longitud fija de N bytes almacenadas consecutivamente en memoria, sin separadores entre ellas. Todos esos grupos se agrupan luego en una sola Tuple, que constituye la estructura subyacente de QBit. Ejemplo: Si partimos de un vector de 8 elementos Float64, cada grupo contendrá 8 bits. Como un Float64 tiene 64 bits, terminamos con 64 grupos (uno por cada bit). Por lo tanto, la disposición interna de QBit(Float64, 8) se asemeja a una Tuple de 64 columnas FixedString(1).
Si la longitud del vector original no es divisible exactamente por 8, la estructura se rellena con elementos invisibles para alinearla a 8. Esto garantiza la compatibilidad con FixedString, que opera estrictamente con bytes completos.

El cálculo de la distancia

Para realizar consultas con QBit, use la función L2DistanceTransposed con un parámetro de precisión:
El tercer parámetro (16) especifica el nivel de precisión, en bits.

Optimización de E/S

Antes de poder calcular distancias, es necesario leer del disco los datos requeridos y luego destransponerlos (es decir, volver de la representación de bits agrupados a vectores completos). Como QBit almacena los valores transpuestos por bits según el nivel de precisión, ClickHouse puede leer solo los planos de bits más altos necesarios para reconstruir números con la precisión deseada. En la consulta anterior, usamos un nivel de precisión de 16. Como un Float64 tiene 64 bits, solo leemos los primeros 16 planos de bits, omitiendo el 75 % de los datos. Después de la lectura, reconstruimos solo la parte más significativa de cada número a partir de los planos de bits cargados, dejando en cero los bits no leídos.

Optimización del cálculo

Cabe preguntarse si convertir a un tipo menor, como Float32 o BFloat16, podría eliminar esta parte no utilizada. Funciona, pero las conversiones explícitas resultan costosas cuando se aplican a cada fila. En su lugar, podemos reducir el tipo solo del vector de referencia y tratar los datos de QBit como si contuvieran valores de menor tamaño (“olvidando” la existencia de algunas columnas), ya que su disposición suele corresponder a una versión truncada de esos tipos.

Optimización de BFloat16

BFloat16 es un Float32 truncado a la mitad. Conserva el mismo bit de signo y el exponente de 8 bits, pero solo los 7 bits superiores de la mantisa de 23 bits. Por ello, leer los primeros 16 planos de bits de una columna QBit reproduce en la práctica la estructura de los valores BFloat16. Así que, en este caso, podemos (y de hecho lo hacemos) convertir con seguridad el vector de referencia a BFloat16.

Complejidad de Float64

Float64, sin embargo, es otra historia. Usa un exponente de 11 bits y una mantisa de 52 bits, lo que significa que no es simplemente un Float32 con el doble de bits. Su estructura y el sesgo del exponente son completamente distintos. Reducir un Float64 a un formato más pequeño, como Float32, requiere una conversión IEEE-754 propiamente dicha, en la que cada valor se redondea al Float32 representable más cercano. Este paso de redondeo es costoso desde el punto de vista computacional.
Si te interesa un análisis en profundidad de los aspectos de rendimiento de QBit, consulta “Let’s vectorize”

Ejemplo con DBpedia

Veamos QBit en acción con un ejemplo del mundo real que utiliza el conjunto de datos de DBpedia, que contiene 1 millón de artículos de Wikipedia representados como embeddings de Float32.

Configuración

Primero, crea la tabla
Inserte los datos desde la línea de comandos:
La inserción de los datos puede tardar un poco. ¡Es un buen momento para una pausa para el café!
Como alternativa, se pueden ejecutar Sentencias SQL individuales, como se muestra a continuación, para cargar cada uno de los 25 archivos Parquet:
Verifique que haya 1 millón de filas en la tabla dbpedia:
A continuación, añade una columna QBit:

Consulta de búsqueda

Buscaremos los conceptos más relacionados con estos términos de búsqueda sobre el espacio: Moon, Apollo 11, Space Shuttle, Astronaut, Rocket:
La consulta busca las 1000 entradas semánticamente más similares para cada uno de los cinco conceptos. Devuelve las entradas que aparecen en al menos tres de esos resultados, ordenadas por la cantidad de conceptos con los que coinciden y por su distancia mínima a cualquiera de ellos (excluyendo las originales). Usando solo 5 bits (1 de signo + 4 bits de exponente, mantisa cero):
Rendimiento: 10 filas en el resultado. Tiempo transcurrido: 0.271 s. Se procesaron 8.46 millones de filas, 4.54 GB (31.19 millones de filas/s, 16.75 GB/s). Uso máximo de memoria: 739.82 MiB.
Rendimiento: 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.

Idea clave

¿Los resultados? No solo buenos. Sorprendentemente buenos. No es obvio que los números de punto flotante, despojados de toda su mantisa y de la mitad de su exponente, sigan conservando información significativa. La idea clave detrás de QBit es que la búsqueda vectorial sigue funcionando si ignoramos los bits insignificantes. El uso de memoria se redujo de 6.05 GB a 740 MB manteniendo una excelente calidad de búsqueda semántica.

Conclusión

QBit es un tipo de columna que almacena valores de coma flotante como planos de bits. Te permite elegir cuántos bits leer durante la búsqueda vectorial, ajustando el recall y el rendimiento sin modificar los datos. Cada método de búsqueda vectorial tiene sus propios parámetros, que determinan el equilibrio entre recall, precisión y rendimiento. Normalmente, hay que elegirlos de antemano. Si te equivocas, se desperdician mucho tiempo y recursos, y cambiar de rumbo más adelante resulta difícil. Con QBit, no hace falta tomar decisiones por adelantado. Puedes ajustar directamente el equilibrio entre precisión y velocidad en tiempo de consulta, y encontrar el balance adecuado sobre la marcha.
Adaptado de la entrada del blog de Raufs Dunamalijevs, publicada el 28 de octubre de 2025
Última modificación el 19 de junio de 2026