Sobre SIFT 1M (128 dimensiones, recall ≈ 0,99) una consulta única sostiene ~356 qps, y ~700 qps bajo carga concurrente en 8 núcleos. El índice es 14–21× más pequeño que los índices HNSW con los que se compara (pgvector, Qdrant) — en torno a 39 MB por millón de vectores —, así que cabe en una fracción de la RAM. La construcción es paralela y en streaming (≈186 s → 64 s), y nunca materializa el conjunto de datos, por lo que escala a decenas de millones de vectores en una máquina modesta.
Arkeion almacena vectores, no los genera
Arkeion no incluye modelos ni dependencias de ML — esa es una regla estricta, y es lo que mantiene el motor determinista y auditable. Los embeddings los produce tu propio modelo, en tu lado, y se le entregan a Arkeion como datos. El motor almacena floats opacos y los busca; nunca los interpreta. Lo que obtienes a cambio es que tus vectores heredan todo lo que el fichero ya garantiza: versionado, el historial encadenado por hashes, AS OF y verify().
Un vector no es más que datos
Un vector es un BLOB de f32 en little-endian: ningún tipo de columna nuevo, ningún cambio de formato. Los vectores se construyen y se comparan con un puñado de funciones escalares puras:
-- vector(x, y, …) pack floats into a BLOB
-- cosine_distance(a, b) 1 − cosine similarity; lower = more similar
-- l2_distance(a, b) Euclidean distance; lower = more similar
-- dot(a, b) dot product; higher = more similar
CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT, emb BLOB);
INSERT INTO docs (body, emb) VALUES ('…', vector(0.1, 0.2, 0.3)); -- or a BLOB from your client
SELECT id, body
FROM docs
ORDER BY cosine_distance(emb, vector(0.11, 0.19, 0.31))
LIMIT 10;
Las distancias se acumulan en f64, y una discrepancia de dimensiones es un error, no una respuesta errónea silenciosa. Ese ORDER BY … LIMIT ya es una búsqueda exacta de los K vecinos más próximos: recorre las filas, calcula la distancia a cada una y se queda con las K mejores. Como no es más que datos en el árbol versionado, añadir AS OF VERSION n busca en el pasado de forma semántica: la misma consulta, contra la base de datos tal y como estaba en cualquier commit.
El KNN exacto es el cimiento
El KNN exacto — fuerza bruta sobre todos los candidatos — es determinista, completo y reproducible. No son virtudes accidentales: son la referencia contra la que se mide un índice ANN (aporta la verdad de referencia para el recall) y la base sobre la que se apoya un paso de rerank. Para colecciones de hasta ~1M de vectores suele bastar por sí solo, sobre todo con vectores int8 (más abajo). Todo lo más rápido se construye encima de él, no en su lugar.
Cuantización int8
vector_i8() almacena un vector como un factor de escala más un byte con signo por dimensión (cuantización simétrica por vector, max|v| / 127). Un tag de un byte distingue los formatos (0x00 = f32, 0x01 = int8), y las funciones de distancia desempaquetan ambos de forma transparente: una consulta en f32 contra vectores almacenados en int8 simplemente funciona. El resultado es ~4× menos almacenamiento con una pérdida de precisión pequeña y acotada.
IVF e IVF-PQ: búsquedas que se saltan casi todos los datos
La fuerza bruta lo lee todo. A escala, lo que quieres es no leer casi nada. Un índice IVF (inverted file) agrupa los vectores con k-means y almacena, por cada clúster, una posting list con sus miembros. Una consulta solo tiene que recorrer el puñado de clústeres más cercanos — nprobe de ellos — en lugar de la colección entera.
Se crea con SQL, y el planificador enruta automáticamente hacia él las consultas que encajan:
CREATE VECTOR INDEX vi ON docs(emb) USING cosine LISTS 1024 PROBES 32;
-- an ORDER BY distance … LIMIT k (no WHERE) is now served by the index
SELECT id, body FROM docs
ORDER BY cosine_distance(emb, vector(0.11, 0.19, 0.31))
LIMIT 10;
LISTS es el número de clústeres y PROBES (nprobe) es cuántos recorre una consulta: el único mando entre recall y velocidad (por defecto ceil(lists / 10)). El índice vive en el catálogo desde el esquema v9, con el modo de rerank añadido en v10.
Por qué IVF y no HNSW
Los índices de grafo que dominan los benchmarks (HNSW) mutan en cada inserción. Eso es incompatible con un fichero versionado y copy-on-write: no puedes tener una traza de auditoría sobre una estructura que se reescribe constantemente. Los centroides IVF, en cambio, son datos versionados: se eligen en un evento discreto de CREATE o REBUILD y después se leen, no se mutan en cada inserción. Las filas nuevas se asignan al centroide existente más cercano, así que la agrupación deriva despacio; REBUILD VECTOR INDEX la reentrena sobre los datos actuales cuando quieras refrescarla. Sacrificas un poco de recall punta a cambio de un índice vectorial que viaja en el tiempo y se verifica igual que el resto de tus datos.
Códigos PQ frente a rerank int8 en línea
Para que las postings sean pequeñas, el índice almacena un código de cuantización por producto (PQ) por vector — en torno a dim / 8 bytes, unas 8× más pequeño que el mismo vector en f32. Eso es lo que hace que el índice completo sea 14–21× más pequeño que el HNSW de pgvector o de Qdrant. PQ reduce el campo a una shortlist; después el ORDER BY … LIMIT exacto rerankea esa shortlist para ganar precisión.
De dónde lee sus vectores el rerank es un compromiso real:
RERANK int8 guarda algo más por posting y reordena en línea, así que no hace falta una búsqueda por candidato.Ambos modos comparten una única ruta de código — la construcción, el insert/update/delete incremental, REBUILD y la búsqueda se rigen todos por si hay codebooks PQ o no —, así que cambiar de modo no bifurca el motor. Cambiarlo sí cambia el formato de las postings, de modo que un índice existente hay que reconstruirlo.
Búsqueda híbrida: léxica + semántica, fusionadas por rango
La búsqueda de texto completo (BM25) encuentra las palabras exactas; los vectores encuentran significados parecidos. Normalmente quieres las dos, y como BM25 y el coseno ya existen en SQL, la búsqueda híbrida no necesita ninguna funcionalidad nueva del motor: basta con una consulta. La forma robusta de combinarlas es Reciprocal Rank Fusion (RRF), que fusiona por rango en lugar de por puntuación bruta, con lo que la escala no acotada de BM25 y la escala [0, 2] del coseno nunca tienen que reconciliarse:
WITH ranked AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY bm25(body, 'rust') DESC) AS lex,
ROW_NUMBER() OVER (ORDER BY cosine_distance(emb, vector(1.0, 0.0, 0.0)) ASC) AS sem
FROM docs
)
SELECT id FROM ranked
ORDER BY 1.0 / (60 + lex) + 1.0 / (60 + sem) DESC
LIMIT 10;
Un fichero, tres maneras de hacer una pregunta — SQL exacto, texto completo y vectores — y una manera de combinarlas, todo versionado y demostrable en conjunto. Consulta los benchmarks para ver las cifras vectoriales completas.