Bases de datos vectoriales a fondo: el corazón de RAG
En el capítulo anterior vimos el flujo completo de RAG con un solo párrafo de ejemplo. Pero en la realidad, la base de conocimientos de RR.HH. de una empresa mediana fácilmente tiene cientos de documentos — manuales, políticas, actualizaciones anuales, contratos tipo — que, una vez fragmentados, pueden convertirse en decenas de miles de fragmentos individuales.
Ahí es donde una base de datos vectorial deja de ser un detalle de implementación y se convierte en una pieza de arquitectura real.
¿Qué es una base de datos vectorial?
A diferencia de una consulta tradicional que busca por coincidencia exacta (“dame todos los registros donde departamento = RRHH”), una base de datos vectorial busca por cercanía semántica (“dame los fragmentos que signifiquen algo parecido a esta pregunta”).
Analogía: una base de datos tradicional es como un catálogo ordenado alfabéticamente — necesitas saber el título exacto o el autor. Una base de datos vectorial es como un bibliotecario que leyó todos los libros y los organizó en un espacio donde los libros de temas similares están cerca físicamente. Le preguntas “¿cómo funcionan las vacaciones pagadas?” y te lleva a la sección de beneficios laborales, aunque ningún libro tenga la palabra “vacaciones” en el título.
Por qué no basta un índice tradicional, con números concretos
Un vector de embeddings típico tiene 768 dimensiones. En precisión estándar (float32, 4 bytes por número), son 3,072 bytes por vector — solo para el significado de un fragmento, sin el texto original. Con 50,000 fragmentos: aproximadamente 150MB en vectores, unos 50MB en texto y alrededor de 200MB total para una base mediana.
El problema real no es el almacenamiento — es la búsqueda. Una base tradicional es excelente para “dame los documentos donde fecha = 2026” (indexable con B-trees). Pero “¿cuál de estos 50,000 vectores es matemáticamente más parecido a este otro?” no es una relación de orden simple que un B-tree pueda indexar. La forma exacta más directa sería calcular similitud contra cada uno de los 50,000 vectores, uno por uno — factible a esa escala, costosa con millones.
Esto no significa que SQL y búsqueda vectorial sean excluyentes: extensiones como pgvector agregan índices vectoriales a PostgreSQL. La distinción importante está entre un índice tradicional de valores escalares y un índice especializado para vecinos cercanos.
Indexado: cómo HNSW evita comparar contra todo
HNSW (Hierarchical Navigable Small World) organiza los vectores en capas: una capa superior con pocos puntos muy conectados (como ver solo las autopistas principales en un mapa), y capas inferiores cada vez más densas (como ver todas las calles).
Analogía del aeropuerto: imagina un aeropuerto con 50,000 puertas, buscando la más cercana a una cafetería. Búsqueda lineal: caminas por las 50,000, una por una. HNSW: el aeropuerto está organizado en niveles — en el nivel superior solo ves las terminales principales, caminas hacia la más cercana, bajas un nivel, repites. En cada paso descartas regiones enteras sin visitarlas.
El resultado práctico: la búsqueda lineal crece con el tamaño de la base (O(N) — el doble de documentos implica aproximadamente el doble de comparaciones). La búsqueda HNSW suele mostrar un crecimiento aproximadamente logarítmico (O(log N) en condiciones típicas), mucho más lento que el lineal. Esta diferencia de comportamiento —no una cifra específica de milisegundos, que depende enormemente del hardware, los datos y la configuración— es lo que permite que sistemas con millones de vectores respondan en tiempos utilizables en producción.
HNSW es un índice aproximado — sacrifica una pequeña posibilidad de no encontrar el resultado teóricamente óptimo, a cambio de velocidad muchísimo mayor. En la práctica esa pérdida se puede ajustar con parámetros de configuración (ver la sección de código más abajo) y debe medirse como recall sobre datos reales.
Cuantización binaria: comprimiendo vectores para mayor velocidad
3,072 bytes por vector en precisión completa. La cuantización binaria (usada por Qdrant, Milvus y otros) comprime cada uno de los 768 números a un solo bit: por encima de cierto punto de referencia → 1, por debajo → 0. Resultado: 768 bits = 96 bytes por vector — 32 veces más pequeño.
Analogía: es como convertir una foto de alta resolución a blanco y negro de 2 tonos. Pierdes detalle, pero la imagen es mucho más pequeña y rápida de procesar.
Comparar cadenas de bits es mucho más barato que comparar vectores decimales completos. Qdrant reporta mejoras de velocidad de hasta 40× para su implementación, aunque el resultado real depende de la dimensionalidad y de la distribución de los vectores; no es una garantía para cualquier dataset.
Estrategia de dos etapas, común en producción:
- Primer filtro (rápido): busca candidatos usando la representación binaria — operaciones de bits, muy veloces.
- Segundo filtro (preciso): toma los mejores candidatos del primer filtro y recalcula la similitud completa usando los vectores originales sin comprimir, solo sobre ese subconjunto reducido.
El resultado combina la velocidad de la búsqueda binaria con la precisión de los vectores completos. El tamaño del conjunto que se reevalúa debe afinarse con evals de recuperación, no asumirse como universal.
La arquitectura de una base de datos vectorial (con Qdrant)
Colección: el contenedor de todos los fragmentos — como una “tabla” en SQL. Todos sus vectores comparten dimensionalidad y métrica de distancia.
Puntos: cada fragmento es un punto, con un vector, un payload (metadatos JSON) y un ID único:
{
"id": "policy_12345",
"vector": [0.12, -0.34, 0.56, "... 768 números ..."],
"payload": {
"texto": "Los empleados con 2 a 5 años de antigüedad tienen derecho a 20 días hábiles de vacaciones al año.",
"documento": "politica_vacaciones_2026",
"seccion": "antiguedad",
"fecha_actualizacion": "2026-01-15",
"vigente": true,
"departamento": "RRHH"
}
}
Métrica de distancia:
| Métrica | Descripción | Cuándo usarla |
|---|---|---|
| Coseno | Mide el ángulo entre vectores, independiente de la magnitud | Cuando el modelo de embeddings recomienda similitud coseno |
| Euclidiana | Distancia en línea recta | Cuando la geometría y magnitud del espacio entrenado tienen significado |
| Producto punto | Combina dirección y magnitud | Cuando el modelo fue entrenado para producto punto; con vectores normalizados equivale al coseno |
La métrica no se elige solo por la modalidad del dato: debe coincidir con la función usada al entrenar el modelo de embeddings.
Filtros: resolviendo el problema de versiones desactualizadas
¿Qué pasa si la política de vacaciones se actualizó, pero el documento viejo sigue en la base? La búsqueda por similitud podría recuperar la versión desactualizada perfectamente bien — la similitud vectorial no sabe cuál versión es la vigente.
Los filtros combinan búsqueda vectorial con condiciones exactas sobre el payload:
from qdrant_client import models
resultados = client.query_points(
collection_name="politicas_rrhh",
query=vector_pregunta.tolist(),
query_filter=models.Filter(
must=[
models.FieldCondition(
key="vigente",
match=models.MatchValue(value=True),
),
models.FieldCondition(
key="departamento",
match=models.MatchValue(value="RRHH"),
),
]
),
limit=5,
with_payload=True,
).points
El sistema no busca “lo más parecido entre todo” — busca “lo más parecido, solo entre lo que además cumple esta condición exacta”. Guardas ambos documentos (2025 y 2026), con vigente: true solo para el actual, y el filtro asegura que solo se recupere la versión correcta.
En colecciones grandes conviene crear índices de payload para los campos usados frecuentemente en filtros; de lo contrario, el filtro exacto también puede convertirse en un cuello de botella.
Más allá del texto: vectores para imágenes, audio y video
Sí se pueden vectorizar imágenes, audio y video — pero hay una distinción clave que vale la pena tener clara.
El vector es una representación numérica del contenido. Se guarda en la base de datos vectorial. El objeto original (la imagen, el audio, el video) se guarda en almacenamiento de objetos (S3, Google Cloud Storage, un bucket local) con una referencia en el payload.
Analogía: el vector es como el resumen temático de un libro en el catálogo; el objeto original es el libro físico en la estantería; el payload es la referencia que dice en qué pasillo está.
Para imágenes: modelos como CLIP (de OpenAI, genera embeddings de texto e imagen en el mismo espacio vectorial — permitiendo buscar imágenes con texto y viceversa), ResNet (extrae características de visión por computadora) o DINOv2 (de Meta, auto-supervisado). El proceso: la imagen se vectoriza con CLIP, el vector va a la base de datos junto con la URL de la imagen real en el payload. Cuando alguien busca en texto (“muéstrame imágenes de productos con arañazos”), la pregunta se vectoriza con el mismo modelo CLIP y, como texto e imagen comparten espacio vectorial, encuentra las imágenes relevantes.
Para audio: CLAP (Contrastive Language-Audio Pretraining) genera representaciones conjuntas de audio y texto. Wav2Vec 2.0 produce representaciones de audio útiles para tareas posteriores. Whisper se usa principalmente para transcribir: una estrategia común es convertir el audio a texto y generar el embedding de esa transcripción. El archivo de audio original permanece en almacenamiento de objetos.
Para video: se pueden extraer fotogramas clave y vectorizarlos como imágenes, combinar embeddings de frames con subtítulos o usar modelos específicos como VideoCLIP, que alinea representaciones de video y lenguaje. Un video largo suele dividirse en segmentos, cada uno con su propio vector, marcas de tiempo y referencia al archivo original.
Casos de uso reales: búsqueda visual en e-commerce (“encuentra zapatos similares a esta foto”), asistentes de voz que recuperan contexto más allá de coincidencias literales y búsqueda de momentos relevantes dentro de horas de video.
Casos de uso, en una palabra: “match”
| Dominio | Ejemplo | Qué se vectoriza |
|---|---|---|
| Recomendación | Spotify ajustando sugerencias según qué saltas o repites | Canciones y señales de preferencia |
| Búsqueda semántica | Nuestro sistema de RR.HH. | Texto de documentos y preguntas |
| Detección de fraude | Transacciones atípicas en finanzas | Comportamiento transaccional |
| CBIR | Imágenes similares en un catálogo | Imágenes con modelos como CLIP o DINOv2 |
| Búsqueda de video | El momento donde se habla de “transformer” en una conferencia | Frames, segmentos y subtítulos |
| Biología computacional | Proteínas con secuencias o estructuras similares | Secuencias de aminoácidos o estructuras |
Seguridad y control de acceso
Nuestra base de RR.HH. probablemente tiene información que no todos deberían consultar igual — políticas salariales, por ejemplo.
Filtrado basado en roles (RBAC):
from qdrant_client import models
def buscar_con_acceso(consulta, usuario):
vector = modelo_embeddings.encode(consulta)
if usuario.rol == "admin":
filtro = None
elif usuario.rol == "rrhh":
filtro = models.Filter(
must=[
models.FieldCondition(
key="departamento",
match=models.MatchValue(value="RRHH"),
)
]
)
else:
filtro = models.Filter(
must=[
models.FieldCondition(
key="departamento",
match=models.MatchValue(value=usuario.departamento),
),
models.FieldCondition(
key="nivel",
match=models.MatchValue(value="publico"),
),
]
)
resultados = client.query_points(
collection_name="politicas_empresa",
query=vector.tolist(),
query_filter=filtro,
limit=5,
with_payload=True,
).points
registrar_consulta(usuario.id, consulta, resultados)
return resultados
El filtro de autorización debe construirse en el servidor a partir de una identidad ya autenticada; nunca debe aceptarse directamente desde parámetros controlados por el cliente. También hacen falta cifrado en reposo y en tránsito, gestión de secretos, aislamiento entre tenants y registros de auditoría. El algoritmo y los estándares concretos dependen del modelo de amenazas y del régimen aplicable: una regulación como GDPR exige controles adecuados al riesgo, no prescribe por sí sola una única cifra o algoritmo de cifrado.
Implementación práctica con Qdrant
docker run --rm -p 6333:6333 -p 6334:6334 \
-v "$(pwd)/qdrant_storage:/qdrant/storage" \
qdrant/qdrant
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="politicas_rrhh",
vectors_config=models.VectorParams(
size=768,
distance=models.Distance.COSINE,
),
hnsw_config=models.HnswConfigDiff(
m=16, # más conexiones: más recall y más memoria
ef_construct=100, # más candidatos al construir: más recall, indexación más lenta
),
)
# Insertar una sola vez, durante la indexación
points = []
for i, chunk in enumerate(chunks):
embedding = modelo_embeddings.encode(chunk["texto"])
points.append(
models.PointStruct(
id=i,
vector=embedding.tolist(),
payload={
"texto": chunk["texto"],
"documento": chunk["documento"],
"vigente": True,
},
)
)
client.upsert(
collection_name="politicas_rrhh",
points=points,
)
# Buscar con filtro en cada consulta
def buscar_en_rrhh(pregunta, solo_vigentes=True):
vector_pregunta = modelo_embeddings.encode(pregunta)
filtro = (
models.Filter(
must=[
models.FieldCondition(
key="vigente",
match=models.MatchValue(value=True),
)
]
)
if solo_vigentes
else None
)
resultados = client.query_points(
collection_name="politicas_rrhh",
query=vector_pregunta.tolist(),
query_filter=filtro,
search_params=models.SearchParams(
hnsw_ef=128, # más candidatos: más recall, búsqueda más lenta
),
limit=3,
with_payload=True,
).points
return [hit.payload["texto"] for hit in resultados]
m controla cuántas conexiones mantiene cada nodo del grafo y ef_construct cuánto explora el constructor del índice. En consulta, hnsw_ef controla cuántos candidatos recorre la búsqueda: subirlo suele mejorar el recall, a cambio de más latencia. Los valores mostrados son un punto de partida didáctico, no una configuración universal; deben medirse sobre los embeddings y la carga reales.
Resumen del capítulo
- Una base de datos vectorial es un motor de búsqueda por significado, no solo por coincidencia exacta.
- La búsqueda lineal crece con el tamaño de la base (O(N)); índices como HNSW suelen lograr un crecimiento aproximadamente logarítmico en condiciones típicas.
- Se organiza en colecciones → puntos → vector + payload. Los metadatos del payload permiten filtros que combinan búsqueda semántica con condiciones exactas.
- Maneja multimodalidad: el vector va a la base de datos vectorial, el objeto original (imagen, audio, video) va a almacenamiento de objetos, conectados por una referencia.
- Necesita controles de seguridad reales: filtrado por rol, cifrado, aislamiento y auditoría.
En el siguiente capítulo (5c) veremos el Protocolo de Contexto de Modelo (MCP) — cómo el modelo no solo consulta información, sino que ejecuta acciones sobre el mundo real. La base de datos vectorial resuelve “¿qué debo saber?”; el MCP resuelve “¿qué debo hacer?”.
LinkedIn nelson.zepeda@simov.io SIMOV LABS
¿Encontraste un error? Sugerir una corrección