Índice vectorial en producción: cómo elegir el correcto
Cómo elegir el índice vectorial en producción: Flat, HNSW, IVF-PQ, DiskANN o ScaNN según escala, RAM, filtros y updates. Parámetros reales de pgvector y Qdrant.

Imagina una consulta sobre un índice vectorial que lleva meses funcionando. Busca los diez documentos más parecidos a la pregunta del usuario, pero solo dentro de su empresa. Un WHERE tenant_id = 42, un ORDER BY embedding <=> $1, un LIMIT 10.
Un día el cliente se queja de que el asistente "no encuentra nada". Revisas la tabla y los documentos están ahí. Ejecutas la consulta a mano y devuelve cuatro filas. No diez. Cuatro.
No hay bug en tu código. Elegiste un índice vectorial sin pensar en los filtros, y el índice hizo exactamente lo que dice su documentación. La propia documentación de pgvector lo explica con números: si el filtro deja pasar el 10 % de las filas, con el hnsw.ef_search por defecto de 40 obtienes unas 4 coincidencias de media.
En corto: no hay un índice vectorial mejor; hay uno adecuado para tu escala, tu RAM, tus filtros y tu ritmo de escritura. Por debajo de unas decenas de miles de vectores, búsqueda exacta; si caben en memoria, HNSW; si no, DiskANN o IVF con cuantización. Y si filtras mucho, elige el motor por cómo filtra, no por su benchmark de latencia.
Cómo funcionan HNSW e IVF por dentro ya lo conté en cómo funciona un vector database por dentro: HNSW y IVF. Aquí no repito el grafo ni los clusters. Este post es la parte siguiente: cómo decidir.
¿Qué es un índice vectorial?
Un índice vectorial es una estructura de datos que permite encontrar los vectores más cercanos a una consulta sin compararla contra todos, a cambio de devolver resultados aproximados. Esa palabra, aproximados, es el contrato que firmas. El parámetro que ajustas, sea ef_search, nprobe o diskann.query_search_list_size, decide cuánta precisión (recall) cambias por latencia.
La pregunta útil no es "¿qué índice vectorial es más rápido?", sino "¿qué recurso se me acaba antes?": RAM, tiempo de construcción, capacidad de filtrar o tolerancia a escrituras.
Las 5 estrategias para elegir índice en un vector database
Hay cinco estrategias de índice vectorial en producción: búsqueda exacta (Flat), HNSW, IVF con cuantización, DiskANN y ScaNN; se eligen por escala, RAM disponible, tipo de filtros y ritmo de escritura.
Esta es la tabla que yo tendría pegada al lado del monitor. Las cifras salen de la documentación de cada proyecto, enlazada en las secciones de abajo.
| Estrategia | Escala donde encaja | RAM | Recall | Construcción | Updates y filtros | Dónde la tienes | Riesgo principal |
|---|---|---|---|---|---|---|---|
| Flat (exacta) | Hasta decenas de miles de vectores, o pocas consultas | Solo los vectores | 100 %, exacto | Ninguna | Triviales; el filtro es un WHERE normal |
pgvector (sin índice), FAISS IndexFlat, Milvus FLAT |
La latencia crece lineal con los datos |
| HNSW | Hasta 10-20 M de vectores en memoria, según Google | Alta: vectores + grafo | Muy alto con parámetros por defecto | Lenta y hambrienta de memoria | Inserta sin reentrenar; filtrar depende del motor | pgvector, Qdrant (único índice denso), Milvus, FAISS | Post-filtrado que devuelve menos filas que tu LIMIT |
| IVF / IVF-PQ | De millones a cientos de millones | Baja con PQ o SQ8 | Depende de nprobe y del reranking |
Rápida, pero necesita entrenar con datos | Si la distribución cambia, hay que reentrenar | pgvector (IVFFlat), FAISS, Milvus (IVF_PQ, IVF_SQ8) |
Recall bajo con los valores por defecto |
| DiskANN | Datos que no caben en RAM | Baja: códigos PQ en memoria, grafo en SSD | Alto con rescoring | Lenta | Filtrado por etiquetas en pgvectorscale | pgvectorscale, Milvus DISKANN, Azure PostgreSQL (pg_diskann) |
Necesita disco NVMe; en Milvus va desactivado por defecto |
| ScaNN | Escala masiva; Google habla de hasta 10.000 M | Baja (cuantización SQ8 o AH) | Alto con re-ranking | Necesita datos antes de construir | Mantenimiento automático en AlloyDB | AlloyDB, Milvus SCANN, librería scann (solo Linux) |
Te ata a AlloyDB o a Milvus en producción |
Flat: cuándo la búsqueda exacta es la respuesta correcta
Flat significa comparar la consulta contra todos los vectores: el único método con resultados exactos. La guía de FAISS para elegir índice lo dice sin rodeos: usa Flat si necesitas resultados exactos o si vas a hacer pocas búsquedas, del orden de 1.000 a 10.000, porque el tiempo de construir un índice no se recupera (FAISS, Guidelines to choose an index).
En pgvector, Flat es el comportamiento por defecto. La documentación lo dice literal: "By default, pgvector performs exact nearest neighbor search, which provides perfect recall". No crear índice también es una decisión.
Google pone una frontera parecida en su guía de AlloyDB: búsqueda exacta (KNN) si tu aplicación es muy sensible a la precisión y tienes decenas de miles de vectores; búsqueda aproximada si necesitas baja latencia y pasas de 100.000 (AlloyDB, elegir estrategia de índice).
Flat gana además en un caso que se olvida: filtros muy selectivos. Si tu WHERE deja pasar 2.000 filas de 5 millones, comparar esas 2.000 a fuerza bruta es rápido y exacto. Milvus lo recomienda en su guía: si el filtro descarta más del 98 % de los datos, usa FLAT (Milvus, Index explained).
HNSW en pgvector: parámetros que importan y cuánta RAM pide
HNSW es el índice vectorial por defecto razonable cuando los vectores caben en memoria. pgvector lo resume así: tiene mejor equilibrio velocidad-recall que IVFFlat, pero construye más lento y usa más memoria. Y no necesita datos para crearse, porque no hay paso de entrenamiento.
Los tres parámetros y sus valores por defecto según la documentación de pgvector:
| Parámetro | Default en pgvector | Default en Qdrant | Qué controla |
|---|---|---|---|
m |
16 | 16 | Conexiones por nodo. Más recall, más RAM |
ef_construction / ef_construct |
64 | 100 | Candidatos al construir. Más recall, build más lento |
ef_search |
40 | (se pasa en la consulta) | Candidatos al buscar. Más recall, más latencia |
Los valores de Qdrant salen de su documentación de indexación, que además deja claro que "Qdrant currently only uses HNSW as a dense vector index". En Qdrant no eliges algoritmo: eliges parámetros y cuantización.
En SQL queda así:
-- Construcción: más rápida si el grafo cabe en maintenance_work_mem
SET maintenance_work_mem = 39;8GB39;;
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Consulta: sube ef_search solo en esta transacción
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SET LOCAL hnsw.iterative_scan = relaxed_order; -- pgvector 0.8.0+
SELECT id, title
FROM documents
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
COMMIT;
Ese iterative_scan es el arreglo del problema de la apertura. Desde la versión 0.8.0 (la actual es la 0.8.7), pgvector puede seguir recorriendo el índice hasta completar el LIMIT, con strict_order si necesitas el orden exacto por distancia o relaxed_order si prefieres más recall. El tope lo marca hnsw.max_scan_tuples, 20.000 por defecto.
Ahora la RAM. FAISS da la fórmula de HNSW: (d * 4 + M * 2 * 4) bytes por vector. Con embeddings de 1.536 dimensiones y M = 16 salen 6.272 bytes por vector. Un millón de vectores son unos 6,3 GB; cien millones, unos 627 GB. Con 768 dimensiones, 3,2 GB por millón.
La frontera real de HNSW como índice vectorial no es el número de vectores: es si 6 GB por millón caben en el servidor que pagas.
Antes de abandonar HNSW, prueba a cuantizar. pgvector indexa halfvec (media precisión, hasta 4.000 dimensiones frente a las 2.000 de vector) y bit con binary_quantize. La documentación recomienda reordenar con los vectores originales para recuperar recall:
CREATE INDEX ON documents
USING hnsw ((binary_quantize(embedding)::bit(1536)) bit_hamming_ops);
-- 50 candidatos baratos por Hamming, reordenados por coseno real
SELECT * FROM (
SELECT id, title, embedding
FROM documents
ORDER BY binary_quantize(embedding)::bit(1536) <~> binary_quantize($1::vector)
LIMIT 50
) AS candidatos
ORDER BY embedding <=> $1::vector
LIMIT 10;
IVF-PQ y cuantización: cuando la RAM es el cuello de botella
IVF divide los vectores en listas y solo revisa las más cercanas a la consulta; con Product Quantization (PQ) o cuantización escalar, además comprime cada vector. Es la estrategia para cuando la RAM manda.
Milvus da una comparativa de memoria para un millón de vectores de 128 dimensiones (Milvus, Index explained):
| Índice | Memoria aproximada (1 M × 128 dim) |
|---|---|
| HNSW (grafo + vectores FP32) | 640 MB |
| IVF-FLAT | 515 MB |
| HNSW-PQ | 136 MB |
| IVF-SQ8 | 131 MB |
| IVF-PQ con 10 % de refinado | 62,2 MB |
| IVF-PQ sin refinado | 11 MB |
De 640 MB a 11 MB. El precio es el recall, que recuperas en parte con una fase de reordenado sobre vectores originales.
Los parámetros que importan son el número de listas y cuántas revisas. En pgvector se llaman lists e ivfflat.probes. La documentación recomienda lists = filas / 1000 hasta un millón de filas y sqrt(filas) por encima, y empezar con probes = sqrt(lists). Ojo: probes vale 1 por defecto, así que un IVFFlat recién creado revisa una sola lista. Con 100 listas, estás mirando el 1 % de tus datos.
La otra trampa es el entrenamiento. pgvector pide crear el índice IVFFlat cuando la tabla ya tiene datos. FAISS pide entre 30 * K y 256 * K vectores de entrenamiento, siendo K el número de clusters. Si tu distribución cambia mucho (nuevo modelo de embeddings, nuevo tipo de documento), los centroides se quedan viejos y el recall cae sin avisar.
La cuantización no es exclusiva de IVF. Qdrant la aplica sobre HNSW: escalar (float32 a uint8, 4 veces menos), binaria (hasta 32 veces a 1 bit) y PQ (hasta 64 veces). Según su guía de cuantización, la binaria con text-embedding-3-small de 1.536 dimensiones llega a 0,9847 de recall con sobremuestreo de 3x y rescoring. Su recomendación de partida es exactamente esa: oversampling de 3 con rescoring activado.
DiskANN: cuando los vectores no caben en memoria
DiskANN guarda el grafo (Vamana) y los vectores completos en SSD, y en RAM solo mantiene códigos PQ comprimidos para puntuar candidatos. Busca en memoria con los códigos y lee del disco solo los vectores de los candidatos más prometedores (Milvus, DiskANN).
La regla de Milvus para decidir es la más clara que he encontrado: "If a quarter of your raw data fits into memory, consider DiskANN for its stable latency". Si cabe todo en RAM, usa índices en memoria.
En Milvus va desactivado por defecto (queryNode.enableDisk: true para activarlo) y recomiendan NVMe.
En Postgres tienes pgvectorscale, de Timescale (hoy Tiger Data), con su índice StreamingDiskANN, cuantización binaria estadística (SBQ) y filtrado por etiquetas integrado en el índice:
CREATE EXTENSION IF NOT EXISTS vectorscale CASCADE;
CREATE INDEX ON documents
USING diskann (embedding vector_cosine_ops, labels);
SELECT id, title
FROM documents
WHERE labels && ARRAY[1, 3] -- labels es smallint[]
ORDER BY embedding <=> $1
LIMIT 10;
Defaults de construcción: num_neighbors 50, search_list_size 100, max_alpha 1,2. En consulta, diskann.query_search_list_size 100 y diskann.query_rescore 50. Timescale publica un benchmark con 50 millones de embeddings de 768 dimensiones, con 28x menos latencia p95 que Pinecone s1 al 99 % de recall. Es su propio benchmark y compara contra pods s1, no contra Pinecone serverless: tómalo como hipótesis para medir con tus datos, no como garantía.
ScaNN: cuantización anisotrópica para escala masiva
ScaNN es la librería de Google que combina particionado, cuantización anisotrópica y re-ranking, optimizada para búsqueda por producto interno. La cuantización anisotrópica comprime los vectores preservando sobre todo la componente que afecta al producto interno (Milvus, SCANN).
La encuentras en tres sitios. Como librería (pip install scann, solo Linux). Como índice SCANN en Milvus, con nlist en construcción y nprobe (8 por defecto) y reorder_k en consulta. Y en AlloyDB con la extensión alloydb_scann, donde Google afirma hasta 6 veces más velocidad y hasta 4 veces menos memoria que HNSW en PostgreSQL estándar (AlloyDB, elegir estrategia de índice).
Dos detalles de AlloyDB que muerden: el cuantizador por defecto es SQ8, con pérdida de recall típica por debajo del 1-2 %. Y por defecto no puedes crear el índice en una tabla vacía ni con menos filas que num_leaves.
Mi opinión: ScaNN merece la pena si ya estás en Google Cloud o en Milvus. No migraría de plataforma solo por él.
El filtrado por metadatos decide más que el algoritmo
Con filtros por metadatos, un índice aproximado puede devolver menos filas que tu LIMIT, porque muchos motores filtran después de buscar; por eso cómo filtra el motor pesa más que su latencia. En la práctica, la mayoría de consultas RAG llevan filtro: tenant, idioma, permisos, fecha. Y cada índice vectorial aproximado filtra de una forma muy distinta.
En el hilo de Hacker News sobre el artículo "Vector databases are the wrong abstraction", uno de sus coautores, ingeniero de Timescale, lo explicó así: "In pgvector it usually does post-filtering in practice (filter after vector search)". Y añadía que si ninguno de los C candidatos cumple el filtro, no encuentra resultados, aunque él mismo reconocía que las versiones nuevas de pgvector lo abordan (comentario en HN, octubre 2024). Ojo con el sesgo: quien lo dice trabaja en la empresa que vende la alternativa.
Otro developer, con experiencia en "hundreds of millions of embeddings", describía en otro hilo, meses antes de la 0.8.0, los bordes afilados de pgvector a esa escala: el vacuum de índices de cientos de GB y lo difícil que es razonar sobre el recall (comentario en HN, febrero 2024).
Las tres soluciones que existen hoy:
- Escaneo iterativo (pgvector 0.8.0+): sigue buscando hasta completar el
LIMIT. - Filtrado dentro del índice: Qdrant añade aristas al grafo HNSW según los payloads indexados, y pgvectorscale filtra por etiquetas. En Qdrant hay que crear los índices de payload antes de cargar datos; si los creas después, hay que reconstruir el HNSW.
- Fuerza bruta sobre el subconjunto filtrado, cuando el filtro es muy selectivo.
Si tu RAG combina filtros con texto, mira también la búsqueda híbrida con embeddings en Supabase: ahí el índice es solo una de las piezas.
Qué índice vectorial elegir según tu escala: árbol de decisión
- Menos de ~50.000 vectores: sin índice. Búsqueda exacta, recall perfecto, cero mantenimiento.
- De ~50.000 a ~10 millones, caben en RAM: HNSW con los defaults. Mide recall contra Flat sobre una muestra y sube
ef_searchantes de tocarm. - Igual, pero la RAM aprieta: HNSW con
halfveco cuantización binaria más reordenado. Después, IVF-SQ8 o IVF-PQ. - Decenas o cientos de millones, sin RAM para todo: DiskANN (pgvectorscale o Milvus) o IVF-PQ con reordenado.
- Filtros muy selectivos: prioriza un motor que filtre dentro del índice, o fuerza bruta sobre lo filtrado.
- No quieres operar nada: Pinecone. Al crear un índice declaras dimensión, métrica y tipo de vector; el algoritmo no lo eliges tú. Lo conté en la introducción a Pinecone para agentes.
La memoria vectorial es una pieza más de un agente. Si estás montando el resto, en el ebook gratuito El Developer Agéntico tienes la arquitectura completa: tools, memoria, RAG y orquestación.
Cuándo NO necesitas un índice vectorial
Si tienes pocos datos, no necesitas índice. Con decenas de miles de vectores, el índice añade construcción, RAM y recall aproximado a cambio de milisegundos que nadie va a notar. Es el caso de la mayoría de chatbots sobre la documentación de un producto.
Si tus consultas necesitan exactitud, un índice aproximado es una fuente de bugs silenciosos. Deduplicación, detección de fraude o cumplimiento normativo: ahí un falso negativo cuesta más que la latencia.
Si filtras casi todo, el índice estorba. Con filtros que descartan más del 95-98 % de los datos, Milvus recomienda FLAT.
Un índice no arregla malos embeddings. Si el modelo no separa bien tus documentos, el recall perfecto de Flat te devolverá los vecinos equivocados con total precisión. Antes de ajustar ef_search, revisa el chunking y el modelo; el RAG avanzado híbrido suele aportar más que cualquier parámetro.
Y un límite de este post: todas las cifras son de la documentación de cada proyecto, consultada en noviembre de 2026, no de benchmarks míos. Varían con la dimensión, la distribución y el hardware. La única tabla que vale es la que generes con tus datos.
Qué hacer con tu índice esta semana
Abre tu base de datos y responde a una pregunta: ¿cuál es el recall real de tu índice vectorial en la consulta más importante? Toma 100 consultas, ejecútalas con y sin índice (en Postgres, dentro de una transacción con SET LOCAL enable_indexscan = off, como recomienda la documentación de pgvector, tienes la exacta sin que el cambio se filtre al resto de la sesión) y compara los resultados. Si no lo has medido nunca, no sabes si tu índice funciona; solo sabes que es rápido.
Si quieres construir el producto entero alrededor de esa búsqueda, del spec al despliegue, es lo que hacemos en el curso Construye con IA: de la idea al producto con Claude Code.
Preguntas frecuentes
¿Qué índice vectorial uso en pgvector por defecto?
HNSW, si tienes más de unas decenas de miles de filas. Su documentación le atribuye mejor equilibrio velocidad-recall que IVFFlat, y no necesita datos previos para crearse. Empieza con m = 16 y ef_construction = 64, y ajusta hnsw.ef_search (40 por defecto) en la consulta.
¿Cuánta RAM necesita un índice HNSW?
Según la fórmula de FAISS, (d * 4 + M * 2 * 4) bytes por vector. Con 1.536 dimensiones y M = 16 son unos 6,3 GB por millón de vectores. Con halfvec o cuantización binaria se reduce mucho, a cambio de un reordenado para recuperar recall.
¿Por qué mi consulta con WHERE devuelve menos resultados que el LIMIT?
Porque el índice aproximado devuelve un número fijo de candidatos y el filtro se aplica después. En pgvector, con un filtro que deja pasar el 10 % y ef_search = 40, obtienes unas 4 filas. Desde la 0.8.0 lo arreglas con hnsw.iterative_scan.
¿HNSW vs DiskANN: cuándo compensa cada uno?
HNSW, mientras los vectores caben en RAM; DiskANN, cuando no caben. Milvus recomienda considerar DiskANN si cabe en memoria una cuarta parte de los datos. Necesitas SSD, idealmente NVMe. En Postgres lo tienes con pgvectorscale y en Milvus con DISKANN.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Quieres construir con agentes en serio?
Curso «Construye con IA» con Claude Code, de la idea al producto.
Developer con más de 15 años en frontend y creador de Dominicode. Escribe sobre IA aplicada al desarrollo de software.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.