Versión v0.13

Primero, los números

SIFT-1M — el benchmark ANN estándar: un millón de vectores de 128 dimensiones, 10,000 consultas reales, ground truth incluido. Misma máquina, misma configuración, solo cambió el motor. Y como la construcción del índice de Arkeion es determinista, ambas versiones entrenan exactamente los mismos clústeres: el recall es idéntico byte a byte en cada punto de la curva. Lo que ganas es velocidad pura:

| nprobe | recall@10 | v0.12 | v0.13 | | |—|—|—|—|—| | 10 | 0.850 | 2.73 ms | 1.10 ms | 2.5× | | 20 | 0.938 | 4.09 ms | 1.39 ms | 2.9× | | 50 | 0.988 | 9.12 ms | 2.48 ms | 3.7× | | 100 | 0.996 | 15.8 ms | 3.93 ms | 4.0× | | 400 | 0.997 | 58.9 ms | 11.7 ms | 5.0× |

El rendimiento concurrente en el punto de operación recall@10 = 0.988 pasa de 375 a 1,381 consultas por segundo (8 hilos, 4 núcleos). El escaneo KNN exacto (sin índice) baja de 1.34 s a 322 ms por consulta. El índice vectorial se encoge de 38.8 MB a 25.4 MB (−35%).

Milisegundos por consulta — menos es mejor v0.13 v0.12 recall 0.94 recall 0.988 recall 0.996 1.39 ms 4.09 ms 2.48 ms 9.12 ms 3.93 ms 15.8 ms 2.9× más rápido 3.7× más rápido 4.0× más rápido 0 5 ms 10 ms 15 ms
SIFT-1M, un solo hilo, consulta a consulta — mismos clusters, mismo recall, solo cambió el motor.

Nada de la semántica se movió. Construcciones deterministas, historial versionado, AS OF sobre la búsqueda: todo intacto. Los empates en una shortlist ahora incluso se resuelven de forma determinista (por rowid), independientemente del orden de escaneo o del número de hilos.

La pregunta de FAISS

La comparación que todo el mundo quiere de verdad: FAISS, la biblioteca ANN de referencia. Misma máquina, mismo SIFT-1M, configuración idéntica en ambos lados — IVF con 1,000 listas, códigos PQ16, rerank exacto sobre una shortlist ×32 (IVF1000,PQ16,RFlat con k_factor=32 en términos de FAISS). Consulta a consulta, un hilo:

| recall@10 | FAISS | Arkeion v0.13 | v0.12, como referencia | |—|—|—|—| | 0.85 | 0.42 ms | 1.10 ms | 2.73 ms | | 0.94 | 0.60 ms | 1.39 ms | 4.09 ms | | 0.99 | 1.32 ms | 2.48 ms | 9.12 ms | | 0.999 | 5.62 ms | 11.7 ms | 58.9 ms |

Recall@10 vs latencia — escala logarítmica v0.13 FAISS v0.12 1.00 0.90 0.80 0.70 recall 0.99 FAISS v0.13 v0.12 0.5 1 2 5 10 20 50 milisegundos por consulta (log)
La misma subida hasta recall 0,99: FAISS llega en ~1,3 ms en RAM, v0.13 en ~2,5 ms desde un fichero duradero, v0.12 necesitaba ~9 ms.

FAISS lo mantiene todo en RAM y no promete nada: sin durabilidad, sin transacciones, sin versionado, sin cifrado, sin SQL — si el proceso muere, reentrenas. Arkeion sirve el mismo recall desde un fichero cifrado, versionado y a prueba de caídas, en dos milisegundos en lugar de uno. Antes de v0.13 esa brecha era de 7–10×; ahora es el precio de ser una base de datos, y creemos que es el precio correcto. (Una nota metodológica: FAISS sin la etapa de rerank exacto se topa con un techo de recall 0.56 en esta configuración — si has visto por ahí cifras de IVFPQ sospechosamente rápidas, comprueba eso.)

Qué se publicó

Postings en bloques. El índice antiguo guardaba una celda de b-tree por vector: cada candidato pagaba una visita completa a la celda —decodificar, comprobar la clave, callback— antes incluso de calcular su distancia. En v0.13 cada clúster guarda bloques columnares de códigos y rowids (~3 KB por celda), así que el kernel de distancia recorre memoria contigua y el peaje del b-tree se amortiza a lo largo de un bloque. Este es el cambio de formato que hay detrás tanto de la velocidad como del índice más pequeño. Tus índices vectoriales existentes siguen funcionando intactos en el formato antiguo; REBUILD VECTOR INDEX los migra cuando tú decidas.

El KNN filtrado usa el índice. La consulta real más común —WHERE tenant_id = ? ORDER BY distance LIMIT k— solía caer en un escaneo completo exacto, porque una cláusula WHERE desactivaba por completo el plan vectorial. Ahora el planificador sobremuestrea la shortlist, filtra fila a fila, escala a todos los clústeres si el filtro es voraz, y solo recurre al escaneo exacto cuando ni así puede garantizar k supervivientes. Correcto por construcción; rápido en el caso común.

Una palanca que faltaba. CREATE VECTOR INDEX … FACTOR n controla cuánto sobremuestrea el índice antes del rerank exacto (por defecto 32, el antiguo valor hardcodeado). Unos buenos codebooks PQ están contentos con 8 —una cuarta parte de las lecturas de fila—; los datos difíciles pueden pedir más. Ahora es una decisión por índice, no nuestra.

Los silenciosos. El camino SQL cachea los centroides decodificados como la API ya hacía siempre (~2 ms de cada consulta, fuera). Los cursores del b-tree descienden dentro de la página en lugar de materializar cada nodo interno — eso solo llevó el escaneo de clúster de 2.9 ms a 1.2 ms, y aceleró el KNN exacto como efecto secundario. Los aciertos de la caché de páginas ya no tocan el lock global del pager, que era donde se escondía el escalado concurrente. Una tabla de lookup ADC plana, un heap de candidatos acotado, un rerank paralelo para el camino sensible a la latencia.

En qué gasta el tiempo una consulta ahora metadatos del índice centroides + codebooks PQ decodificado una vez, cacheado antes ~2 ms en cada consulta SQL · ahora 0 recorrido de clusters postings por bloques · LUT plana heap acotado · poda por radio descensos b-tree en página 2.9 → 1.2 ms rerank exacto k × FACTOR candidatos solo la columna de vectores leídos en orden de rowid paralelo, opcional 9.12 ms → 2.48 ms de extremo a extremo SIFT-1M · recall@10 = 0.988 · un solo hilo
Cada etapa mantuvo sus garantías: construcción determinista, rerank exacto, historial versionado.

Lo que nos negamos a publicar

Perfilamos antes de optimizar, y medimos antes de hacer merge. Tres ideas murieron así, y creemos que el cementerio merece publicarse:

  • Un multi-get de cursor compartido para el fetch del rerank: elegante, y 20× más lento con rowids dispersos. Diagnosticar por qué llevó directo al arreglo del descenso dentro de la página de arriba — el prototipo se pagó solo, y luego se borró.
  • Un kernel SIMD entero para escaneos int8: 1.01×. LLVM ya autovectoriza el camino en float; el error de cuantización extra no compró nada.
  • Fastscan de 4 bits al estilo FAISS: un prototipo AVX2 real con shuffle_epi8 midió 1.9× — sobre un kernel que ahora es ~15% del escaneo. Un cambio de formato más una exención de código unsafe a cambio de un ~8% de punta a punta es un mal trato. Nuestro ADC escalar corre a 8 ns por candidato; lo revisitaremos cuando el resto del escaneo lo alcance.
  • Paridad total con FAISS. Sabemos cómo cerrar el 2× restante: mantener una copia plana de cada vector dentro del índice (lo que FAISS llama RFlat) para que el rerank exacto nunca toque el árbol de filas. Funciona — y cuesta +512 MB por millón de vectores a 128 dimensiones, +3 GB por millón a 768. Decidimos que un milisegundo es más barato que un gigabyte: el índice de 25 MB se queda, el milisegundo extra se queda, y tu disco sigue siendo tuyo. Si algún día las cargas de trabajo reales opinan lo contrario, puede llegar más adelante como opción activable — nunca como valor por defecto.
El cementerio, medido — ganancia de extremo a extremo sin ganancia funciona, muy caro 1.0× = sin cambio multi-get con cursor compartido kernel entero int8 fastscan de 4 bits acompañante de rerank plano 0.19× lectura 20× más lenta con rowids dispersos 1.01× — LLVM ya lo había vectorizado 1.08× — kernel 1.9× en el 15% del recorrido 1.9× · +0.5 GB por M vectores 0.5× 1.5× las barras arrancan en 1.0× — a la izquierda de la línea es una regresión
Cuatro ideas, cuatro mediciones, un superviviente por méritos propios — muerto por su propia factura de disco.

Una base de datos que te promete un historial demostrable también debería demostrar sus afirmaciones de rendimiento. Cada número de arriba viene de un benchmark reproducible, y cada optimización rechazada viene con la medición que la mató.

Actualizar

cargo update -p arkeion a 0.13.0. Los ficheros existentes se abren como siempre; los índices vectoriales existentes sirven consultas en su formato actual. Ejecuta REBUILD VECTOR INDEX <name> por índice cuando quieras el formato de bloques y la nueva velocidad — es un evento discreto y determinista, como cada construcción en Arkeion.