Bases de datos vectoriales: cómo las diseña turbopuffer

Bases de datos vectoriales: cómo las diseña turbopuffer

@programacion

Lo esencial

  • turbopuffer separa el almacenamiento (object storage) de la velocidad (caché NVMe y memoria).
  • v1 usó un índice ANN (SPANN, luego SPFresh) como estructura primaria de todo el sistema.
  • v2 sumó filtrado por atributos y búsqueda de texto completo (BM25) sobre el mismo índice.
  • El 30 de septiembre de 2026 turbopuffer anunció v3: el ANN pasa a ser un índice secundario más.
  • Cursor, Notion y el motor de sincronización de Linear ya corren sobre esta arquitectura.

¿Qué es una base de datos vectorial serverless?

Cuando buscás "zapatillas para correr en asfalto" y el buscador también te muestra "tenis para trote en calle", no hay coincidencia de texto: hay una base de datos vectorial comparando significados. turbopuffer es una de esas bases, pero con una particularidad: no tiene servidores propios guardando tus datos, los guarda en el object storage de la nube (S3, GCS) y usa disco NVMe y memoria local solo como caché.

Ese diseño la hace serverless en sentido literal: no pagás por una máquina corriendo todo el día con tus datos adentro, pagás por almacenamiento barato en object storage y por el cómputo que consumís en cada consulta. Object storage es la fuente de verdad: si se borra el caché completo, no se pierde ningún dato, solo baja la velocidad hasta que se vuelve a llenar.

El problema que resuelve: buscar por significado, no por texto exacto

Una base de datos relacional busca coincidencias exactas o rangos (precio menor a 100). Una base vectorial busca cercanía: convierte cada documento en un vector (una lista de números, típicamente de 384 a 1536 dimensiones) con un modelo de embeddings, y después busca los vectores más cercanos al vector de la consulta usando una métrica como la distancia coseno.

Comparar un vector contra millones de otros, uno por uno, es demasiado lento para una consulta en vivo. Por eso casi todas las bases vectoriales usan un índice ANN (Approximate Nearest Neighbor): sacrifican algo de precisión para ganar velocidad, agrupando vectores parecidos para no tener que revisarlos todos en cada búsqueda.

v1: el índice ANN como columna vertebral del almacenamiento

En su primera versión, cada documento en turbopuffer era apenas un ID y un vector. En vez de un índice basado en grafos (la opción más popular en ese momento), el equipo eligió clustering jerárquico porque convive mejor con object storage: primero con SPANN y después con SPFresh, que permite indexar de forma incremental sin reconstruir el árbol completo en cada escritura.

Los vectores se agrupan en clusters, y los centroides de esos clusters se agrupan a su vez, formando un árbol con una sola raíz. Todo se guarda sobre un almacenamiento tipo mapa clave-valor con claves ordenadas: cada cluster tiene un ClusterId y cada vector dentro del cluster un LocalId denso. La combinación de ambos (por ejemplo C0L1) es la 'dirección ANN', la clave que referencia todo lo demás en el sistema.

// vectores en la hoja del arbol
K::Vector(C0L0) = [0.45, 0.32, ...]
K::Id(C0L0) = 7
K::Vector(C0L1) = [-0.28, 0.96, ...]
K::Id(C0L1) = 13

// el centroide del cluster C0 se indexa a su vez un nivel mas arriba
K::Vector(C1L4) = [0.64, -0.48, ...]
K::Id(C1L4) = C0

Todo en el sistema (filtros, texto completo, agregaciones) terminó indexándose alrededor de esa dirección ANN. Funciona muy bien para buscar vecinos cercanos, pero convierte al índice vectorial en el eje obligatorio de cualquier otra consulta que se agregue después.

v2: filtrado por atributos y búsqueda de texto completo

Los clientes pronto pidieron filtrar resultados por atributos (categoría, precio, fecha) además de por similitud semántica. turbopuffer modeló esto como un índice invertido: cada valor de atributo apunta a la lista de direcciones ANN de los documentos que lo contienen.

K::AttrIndex("categoria", "zapatillas") -> [C0L3, C1L2, C1L3, ...]
K::AttrIndex("talle", "42") -> [C0L3, C1L2, C1L9, ...]

Para la búsqueda de texto completo con BM25 (el algoritmo de ranking que usan Elasticsearch y Lucene) se agregó un índice parecido, pero guardando además cuántas veces aparece cada término y qué tan largo es el documento, datos que BM25 necesita para puntuar relevancia.

K::FTS("descripcion", "running") -> [(C0L0, 2, 37), (C9L4, 1, 42), ...]
// (direccion ANN, apariciones del termino, longitud del documento)

El problema de construir todo sobre un único índice

Con los años, turbopuffer sumó agregaciones, búsqueda por regex, coincidencia difusa y vectores dispersos, siempre sobre la misma base: el índice ANN como estructura primaria. El diseño rinde muy bien para su tarea original, pero complica cualquier consulta que no gire alrededor de la similitud vectorial, como un simple GROUP BY.

Si querés agrupar o contar documentos sin pasar por el árbol de clusters, el motor de consultas tiene que rodear al índice ANN en lugar de usarlo. Esa fue la razón concreta que llevó a turbopuffer a anunciar, el 30 de septiembre de 2026, que en la versión 3 el ANN deja de ser el índice primario y pasa a ser uno más entre varios índices secundarios.

Dónde vive la velocidad: caché en niveles sobre NVMe y memoria

La pieza que hace viable que todos tus vectores vivan en object storage (barato, pero con decenas de milisegundos de latencia) es el caché en niveles. turbopuffer mantiene una copia de los clusters consultados seguido en SSD NVMe local, y una copia todavía más caliente en memoria RAM del proceso que atiende la consulta.

Si el dato ya está en memoria, la consulta resuelve en menos de un milisegundo. Si hay que ir a NVMe, tarda un poco más. Si no hay nada cacheado (una namespace que no se consultó en horas), hay que leer de object storage y ahí sí se nota la latencia. Por eso el primer query después de una pausa larga (un 'cold start') siempre es más lento que los siguientes.

Ejemplo práctico: una consulta con filtro

Así se ve, en código, una búsqueda vectorial con filtro de atributos sobre una colección de productos:

import turbopuffer

ns = turbopuffer.Namespace("catalogo-productos")

resultado = ns.query(
rank_by=("vector", "ANN", vector_consulta),
filters=("And", [
("categoria", "Eq", "zapatillas"),
("precio", "Lte", 100),
]),
top_k=10,
)

for doc in resultado.rows:
print(doc.id, doc.dist)

Esta consulta pide los 10 vectores más cercanos a vector_consulta, pero solo entre documentos de la categoría "zapatillas" con precio menor o igual a 100. Por abajo, el motor resuelve primero el filtro contra el índice invertido de atributos y después recorre únicamente esas direcciones ANN dentro del árbol de clusters, en vez de comparar contra todos los vectores de la namespace.

Cuándo NO conviene una base vectorial serverless

El diseño tiene un costo real: si necesitás latencia de un solo milisegundo garantizada en cada consulta, incluso en la primera después de horas sin tráfico, un caché que puede estar frío juega en contra. Para ese caso puntual conviene una base en memoria dedicada, aunque implique pagar por la máquina encendida todo el día. turbopuffer resuelve bien el caso común, consultas intermitentes con picos, no el caso de latencia extrema constante.

Conclusión

La arquitectura de turbopuffer muestra una idea aplicable más allá de la búsqueda vectorial: separar dónde vive la verdad (object storage, barato y durable) de dónde vive la velocidad (caché en niveles, caro pero rápido) permite ofrecer un servicio serverless sin sacrificar demasiado rendimiento. El cambio de v3, sacar el índice ANN del centro para que agregaciones y otras consultas no tengan que rodearlo, es la continuación lógica de esa misma idea.

📖 Versión extendida con más detalle: https://elsolitario.org/2026/10/01/turbopuffer-v3-arquitectura-vectorial-serverless/?utm_source=telegraph&utm_medium=instant_view&utm_campaign=programacion

Report Page