- La búsqueda vectorial exacta calcula la distancia entre el punto dado y todos los puntos del espacio vectorial. Esto garantiza la máxima precisión posible; es decir, los puntos devueltos son necesariamente los vecinos más cercanos reales. Como el espacio vectorial se recorre de forma exhaustiva, la búsqueda vectorial exacta puede resultar demasiado lenta para su uso en escenarios reales.
- La búsqueda vectorial aproximada engloba un conjunto de técnicas (por ejemplo, estructuras de datos especiales como grafos y bosques aleatorios) que calculan resultados mucho más rápido que la búsqueda vectorial exacta. Por lo general, la precisión del resultado es “lo bastante buena” para un uso práctico. Muchas técnicas aproximadas ofrecen parámetros para ajustar el equilibrio entre la precisión del resultado y el tiempo de búsqueda.
vectors de tipo Array, p. ej., Array(Float64), Array(Float32) o Array(BFloat16).
El vector de referencia es un Array constante y se define como una expresión de tabla común.
<DistanceFunction> calcula la distancia entre el punto de referencia y todos los puntos almacenados.
Para ello, puede utilizarse cualquiera de las funciones de distancia disponibles.
<N> especifica cuántos vecinos se deben devolver.
Búsqueda vectorial exacta
Ejemplo
Búsqueda vectorial aproximada
Índices de similitud vectorial
Los índices de similitud vectorial están disponibles en la versión 25.8 de ClickHouse y posteriores.
Si tiene algún problema, abra una incidencia en el repositorio de ClickHouse.
Crear un índice de similitud vectorial
ALTER TABLE anterior solo hace que el índice se construya para los nuevos datos que se inserten en la tabla a partir de ese momento.
Para construir también el índice para los datos existentes, debes materializarlo:
<distance_function> debe ser
L2Distance, la distancia euclidiana, que representa la longitud de una recta entre dos puntos en el espacio euclidiano,cosineDistance, la distancia de coseno, que representa el ángulo entre dos vectores no nulos, odotProduct, el producto escalar (producto interno), que representa la suma de los productos elemento a elemento de dos vectores. Es equivalente acosineDistanceen datos normalizados.
L2Distance suele ser la mejor opción; en caso contrario, se recomienda cosineDistance para compensar la escala.
Para las funciones de distancia
L2Distance y cosineDistance, un valor más pequeño implica una mayor similitud, mientras que para dotProduct, un valor más alto implica una mayor similitud.
Como resultado, los índices vectoriales con L2Distance y cosineDistance solo pueden ser utilizados por consultas SELECT [...] ORDER BY [...] ASC (ASC es el valor predeterminado de ORDER BY), mientras que los índices vectoriales creados para dotProduct solo pueden ser utilizados por consultas SELECT [...] ORDER BY [...] DESC.<dimensions> especifica la cardinalidad del array (número de elementos) en la columna subyacente.
Si ClickHouse encuentra un array con una cardinalidad distinta durante la creación del índice, el índice se descarta y se devuelve un error.
El parámetro opcional GRANULARITY <N> se refiere al tamaño de los gránulos del índice (consulte aquí).
A diferencia de los skip indexes normales, que usan una granularidad de índice predeterminada de 1, los índices de similitud vectorial usan 100 millones como granularidad de índice predeterminada.
Este valor garantiza que internamente solo se creen unos pocos índices, incluso para partes grandes.
Recomendamos cambiar la granularidad del índice solo a usuarios avanzados que comprendan las implicaciones de lo que están haciendo (consulte abajo).
Los índices de similitud vectorial son genéricos en el sentido de que pueden admitir distintos métodos de búsqueda aproximada.
El método que se usa realmente se especifica mediante el parámetro <type>.
Por ahora, el único método disponible es HNSW (artículo académico), una técnica popular y de última generación para la búsqueda vectorial aproximada basada en grafos jerárquicos de proximidad.
Si se usa HNSW como tipo, los usuarios pueden especificar opcionalmente más parámetros específicos de HNSW:
<quantization>controla la cuantización de los vectores en el grafo de proximidad. Los valores posibles sonf64,f32,f16,bf16,i8ob1. El valor predeterminado esbf16. Tenga en cuenta que este parámetro no afecta a la representación de los vectores en la columna subyacente.<hnsw_max_connections_per_layer>controla el número de vecinos por nodo del grafo, también conocido como el hiperparámetroMde HNSW. El valor predeterminado es32. El valor0significa que se usa el valor predeterminado.<hnsw_candidate_list_size_for_construction>controla el tamaño de la lista dinámica de candidatos durante la construcción del grafo HNSW, también conocido como el hiperparámetroef_constructionde HNSW. El valor predeterminado es128. El valor0significa que se usa el valor predeterminado.
- Los índices de similitud vectorial solo pueden crearse sobre columnas de tipo Array(Float32), Array(Float64) o Array(BFloat16). No se permiten Arrays de valores de coma flotante Nullable ni LowCardinality, como
Array(Nullable(Float32))yArray(LowCardinality(Float32)). - Los índices de similitud vectorial deben crearse sobre una única columna.
- Los índices de similitud vectorial pueden crearse sobre expresiones calculadas (p. ej.,
INDEX index_name arraySort(vectors) TYPE vector_similarity([...])), pero esos índices no podrán usarse más adelante para la búsqueda aproximada de vecinos. - Los índices de similitud vectorial requieren que todos los arrays de la columna subyacente tengan
<dimension>elementos; esto se comprueba durante la creación del índice. Para detectar incumplimientos de este requisito lo antes posible, los usuarios pueden añadir una restricción a la columna del vector, por ejemplo,CONSTRAINT same_length CHECK length(vectors) = 256. - Del mismo modo, los valores de array de la columna subyacente no deben estar vacíos (
[]) ni tener un valor predeterminado (también[]).
Uso de un índice de similitud vectorial
Para usar índices de similitud vectorial, la configuración compatibility debe estar establecida en
'' (el valor predeterminado), o en '25.1' o una versión posterior.SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>).
El valor predeterminado de 256 para este parámetro funciona bien en la mayoría de los casos de uso.
Valores más altos implican mayor precisión a costa de un rendimiento más lento.
Si la consulta puede utilizar un índice de similitud vectorial, ClickHouse comprueba que el LIMIT <N> proporcionado en las consultas SELECT esté dentro de límites razonables.
Más concretamente, se devuelve un error si <N> es mayor que el valor del parámetro max_limit_for_vector_search_queries, cuyo valor predeterminado es 100.
Valores de LIMIT demasiado grandes pueden ralentizar las búsquedas y, por lo general, indican un error de uso.
Para comprobar si una consulta SELECT utiliza un índice de similitud vectorial, puede agregar el prefijo EXPLAIN indexes = 1 a la consulta.
A modo de ejemplo, consulta
Skip y el nombre y tipo del índice vectorial (en el ejemplo, idx y vector_similarity).
En este caso, el índice de similitud vectorial descartó dos de cuatro gránulos, es decir, el 50 % de los datos.
Cuantos más gránulos se puedan descartar, más efectivo será el uso del índice.
Post-filtrado y pre-filtrado
Los usuarios pueden especificar opcionalmente una cláusula WHERE con condiciones de filtro adicionales para la consulta SELECT.
ClickHouse evaluará estas condiciones de filtro mediante la estrategia de post-filtering o pre-filtering.
En resumen, ambas estrategias determinan el orden en que se evalúan los filtros:
- El posfiltrado significa que primero se evalúa el índice de similitud vectorial y, después, ClickHouse evalúa los filtros adicionales especificados en la cláusula
WHERE. - Con el prefiltrado, el orden de evaluación del filtro es el contrario.
- El posfiltrado tiene el problema general de que puede devolver menos filas de las solicitadas en la cláusula
LIMIT <N>. Esta situación se produce cuando una o más filas de resultado devueltas por el índice de similitud vectorial no cumplen los filtros adicionales. - El prefiltrado es, por lo general, un problema no resuelto. Algunas bases de datos vectoriales especializadas proporcionan algoritmos de prefiltrado, pero la mayoría de las bases de datos relacionales (incluido ClickHouse) recurren a la búsqueda exacta de vecinos, es decir, a un escaneo por fuerza bruta sin índice.
year y se ejecuta la siguiente consulta:
- la condición de filtro elimina al menos una fila dentro de una parte, ClickHouse recurrirá al prefiltrado para los rangos “supervivientes” dentro de la parte,
- la condición de filtro no elimina ninguna fila dentro de una parte, ClickHouse realizará postfiltrado para esa parte.
auto, que implementa las heurísticas anteriores) puede establecerse en prefilter.
Esto resulta útil para forzar el prefiltrado en los casos en que las condiciones de filtro adicionales son extremadamente selectivas.
Como ejemplo, la siguiente consulta puede beneficiarse del prefiltrado:
SETTINGS vector_search_filter_strategy = 'prefilter' a la consulta), ClickHouse primero encuentra todos los libros con un precio inferior a 2 dólares y luego ejecuta una búsqueda vectorial por fuerza bruta sobre los libros encontrados.
Como enfoque alternativo para resolver el problema anterior, vector_search_index_fetch_multiplier (valor predeterminado: 1.0, máximo: 1000.0) puede configurarse con un valor > 1.0 (por ejemplo, 2.0).
La cantidad de vecinos más cercanos obtenidos del índice vectorial se multiplica por el valor de esta configuración y, a continuación, se aplica el filtro adicional sobre esas filas para devolver tantas filas como indique LIMIT.
Por ejemplo, podemos volver a ejecutar la consulta, pero con un multiplicador de 3.0:
vector_search_index_fetch_multiplier puede mitigar el problema, pero en casos extremos (una condición WHERE muy selectiva) sigue siendo posible que se devuelvan menos de N filas de las solicitadas.
Reclasificación
Los índices de omisión de dato en ClickHouse suelen filtrar a nivel de gránulo; es decir, una búsqueda en un índice de omisión de dato (internamente) devuelve una lista de gránulos que podrían coincidir, lo que reduce la cantidad de datos leídos en el escaneo posterior.
Esto funciona bien para los índices de omisión de dato en general, pero en el caso de los índices de similitud vectorial, crea un “desajuste de granularidad”.
En más detalle, el índice de similitud vectorial determina los números de fila de los N vectores más similares para un vector de referencia dado, pero luego necesita extrapolar esos números de fila a números de gránulo.
ClickHouse cargará entonces estos gránulos desde disco y repetirá el cálculo de distancias para todos los vectores de esos gránulos.
Este paso se llama reclasificación y, aunque en teoría puede mejorar la precisión —recuerde que el índice de similitud vectorial solo devuelve un resultado aproximado—, claramente no es óptimo en términos de rendimiento.
Por lo tanto, ClickHouse proporciona una optimización que desactiva la reclasificación y devuelve los vectores más similares y sus distancias directamente desde el índice.
La optimización está habilitada de forma predeterminada; consulte la configuración vector_search_with_rescoring.
A grandes rasgos, funciona así: ClickHouse pone los vectores más similares y sus distancias a disposición como una columna virtual _distances.
Para verlo, ejecute una consulta de búsqueda vectorial con EXPLAIN header = 1:
Una consulta ejecutada sin reclasificación (
vector_search_with_rescoring = 0) y con las réplicas paralelas habilitadas puede recurrir a la reclasificación.Optimización del rendimiento
CODEC(NONE) para la columna de vectores de esta manera:
system.text_log) indican que se está cargando el índice de similitud vectorial.
Si estos mensajes aparecen repetidamente para distintas consultas de búsqueda vectorial, esto indica que el tamaño de la caché es demasiado pequeño.
La caché del índice de similitud vectorial almacena gránulos del índice vectorial.
Si los gránulos individuales del índice vectorial son más grandes que la caché, no se almacenarán en caché.
Por lo tanto, asegúrese de calcular el tamaño del índice vectorial (según la fórmula de “Estimación del consumo de almacenamiento y memoria” o system.data_skipping_indices) y dimensionar la caché en consecuencia.
La cuantización reduce la precisión de las búsquedas vectoriales en comparación con la búsqueda sobre los valores originales de coma flotante de precisión completa (
f32).
Sin embargo, en la mayoría de los conjuntos de datos, la cuantización brain float de precisión media (bf16) da como resultado una pérdida de precisión insignificante, por lo que los índices de similitud vectorial usan esta técnica de cuantización de forma predeterminada.
La cuantización de cuarto de precisión (i8) y la cuantización binaria (b1) provocan una pérdida de precisión apreciable en las búsquedas vectoriales.
Recomendamos ambas cuantizaciones solo si el tamaño del índice de similitud vectorial es significativamente mayor que la DRAM disponible.
En ese caso, también sugerimos habilitar el rescoring (vector_search_index_fetch_multiplier, vector_search_with_rescoring) para mejorar la precisión.
La cuantización binaria solo se recomienda para 1) embeddings normalizados (es decir, longitud del vector = 1; los modelos de OpenAI suelen estar normalizados), y 2) si se utiliza la distancia de coseno como función de distancia.
La cuantización binaria usa internamente la distancia de Hamming para construir el grafo de proximidad y buscar en él.
El paso de rescoring utiliza los vectores originales de precisión completa almacenados en la tabla para identificar los vecinos más cercanos mediante la distancia de coseno.
Ajuste de la transferencia de datos
El vector de referencia en una consulta de búsqueda vectorial lo proporciona el usuario y, por lo general, se obtiene mediante una llamada a un Large Language Model (LLM).
El código Python típico que ejecuta una búsqueda vectorial en ClickHouse podría verse así
search_v en el fragmento anterior) pueden tener una dimensión muy grande.
Por ejemplo, OpenAI ofrece modelos que generan vectores de embeddings con 1536 o incluso 3072 dimensiones.
En el código anterior, el driver de Python de ClickHouse sustituye el vector de embedding por una cadena legible para las personas y luego envía la consulta SELECT completa como una cadena.
Suponiendo que el vector de embedding consta de 1536 valores de coma flotante de precisión simple, la cadena enviada alcanza una longitud de 20 kB.
Esto genera un uso elevado de CPU para la tokenización, el análisis sintáctico y la realización de miles de conversiones de cadena a float.
Además, se requiere una cantidad considerable de espacio en el archivo de registro del servidor de ClickHouse, lo que también provoca un aumento de tamaño en system.query_log.
Tenga en cuenta que la mayoría de los modelos LLM devuelven un vector de embedding como una lista o un array de NumPy de floats nativos.
Por lo tanto, recomendamos que las aplicaciones de Python vinculen el parámetro del vector de referencia en formato binario usando el siguiente estilo:
system.query_log.
Administración y monitorización
Diferencias con los índices de omisión normales
GRANULARITY = [N] gránulos ([N] = 1 de forma predeterminada para los índices de omisión normales).
Por ejemplo, si la granularidad del índice primario de la tabla es 8192 (configuración index_granularity = 8192) y GRANULARITY = 2, entonces cada bloque indexado contendrá 16384 filas.
Sin embargo, las estructuras de datos y los algoritmos para la búsqueda aproximada de vecinos son intrínsecamente orientados a filas.
Almacenan una representación compacta de un conjunto de filas y también devuelven filas para las consultas de búsqueda vectorial.
Esto da lugar a algunas diferencias poco intuitivas en la forma en que se comportan los índices de similitud vectorial en comparación con los índices de omisión normales.
Cuando un usuario define un índice de similitud vectorial sobre una columna, ClickHouse crea internamente un “subíndice” de similitud vectorial para cada bloque de índice.
El subíndice es “local” en el sentido de que solo conoce las filas del bloque de índice al que pertenece.
En el ejemplo anterior, y suponiendo que una columna tiene 65536 filas, obtenemos cuatro bloques de índice (que abarcan ocho gránulos) y un subíndice de similitud vectorial para cada bloque de índice.
En teoría, un subíndice puede devolver directamente las filas con los N puntos más cercanos dentro de su bloque de índice.
Sin embargo, como ClickHouse carga datos del disco a la memoria con la granularidad de los gránulos, los subíndices extrapolan las filas coincidentes a la granularidad de los gránulos.
Esto difiere de los índices de omisión normales, que omiten datos con la granularidad de los bloques de índice.
El parámetro GRANULARITY determina cuántos subíndices de similitud vectorial se crean.
Los valores más grandes de GRANULARITY implican menos subíndices de similitud vectorial, pero más grandes, hasta el punto en que una columna (o una parte de datos de una columna) tiene un único subíndice.
En ese caso, el subíndice tiene una vista “global” de todas las filas de la columna y puede devolver directamente todos los gránulos de la columna (parte) con filas relevantes (hay como máximo LIMIT [N] gránulos de este tipo).
En un segundo paso, ClickHouse cargará estos gránulos e identificará las mejores filas reales realizando un cálculo de distancia por fuerza bruta sobre todas las filas de esos gránulos.
Con un valor pequeño de GRANULARITY, cada subíndice devuelve hasta LIMIT N gránulos.
Como resultado, es necesario cargar más gránulos y posfiltrarlos.
Tenga en cuenta que la precisión de la búsqueda es igual de buena en ambos casos; solo difiere el rendimiento del procesamiento.
En general, se recomienda usar un valor grande de GRANULARITY para los índices de similitud vectorial y recurrir a valores más pequeños de GRANULARITY solo en caso de problemas, como un consumo excesivo de memoria de las estructuras de similitud vectorial.
Si no se especifica GRANULARITY para los índices de similitud vectorial, el valor predeterminado es 100 millones.
Ejemplo
Query
Response
Bit cuantizado (QBit)
Array(BFloat16) en lugar de Array(Float32), el tamaño de los datos se reduce a la mitad y cabe esperar que los tiempos de ejecución de las consultas disminuyan proporcionalmente.
Este método se conoce como cuantización. Aunque acelera el procesamiento, puede reducir la precisión de los resultados a pesar de realizar un escaneo exhaustivo de todos los vectores.
Con la cuantización tradicional, perdemos precisión tanto durante la búsqueda como al almacenar los datos. En el ejemplo anterior, almacenaríamos BFloat16 en lugar de Float32, lo que significa que nunca podríamos realizar después una búsqueda más precisa, aunque lo quisiéramos. Otra alternativa es almacenar dos copias de los datos: una cuantizada y otra con precisión completa. Aunque esto funciona, requiere almacenamiento redundante. Imagina un caso en el que tenemos Float64 como dato original y queremos ejecutar búsquedas con distinta precisión (16 bits, 32 bits o los 64 bits completos). Necesitaríamos almacenar tres copias independientes de los datos.
ClickHouse ofrece el tipo de dato Quantized Bit (QBit), que resuelve estas limitaciones de la siguiente manera:
- Almacena los datos originales con precisión completa.
- Permite especificar la precisión de cuantización en tiempo de consulta.
QBit, usa la siguiente sintaxis:
element_type– el tipo de cada elemento del vector. Los tipos admitidos sonBFloat16,Float32yFloat64dimension– la cantidad de elementos de cada vector
Creación de una tabla QBit y adición de datos
Búsqueda vectorial con QBit
QBit aquí.
Búsqueda con precisión completa (64 bits):
Consideraciones de rendimiento
QBit se debe a la reducción de las operaciones de E/S, ya que al usar una precisión menor es necesario leer menos datos desde el almacenamiento. Además, cuando QBit contiene datos Float32, si el parámetro de precisión es 16 o menos, se obtienen beneficios adicionales al reducirse el cómputo. El parámetro de precisión controla directamente el equilibrio entre exactitud y velocidad:
- Mayor precisión (más cercana al ancho original de los datos): resultados más precisos, consultas más lentas
- Menor precisión: consultas más rápidas con resultados aproximados y menor uso de memoria