Índices bitmap en Go: búsqueda a velocidad salvaje

Índices bitmap en Go: búsqueda a velocidad salvaje

Palabras Introductorias

Presenté esta charla en inglés en la conferencia GopherCon Rusia 2019 en Moscú y en ruso en un meetup en Nizhni Nóvgorod. Se trata del índice bitmap, menos común que el B-tree, pero igualmente interesante. Estoy compartiendo la grabación de la charla en la conferencia en inglés y la transcripción en texto en ruso.

Exploraremos cómo funciona el índice bitmap, cuándo es mejor y cuándo es peor que otros índices y en qué casos es significativamente más rápido; veremos en qué populares SGBD ya existen índices bitmap; intentaremos escribir el nuestro en Go. Y como 'postre', utilizaremos bibliotecas listas para crear nuestra base de datos especializada súper rápida.

Espero sinceramente que mis esfuerzos sean útiles e interesantes para ustedes. ¡Vamos a ello!

Introducción

Reproducir video

http://bit.ly/bitmapindexes
https://github.com/mkevac/gopherconrussia2019

¡Hola a todos! Son las seis de la tarde y todos estamos súper cansados. Es un momento maravilloso para hablar sobre la aburrida teoría de índices de bases de datos, ¿verdad? No se preocupen, tendré un par de líneas de código fuente aquí y allá. 🙂

Sin bromas, la charla está llena de información y no tenemos tanto tiempo. Así que, ¡empecemos!
Índices bitmap en Go: búsqueda a velocidad salvaje
Hoy hablaré sobre lo siguiente:

  • ¿Qué son los índices;
  • ¿Qué es un índice bitmap;
  • ¿Dónde se utiliza y dónde NO se utiliza y por qué;
  • una implementación simple en Go y un poco de lucha con el compilador;
  • una implementación un poco menos simple, pero mucho más eficiente en ensamblador Go;
  • los 'problemas' de los índices bitmap;
  • implementaciones existentes.

¿Qué son los índices?

Índices bitmap en Go: búsqueda a velocidad salvaje

Un índice es una estructura de datos separada que mantenemos y actualizamos además de los datos principales. Se utiliza para acelerar la búsqueda. Sin índices, la búsqueda requeriría un recorrido completo de los datos (un proceso llamado 'búsqueda completa'), y este proceso tiene una complejidad algorítmica lineal. Pero las bases de datos suelen contener una enorme cantidad de datos y la complejidad lineal es demasiado lenta. Idealmente, nos gustaría obtener una complejidad logarítmica o constante.

Este es un tema enorme y complejo, lleno de matices y compromisos, pero después de observar décadas de desarrollo e investigación en diferentes bases de datos, estoy dispuesto a afirmar que existen solo unos pocos enfoques ampliamente utilizados para crear índices de bases de datos.

Índices bitmap en Go: búsqueda a velocidad salvaje

El primer enfoque consiste en reducir jerárquicamente el área de búsqueda, dividiendo el área de búsqueda en partes más pequeñas.

Normalmente, hacemos esto utilizando diferentes tipos de árboles. Un ejemplo podría ser una gran caja con materiales en su armario, donde hay cajas más pequeñas con materiales divididos por diferentes temáticas. Si necesitas materiales, seguramente buscarás en la caja etiquetada como 'Materiales', y no en la que dice 'Galletas', ¿verdad?

Índices bitmap en Go: búsqueda a velocidad salvaje

El segundo enfoque consiste en identificar de inmediato el elemento o grupo de elementos que se necesita. Hacemos esto en hash maps o en índices inversos. El uso de hash maps es muy parecido al ejemplo anterior, solo que en lugar de una caja con cajas tienes en tu armario un montón de pequeñas cajas con artículos finales.

Índices bitmap en Go: búsqueda a velocidad salvaje

El tercer enfoque es eliminar la necesidad de búsqueda. Esto lo hacemos con mediante filtros de Bloom o filtros cuckoo. Los primeros dan una respuesta instantánea, liberándote de la necesidad de buscar.

Índices bitmap en Go: búsqueda a velocidad salvaje

El último enfoque se basa en aprovechar al máximo todo el poder que nos ofrece el hardware moderno. Es lo que hacemos en índices bitmap. Sí, al usarlos a veces necesitamos recorrer todo el índice, pero lo hacemos de manera súper eficiente.

Como ya mencioné, el tema de los índices de bases de datos es amplio y está lleno de compromisos. Esto significa que a veces podemos utilizar varios enfoques al mismo tiempo: si necesitamos acelerar aún más la búsqueda o si es necesario cubrir todos los posibles tipos de búsqueda.

Hoy les hablaré sobre el enfoque menos conocido de los mencionados: los índices bitmap.

¿Quién soy yo para hablar sobre este tema?

Índices bitmap en Go: búsqueda a velocidad salvaje

Soy el líder de equipo en Badoo (quizás te suene más otro de nuestros productos: Bumble). Ya tenemos más de 400 millones de usuarios en todo el mundo y muchas funciones que se encargan de encontrarles la mejor pareja. Lo hacemos a través de servicios personalizados, que utilizan también índices bitmap.

¿Qué es entonces un índice bitmap?

Índices bitmap en Go: búsqueda a velocidad salvaje
Los índices de bitmap, como su nombre indica, utilizan bitmaps o conjuntos de bits para implementar un índice de búsqueda. Desde una vista general, este índice consiste en uno o varios de estos bitmaps, que representan entidades (como personas) y sus propiedades o parámetros (edad, color de ojos, etc.), y un algoritmo que utiliza operaciones binarias (AND, OR, NOT) para responder a la consulta de búsqueda.
Índices bitmap en Go: búsqueda a velocidad salvaje
Se nos dice que los índices de bitmap son especialmente adecuados y muy eficientes para casos en los que hay búsquedas que combinan consultas en muchas columnas de baja cardinalidad (imagina 'color de ojos' o 'estado civil' frente a algo como 'distancia al centro de la ciudad'). Pero más adelante demostraré que también funcionan muy bien en columnas de alta cardinalidad.

Veamos un ejemplo básico de un índice de bitmap.
Índices bitmap en Go: búsqueda a velocidad salvaje
Imagina que tenemos una lista de restaurantes en Moscú con propiedades binarias como estas:

  • cerca del metro (near metro);
  • tiene estacionamiento privado (has private parking);
  • tiene terraza (has terrace);
  • aceptan reservas (accepts reservations);
  • apto para vegetarianos (vegan friendly);
  • caro (expensive).

Índices bitmap en Go: búsqueda a velocidad salvaje
Asignemos a cada restaurante un número secuencial comenzando desde 0 y reservemos memoria para 6 bitmaps (uno para cada característica). Luego, llenaremos estos bitmaps dependiendo de si el restaurante tiene o no dicha propiedad. Si el restaurante 4 tiene terraza, entonces el bit nº4 en el bitmap 'tiene terraza' se establecerá en 1 (si no hay terraza, en 0).
Índices bitmap en Go: búsqueda a velocidad salvaje
Ahora tenemos el índice de bitmap más simple que se puede imaginar, y podemos usarlo para responder a consultas como:

  • «Muéstrame los restaurantes aptos para vegetarianos»;
  • «Muéstrame restaurantes económicos con terraza, donde se pueda reservar mesa».

Índices bitmap en Go: búsqueda a velocidad salvaje
Índices bitmap en Go: búsqueda a velocidad salvaje
¿Cómo? Veámoslo. La primera consulta es muy sencilla. Todo lo que necesitamos hacer es tomar el bitmap 'apto para vegetarianos' y convertirlo en una lista de restaurantes cuyas bits están activadas.
Índices bitmap en Go: búsqueda a velocidad salvaje
Índices bitmap en Go: búsqueda a velocidad salvaje
La segunda consulta es un poco más complicada. Necesitamos aplicar la operación bit a bit NOT al bitmap "caro" para obtener una lista de restaurantes económicos, luego hacer un AND con el bitmap "se puede reservar" y AND también el resultado con el bitmap "tiene veranda". El bitmap resultante contendrá una lista de establecimientos que cumplen con todos nuestros criterios. En este ejemplo, solo hay un restaurante: "Juventud".
Índices bitmap en Go: búsqueda a velocidad salvaje
Índices bitmap en Go: búsqueda a velocidad salvaje
Aquí hay mucha teoría, pero no se preocupen, veremos el código muy pronto.

¿Dónde se utilizan los índices de bitmap?

Índices bitmap en Go: búsqueda a velocidad salvaje
Si "googleas" índices de bitmap, el 90% de las respuestas estarán relacionadas de alguna manera con Oracle DB. Pero, ¿otras bases de datos también apoyan esta característica tan genial, verdad? No del todo.

Hagamos un recorrido por la lista de los principales sospechosos.
Índices bitmap en Go: búsqueda a velocidad salvaje
MySQL aún no soporta índices de bitmap, pero hay una propuesta para agregar esta opción (https://dev.mysql.com/worklog/task/?id=1524).

PostgreSQL no soporta índices de bitmap, pero utiliza simples bitmaps y operaciones bit a bit para combinar resultados de búsqueda de varios índices diferentes.

Tarantool tiene índices de bitset, que permiten una búsqueda simple.

Redis tiene campos bit simples (https://redis.io/commands/bitfield) sin capacidad de búsqueda sobre ellos.

MongoDB aún no soporta índices de bitmap, pero también hay una propuesta para agregar esta opción https://jira.mongodb.org/browse/SERVER-1723

Elasticsearch utiliza bitmaps internamente (https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps).

Índices bitmap en Go: búsqueda a velocidad salvaje

  • Pero en nuestro vecindario ha llegado un nuevo vecino: Pilosa. Esta es una nueva base de datos no relacional, escrita en Go. Solo contiene índices de bitmap y se basa completamente en ellos. Hablaremos de esto un poco más tarde.

Implementación en Go

Pero, ¿por qué los índices de bitmap se utilizan tan raramente? Antes de responder a esta pregunta, me gustaría mostrarles una implementación de un índice de bitmap muy simple en Go.
Índices bitmap en Go: búsqueda a velocidad salvaje
Los bitmaps, en esencia, se representan como simples bloques de datos. En Go, usemos para esto slices de bytes.

Tenemos un bitmap para una característica del restaurante, y cada bit en el bitmap indica si un restaurante específico tiene o no esa propiedad.
Índices bitmap en Go: búsqueda a velocidad salvaje
Necesitaremos dos funciones auxiliares. Una se usará para llenar nuestros mapas de bits con datos aleatorios. Aleatorios, pero con una probabilidad específica de que un restaurante tenga cada atributo. Por ejemplo, creo que en Moscú hay muy pocos restaurantes en los que no se pueda reservar una mesa, y me parece que aproximadamente el 20% de los establecimientos son aptos para vegetarianos.

La segunda función convertirá el mapa de bits en una lista de restaurantes.
Índices bitmap en Go: búsqueda a velocidad salvaje
Índices bitmap en Go: búsqueda a velocidad salvaje
Para responder a la consulta "Muéstrame restaurantes económicos que tengan una terraza y donde se pueda reservar una mesa", necesitaremos dos operaciones de bits: NOT y AND.

Podemos simplificar un poco nuestro código utilizando una operación más compleja AND NOT.

Tenemos funciones para cada una de estas operaciones. Ambas recorren los slices, toman los elementos correspondientes de cada uno, combinan con la operación de bits y colocan el resultado en el slice resultante.
Índices bitmap en Go: búsqueda a velocidad salvaje
Y ahora podemos utilizar nuestros mapas de bits y funciones para responder a la consulta de búsqueda.
Índices bitmap en Go: búsqueda a velocidad salvaje
El rendimiento no es tan alto, a pesar de que las funciones son muy simples y hemos ahorrado bastante al no devolver un nuevo slice resultante en cada llamada a la función.

Al perfilar un poco con pprof, noté que el compilador de Go omitió una optimización muy sencilla pero muy importante: la inlining de funciones.
Índices bitmap en Go: búsqueda a velocidad salvaje
El problema es que el compilador de Go tiene un gran temor a los bucles que recorren slices y se niega a inlinear funciones que contienen tales bucles.
Índices bitmap en Go: búsqueda a velocidad salvaje
Pero yo no tengo miedo y puedo engañar al compilador utilizando goto en lugar de un bucle, como en los viejos tiempos.

Índices bitmap en Go: búsqueda a velocidad salvaje
Índices bitmap en Go: búsqueda a velocidad salvaje

Y, como pueden ver, ahora el compilador está encantado de inlinear nuestra función. Al final, logramos ahorrar alrededor de 2 microsegundos. ¡No está mal!

Índices bitmap en Go: búsqueda a velocidad salvaje

El segundo cuello de botella no es difícil de ver si observas atentamente la salida del ensamblador. El compilador agregó una verificación de límites de slice justo dentro de nuestro bucle más caliente. El hecho es que Go es un lenguaje seguro, el compilador teme que mis tres argumentos (tres slices) tengan diferentes tamaños. Teóricamente, eso generaría la posibilidad de un desbordamiento de búfer (buffer overflow).

Calmemos al compilador mostrándole que todos los slices tienen el mismo tamaño. Podemos hacer esto añadiendo una simple verificación al inicio de nuestra función.
Índices bitmap en Go: búsqueda a velocidad salvaje
Al ver esto, el compilador felizmente omite la verificación, y al final ahorramos 500 nanosegundos más.

Lotes grandes

Bien, hemos logrado obtener algo de rendimiento de nuestra simple implementación, pero este resultado, en realidad, es mucho peor de lo que podría ser con el hardware actual.

Todo lo que hacemos son operaciones básicas con bits, y nuestros procesadores las ejecutan de manera muy eficiente. Pero, desafortunadamente, estamos 'alimentando' a nuestro procesador con trozos de trabajo muy pequeños. Nuestras funciones ejecutan operaciones byte a byte. Podemos ajustar muy fácilmente nuestro código para que trabaje con trozos de 8 bytes, utilizando slices de UInt64.

Índices bitmap en Go: búsqueda a velocidad salvaje

Como pueden ver, este pequeño cambio aceleró nuestra programa ocho veces al aumentar el tamaño del lote ocho veces. La mejora se podría considerar lineal.

Índices bitmap en Go: búsqueda a velocidad salvaje

Implementación en ensamblador

Índices bitmap en Go: búsqueda a velocidad salvaje
Pero esto no es el final. Nuestros procesadores pueden trabajar con trozos de 16, 32 e incluso 64 bytes. Estas operaciones 'anchas' se llaman single instruction multiple data (SIMD; una instrucción, muchos datos), y el proceso de transformar el código de tal manera que utilice estas operaciones se llama vectorización.

Desafortunadamente, el compilador de Go no es particularmente bueno en la vectorización. En este momento, la única forma de vectorizar código en Go es tomar y escribir manualmente las operaciones de datos utilizando ensamblador de Go.

Índices bitmap en Go: búsqueda a velocidad salvaje

El ensamblador de Go es una criatura extraña. Seguramente saben que el ensamblador es algo que está muy vinculado a la arquitectura de la computadora para la que están programando, pero en Go no es así. El ensamblador de Go se parece más a un IRL (intermediate representation language) o lenguaje de representación intermedia: es prácticamente independiente de la plataforma. Rob Pike hizo una excelente informe charla al respecto hace unos años en GopherCon en Denver.

Además, Go utiliza un formato inusual llamado Plan 9, que difiere de los formatos reconocidos de AT&T e Intel.
Índices bitmap en Go: búsqueda a velocidad salvaje
Se puede decir con confianza que escribir ensamblador de Go a mano no es la actividad más divertida.

Pero, afortunadamente, ya hay dos herramientas de alto nivel que nos ayudan en la escritura de ensamblador de Go: PeachPy y avo. Ambas utilidades generan ensamblador de Go a partir de código de nivel más alto escrito en Python y Go, respectivamente.
Índices bitmap en Go: búsqueda a velocidad salvaje
Estas utilidades simplifican cosas como la asignación de registros (selección de registros de procesador), la escritura de bucles y, en general, facilitan el proceso de entrada en el mundo de la programación en ensamblador en Go.

Vamos a utilizar avo, así que nuestros programas serán casi programas normales en Go.
Índices bitmap en Go: búsqueda a velocidad salvaje
Este es el ejemplo más simple de un programa en avo. Tenemos una función main() que define dentro de sí misma una función Add(), que se encarga de sumar dos números. Aquí hay funciones auxiliares para obtener parámetros por nombre y conseguir uno de los registros de procesador libres y adecuados. Cada operación de procesador tiene una función correspondiente en avo, como se puede ver en ADDQ. Y, finalmente, vemos una función auxiliar para guardar el valor resultante.
Índices bitmap en Go: búsqueda a velocidad salvaje
Al ejecutar go generate, ejecutaremos el programa en avo y se generarán dos archivos:

  • add.s con el código resultante en ensamblador Go;
  • stub.go con las cabeceras de funciones para conectar dos mundos: Go y ensamblador.

Índices bitmap en Go: búsqueda a velocidad salvaje
Ahora que hemos visto qué hace y cómo funciona avo, echemos un vistazo a nuestras funciones. He implementado versiones tanto escalares como vectoriales (SIMD) de las funciones.

Primero, echemos un vistazo a las versiones escalares.
Índices bitmap en Go: búsqueda a velocidad salvaje
Al igual que en el ejemplo anterior, solicitamos un registro de uso general libre y adecuado, no necesitamos calcular desplazamientos y tamaños para los argumentos. Todo eso lo hace avo por nosotros.
Índices bitmap en Go: búsqueda a velocidad salvaje
Anteriormente, utilizamos etiquetas y goto (o saltos) para mejorar el rendimiento y engañar al compilador de Go, pero ahora lo hacemos desde el principio. La cuestión es que los bucles son un concepto de nivel más alto. En ensamblador, solo tenemos etiquetas y saltos.
Índices bitmap en Go: búsqueda a velocidad salvaje
El código restante debe ser ya familiar y comprensible. Emulamos el bucle con etiquetas y saltos, tomamos un pequeño segmento de datos de nuestros dos slices, los combinamos mediante una operación bit a bit (AND NOT en este caso) y luego colocamos el resultado en el slice resultante. Eso es todo.
Índices bitmap en Go: búsqueda a velocidad salvaje
Así es como se ve el código final en ensamblador. No necesitamos calcular desplazamientos y tamaños (resaltado en verde) ni rastrear los registros utilizados (resaltado en rojo).
Índices bitmap en Go: búsqueda a velocidad salvaje
Si comparamos el rendimiento de la implementación en ensamblador con el rendimiento de la mejor implementación en Go, veremos que son iguales. Y esto es esperado. Después de todo, no hicimos nada especial: simplemente reproducimos lo que haría el compilador de Go.

Desafortunadamente, no podemos hacer que el compilador inlinet nuestras funciones escritas en ensamblador. Hasta la fecha, el compilador de Go no tiene tal capacidad, aunque existe una solicitud para agregarla desde hace bastante tiempo.

Es por eso que es imposible obtener ventajas de las funciones pequeñas en ensamblador. Debemos escribir funciones grandes, utilizar el nuevo paquete math/bits, o evitar el ensamblador por completo.

Ahora, veamos las versiones vectoriales de nuestras funciones.
Índices bitmap en Go: búsqueda a velocidad salvaje
Para este ejemplo, decidí aplicar AVX2, por lo que utilizaremos operaciones que trabajan con fragmentos de 32 bytes. La estructura del código es muy similar a la de la versión escalar: carga de parámetros, solicitud de un registro general libre, etc.
Índices bitmap en Go: búsqueda a velocidad salvaje
Una de las novedades es que las operaciones vectoriales más amplias utilizan registros especiales más anchos. En el caso de fragmentos de 32 bytes, son registros con el prefijo Y. Por eso ves la función YMM() en el código. Si hubiera utilizado AVX-512 con fragmentos de 64 bits, el prefijo sería Z.

La segunda novedad es que decidí usar una optimización llamada desenrollado de bucles (loop unrolling), es decir, realizar ocho operaciones del bucle manualmente antes de saltar al inicio del bucle. Esta optimización reduce la cantidad de ramas en el código y está limitada por la cantidad de registros libres disponibles.
Índices bitmap en Go: búsqueda a velocidad salvaje
¿Y qué hay del rendimiento? ¡Es excelente! Logramos una aceleración de aproximadamente siete veces en comparación con la mejor solución en Go. Impresionante, ¿verdad?
Índices bitmap en Go: búsqueda a velocidad salvaje
Pero incluso esta implementación podría acelerarse potencialmente utilizando AVX-512, prefetching o JIT (compilador en tiempo de ejecución) para el planificador de consultas. Pero ese definitivamente es un tema para otra presentación.

Problemas de índices bitmap

Ahora que hemos revisado la implementación simple de un índice bitmap en Go y una mucho más eficiente en ensamblador, hablemos finalmente sobre por qué los índices bitmap se utilizan tan raramente.
Índices bitmap en Go: búsqueda a velocidad salvaje
En antiguos trabajos científicos se mencionan tres problemas de los índices bitmap, pero trabajos más recientes y yo afirmamos que ya no son relevantes. No profundicemos en cada uno de estos problemas, pero los revisaremos superficialmente.

El problema de alta cardinalidad

Entonces, nos dicen que los índices bitmap son adecuados solo para campos de baja cardinalidad, es decir, aquellos que tienen pocos valores (por ejemplo, sexo o color de ojos), y la razón es que la representación estándar de tales campos (un bit por valor) ocuparía demasiado espacio en caso de alta cardinalidad y, además, estos índices bitmap estarían poco (raramente) llenos.
Índices bitmap en Go: búsqueda a velocidad salvaje
Índices bitmap en Go: búsqueda a velocidad salvaje
A veces podemos usar otra representación, por ejemplo, la estándar que utilizamos para representar números. Pero fue la aparición de algoritmos de compresión lo que cambió todo. En las últimas décadas, científicos e investigadores han ideado una gran cantidad de algoritmos de compresión para bitmaps. Su principal ventaja es que no es necesario descomprimir los bitmaps para realizar operaciones bit a bit; podemos llevar a cabo operaciones bit a bit directamente sobre los bitmaps comprimidos.
Índices bitmap en Go: búsqueda a velocidad salvaje
Recientemente han comenzado a aparecer enfoques híbridos, como los bitmaps roaring. Utilizan simultáneamente tres representaciones diferentes para los bitmaps: los propios bitmaps, arrays y los llamados bit runs, y equilibran entre ellos para maximizar el rendimiento y minimizar el consumo de memoria.

Puedes encontrar bitmaps roaring en las aplicaciones más populares. Ya existen una gran cantidad de implementaciones para muchos lenguajes de programación, incluyendo más de tres implementaciones para Go.
Índices bitmap en Go: búsqueda a velocidad salvaje
Otro enfoque que puede ayudarnos a lidiar con alta cardinalidad se llama agrupación (binning). Imagina que tienes un campo que representa la altura de una persona. La altura es un número de punto flotante, pero nosotros, los humanos, no lo pensamos de esa manera. Para nosotros no hay diferencia entre una altura de 185,2 cm y 185,3 cm.

Así que podemos agrupar valores similares en grupos dentro de 1 cm.

Y si además sabemos que hay muy pocas personas con altura menor a 50 cm y mayor a 250 cm, entonces podemos, en esencia, convertir un campo de cardinalidad infinita en un campo con aproximadamente 200 valores únicos.

Por supuesto, si es necesario, podemos hacer una filtración adicional después.

El problema del gran ancho de banda

El siguiente problema de los índices bitmap es que su actualización puede ser muy costosa.

Las bases de datos deben permitir la actualización de datos en el momento en que potencialmente cientos de otras solicitudes están buscando esos datos. Necesitamos bloqueos para evitar problemas de acceso concurrente a los datos o cualquier otro problema de concurrencia. Y donde hay un gran bloqueo, surge un problema: contentión de bloqueo, cuando ese bloqueo se convierte en un cuello de botella.
Índices bitmap en Go: búsqueda a velocidad salvaje
Este problema se puede resolver o eludir mediante sharding o utilizando índices versionados.

El sharding es una cosa simple y bien conocida. Puedes shardear un índice bitmap de la misma manera que shardearías cualquier otro dato. En lugar de un gran bloqueo, obtienes un montón de bloqueos pequeños y así evitas la contentión de bloqueo.

La segunda forma de abordar el problema es utilizar índices versionados. Puedes tener una copia del índice que utilizas para buscar o leer, y otra para escribir o actualizar. Y cada cierto tiempo (por ejemplo, cada 100 ms o 500 ms) las duplicas y cambias de lugar. Por supuesto, este enfoque solo es aplicable en aquellos casos en los que tu aplicación puede trabajar con un índice de búsqueda ligeramente desfasado.

Estos dos enfoques se pueden utilizar al mismo tiempo: puedes tener un índice versionado shardizado.

Consultas más complejas

El último problema de los índices bitmap es que, como se nos dice, no son adecuados para tipos de consultas más complejas, como las consultas "por rango".

Y es cierto, si lo piensas, las operaciones bit a bit como AND, OR, etc. no son muy adecuadas para consultas tipo "Muéstrame hoteles con precios de habitaciones entre 200 y 300 dólares por noche".
Índices bitmap en Go: búsqueda a velocidad salvaje
Una solución ingenua y muy imprudente sería tomar los resultados para cada valor de dólar y combinarlos mediante una operación OR.
Índices bitmap en Go: búsqueda a velocidad salvaje
Una solución un poco más correcta sería utilizar agrupamiento. Por ejemplo, en grupos de 50 dólares. Esto aceleraría nuestro proceso 50 veces.

Pero el problema también se resuelve fácilmente utilizando una representación creada específicamente para este tipo de consultas. En trabajos académicos, se llama bitmaps codificados por rango.
Índices bitmap en Go: búsqueda a velocidad salvaje
En esta representación, no solo establecemos un bit para algún valor (por ejemplo, 200), sino que establecemos ese valor y todo lo que está por encima. 200 y más. Lo mismo para 300: 300 y más. Y así sucesivamente.

Usando esta representación, podemos responder a este tipo de consulta de búsqueda recorriendo el índice solo dos veces. Primero obtendremos una lista de hoteles donde el precio de la habitación es inferior a 300 dólares, y luego eliminaremos aquellos donde el costo de la habitación es inferior a 199 dólares. Listo.
Índices bitmap en Go: búsqueda a velocidad salvaje
Te sorprenderá saber que incluso las consultas geoespaciales son posibles utilizando índices bitmap. La clave es usar una representación geográfica que rodee tus coordenadas con una figura geométrica. Por ejemplo, S2 de Google. La figura debe ser representable mediante tres o más líneas que se crucen y que puedan ser numeradas. Así podremos convertir nuestra consulta geoespacial en varias consultas "por intervalo" (a lo largo de estas líneas numeradas).

Soluciones listas

Espero haber despertado un poco de tu interés y que ahora tengas otra herramienta útil en tu arsenal. Si alguna vez necesitas hacer algo similar, sabrás en qué dirección mirar.

Sin embargo, no todos tienen el tiempo, la paciencia y los recursos para crear índices bitmap desde cero. Especialmente los más avanzados, que utilizan SIMD, por ejemplo.

Afortunadamente, hay varias soluciones listas que pueden ayudarte.
Índices bitmap en Go: búsqueda a velocidad salvaje

Bitmaps roaring

Primero, está la biblioteca de bitmaps roaring de la que ya he hablado. Contiene todos los contenedores y operaciones de bits necesarias para crear un índice bitmap completo.
Índices bitmap en Go: búsqueda a velocidad salvaje
Desafortunadamente, por el momento, ninguna de las implementaciones en Go utiliza SIMD, por lo que las implementaciones en Go son menos eficientes que las de C, por ejemplo.

Pilosa

Otro producto que puede ayudarte es la base de datos Pilosa, que básicamente solo tiene índices bitmap. Es una solución relativamente nueva, pero está ganando adeptos a una velocidad increíble.
Índices bitmap en Go: búsqueda a velocidad salvaje
Pilosa utiliza bitmaps roaring en su interior y te permite usarlos, simplificando y explicando todas esas cosas sobre las que he hablado anteriormente: agrupación, bitmaps codificados por rango, el concepto de campo, etc.

Echemos un vistazo rápido a un ejemplo de uso de Pilosa para responder a una pregunta que ya conocen.
Índices bitmap en Go: búsqueda a velocidad salvaje
El ejemplo es muy similar a lo que han visto anteriormente. Creamos un cliente para el servidor Pilosa, creamos un índice y los campos necesarios, luego llenamos nuestros campos con datos aleatorios según probabilidades y, finalmente, realizamos la consulta conocida.

Después de esto, utilizamos NOT en el campo 'expensive', luego cruzamos el resultado (o AND) con el campo 'terrace' y con el campo 'reservations'. Y al final, obtenemos el resultado final.
Índices bitmap en Go: búsqueda a velocidad salvaje
Espero que en un futuro cercano este nuevo tipo de índices, los índices bitmap, también aparezca en bases de datos como MySQL y PostgreSQL.
Índices bitmap en Go: búsqueda a velocidad salvaje

Conclusión

Índices bitmap en Go: búsqueda a velocidad salvaje
Si todavía no se han quedado dormidos, gracias. Tuve que tocar muchos temas brevemente debido al tiempo limitado, pero espero que la presentación haya sido útil y, quizás, incluso motivadora.

Es bueno conocer los índices bitmap, incluso si no los necesitan en este momento. Que sean una herramienta más en su caja.

Hemos revisado varios trucos para aumentar el rendimiento en Go y aquellas cosas con las que el compilador de Go aún no se maneja muy bien. Esto es, sin duda, algo que todos los programadores de Go deben saber.

Eso es todo lo que quería contar. ¡Gracias!

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster