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%).
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 |
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.
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_epi8midió 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.
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.