Vector embeddings: qué son y cómo usarlos en TypeScript
Imagina que cambias de modelo de vector embeddings un lunes. El nuevo da mejores resultados en los benchmarks, así que reindexas los documentos nuevos con él y dejas los antiguos como estaban. Pides las mismas 1.536 dimensiones para no tocar el esquema de la base de datos.
No salta ningún error. Los tests pasan. Y la búsqueda empieza a devolver cosas sin sentido para la mitad de las consultas.
Has mezclado vectores de dos modelos distintos en el mismo índice. Tienen la misma longitud, pero no viven en el mismo espacio. Compararlos es como medir la distancia entre una coordenada de Madrid y otra de un mapa de Tokio dibujado con otra escala. La cuenta sale, pero no significa nada.
En corto: un embedding es una lista de números que representa el significado de un texto; dos textos parecidos dan vectores cercanos. Con OpenAI se generan con client.embeddings.create, se comparan con similitud coseno y salen normalizados a longitud 1. Los fallos típicos en producción son mezclar modelos o dimensiones, recortar vectores sin renormalizar y usarlos donde hace falta coincidencia exacta.
¿Qué son los vector embeddings?
Un vector embedding es una representación numérica de un texto, una lista de números en coma flotante, en la que la distancia entre dos vectores mide cuánto se parecen sus significados.
La guía oficial de OpenAI lo dice así: distancias pequeñas sugieren mucha relación y distancias grandes, poca. De ahí salen la búsqueda semántica, la clasificación, el clustering, las recomendaciones y la detección de anomalías.
Un número suelto no dice nada: en los vector embeddings importa la posición de cada vector respecto a otros generados por el mismo modelo.
Qué modelos de embeddings ofrece OpenAI en noviembre de 2026
OpenAI tiene tres modelos de embeddings en su catálogo a 5 de noviembre de 2026, y text-embedding-3-small es el que usa la guía en sus ejemplos. Datos de la guía, la referencia de la API y la página de precios:
| Modelo | Dimensiones por defecto | Máx. entrada | Precio por 1M tokens | MTEB | Cuándo usarlo |
|---|---|---|---|---|---|
text-embedding-3-small |
1.536 | 8.192 tokens | 0,02 USD | 62,3 % | Punto de partida para casi todo |
text-embedding-3-large |
3.072 | 8.192 tokens | 0,13 USD | 64,6 % | Cuando has medido que small no llega; OpenAI lo da como el mejor en inglés y otros idiomas |
text-embedding-ada-002 |
1.536 | 8.192 tokens | 0,10 USD | 61,0 % | Solo si tienes un índice antiguo que aún no has migrado |
Según la guía, text-embedding-3-small y text-embedding-3-large no conocen nada de lo ocurrido después de septiembre de 2021. large cuesta 6,5 veces más que small (0,13 / 0,02) para ganar 2,3 puntos en MTEB. ada-002 es 5 veces más caro que small y puntúa peor: si aún lo usas, migrar te ahorra dinero.
Cómo generar embeddings en TypeScript con la API de OpenAI
Para generar embeddings en TypeScript llamas a client.embeddings.create con el modelo y un input: un string o un array de strings. La referencia marca tres límites: cada input debe caber en 8.192 tokens, un array admite como mucho 2.048 elementos y la suma de todos los inputs de una petición no puede pasar de 300.000 tokens.
import OpenAI from "openai";
const client = new OpenAI(); // lee OPENAI_API_KEY del entorno
const MODEL = "text-embedding-3-small";
// 32 textos × 8.192 tokens = 262.144 tokens: nunca supera el tope de 300.000
const BATCH_SIZE = 32;
export async function embed(texts: string[], dimensions?: number): Promise<number[][]> {
const vectors: number[][] = [];
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE).map((t) => t.replace(/\n/g, " ").trim());
if (batch.some((t) => t.length === 0)) throw new Error("La API no acepta textos vacíos");
const response = await client.embeddings.create({
model: MODEL,
input: batch,
encoding_format: "float",
...(dimensions ? { dimensions } : {}),
});
// ordena por index: no des por hecho el orden del array
const sorted = [...response.data].sort((a, b) => a.index - b.index);
vectors.push(...sorted.map((d) => d.embedding));
}
return vectors;
}
Antes de guardar, valida la longitud de cada vector. Un z.array(z.number()).length(1536) evita que un cambio de configuración te llene la tabla de vectores incompatibles. Es el tipo de contrato en la frontera que trabajamos en el curso de Zod.
Similitud coseno: cómo comparar dos vector embeddings
La similitud coseno mide el ángulo entre dos vectores y va de -1 a 1: 1 es misma dirección, 0 es vectores sin relación y -1, direcciones opuestas. Es la función que OpenAI recomienda, aunque su propia guía añade que la elección "typically doesn't matter much".
Los embeddings de OpenAI salen normalizados a longitud 1, así que el coseno equivale al producto escalar, y coseno y distancia euclídea dan exactamente el mismo ranking.
export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error(`Dimensiones distintas: ${a.length} vs ${b.length}`);
}
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
if (normA === 0 || normB === 0) return 0;
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
La función calcula las normas aunque OpenAI ya normalice: te protege de vectores recortados a mano o de otro proveedor.
Búsqueda semántica en memoria: ejemplo mínimo
Para buscar con vector embeddings en un corpus pequeño no necesitas base de datos vectorial: embebes todo, calculas el coseno contra la consulta y ordenas.
const docs = [
"Cómo cancelar la suscripción desde el panel de usuario",
"El reembolso tarda entre 5 y 7 días hábiles en llegar",
"Para cambiar la contraseña, ve a Ajustes > Seguridad",
"Aceptamos pagos con tarjeta, PayPal y transferencia",
"El plan Pro incluye soporte prioritario por chat",
"Puedes exportar tus facturas en PDF desde Facturación",
];
const query = "quiero darme de baja y que me devuelvan el dinero";
// una sola petición para la consulta y los documentos
const [queryVector, ...docVectors] = await embed([query, ...docs]);
const ranking = docVectors
.map((vector, i) => ({ text: docs[i], score: cosineSimilarity(queryVector, vector) }))
.sort((a, b) => b.score - a.score);
console.table(ranking.slice(0, 3));
Ejecutado el 5 de noviembre de 2026 con text-embedding-3-small y el SDK openai 7.51.0, los dos primeros resultados fueron "cancelar la suscripción" (0,51) y "el reembolso tarda" (0,40), sin compartir palabras clave con la consulta.
El 0,51 no es un porcentaje de parecido: vale el orden, y cualquier umbral hay que calibrarlo con datos reales. Con millones de vectores necesitas un índice aproximado; lo explico en cómo funciona una vector database con HNSW e IVF.
El parámetro dimensions y la normalización
El parámetro dimensions acorta el embedding en origen sin que pierda lo que representa, y es lo que OpenAI sugiere frente a recortarlo tú. Según la guía, text-embedding-3-large recortado a 256 dimensiones supera en MTEB a ada-002 con sus 1.536.
Si recortas a mano con slice, tienes que renormalizar. En la misma prueba, los primeros 256 valores de un vector de 1.536 tenían norma 0,48, no 1, y el producto escalar deja de equivaler al coseno.
export function normalize(v: number[]): number[] {
let sumSquares = 0;
for (const x of v) sumSquares += x * x;
const magnitude = Math.sqrt(sumSquares);
return magnitude === 0 ? v : v.map((x) => x / magnitude);
}
const shortened = normalize(queryVector.slice(0, 256));
Las dimensiones también deciden cuánto ocupan tus vectores. En pgvector cada vector ocupa 4 * dimensiones + 8 bytes, y el tipo vector solo se indexa con HNSW hasta 2.000 dimensiones, según su README. Para 100.000 documentos:
| Dimensiones | Bytes por vector (4 × d + 8) | 100.000 vectores | ¿Indexable con HNSW como vector? |
|---|---|---|---|
| 256 | 1.032 | 103,2 MB | Sí |
| 1.024 | 4.104 | 410,4 MB | Sí |
| 1.536 | 6.152 | 615,2 MB | Sí |
| 3.072 | 12.296 | 1.229,6 MB | No: usa halfvec o dimensions |
Por eso large con dimensions: 1024 suele ser mejor plan que large a 3.072: los vectores ocupan la tercera parte y se pueden indexar con HNSW. El índice HNSW ocupa espacio aparte, que también crece con las dimensiones.
Errores típicos con vector embeddings en producción
Mezclar modelos o dimensiones al reindexar. Guarda modelo y dimensiones junto a cada vector y filtra por ellos. Si cambias de modelo, reindexa todo y embebe la consulta con el nuevo.
Recortar sin renormalizar. Usa dimensions en la llamada. Si recortas a mano, pasa normalize() siempre.
Embeber textos demasiado largos. El límite es 8.192 tokens por input, pero un vector que resume diez páginas es una media difusa de todo: trocea antes. Cuenta tokens con cl100k_base, el encoding que indica la guía; en TypeScript, con js-tiktoken.
No presupuestar la reindexación. Embeber 100.000 documentos de 500 tokens (50 millones de tokens) cuesta 1 USD con small. Lo caro es mantener dos índices mientras migras y revalidar la calidad.
Cuándo NO usar vector embeddings
Los embeddings no sirven cuando importa la coincidencia exacta, cuando la respuesta depende de una negación o cuando el dominio usa un vocabulario que el modelo no conoce. Dos casos medidos el 5 de noviembre de 2026 con similitud coseno:
| Frase A | Frase B (casi igual) | Frase C (lo que buscas) | small A-B | small A-C | large A-B | large A-C |
|---|---|---|---|---|---|---|
| "El pedido se ha enviado" | "El pedido no se ha enviado" | "Tu paquete ya está en camino" | 0,852 | 0,543 | 0,851 | 0,562 |
| "ERR-4012" | "ERR-4021" | "Error de pago rechazado" | 0,955 | 0,267 | 0,929 | 0,352 |
IDs, SKUs y códigos de error. Aunque en tu sistema ERR-4012 signifique "pago rechazado", el modelo no lo sabe: ERR-4012 y ERR-4021 salen casi idénticos. Si tu usuario busca un código concreto, usa BM25 o un filtro exacto. Lo habitual es combinar las dos cosas, como explico en búsqueda híbrida con embeddings en Supabase.
Negaciones. "Se ha enviado" y "no se ha enviado" quedan más cerca entre sí que de una paráfrasis correcta. En el hilo de Hacker News sobre el paper "Is Cosine-Similarity of Embeddings Really About Similarity?", el usuario latency-guy2 puso el mismo ejemplo con "Yes, this is a demonstration" frente a "Yes, this isn't a demonstration". En el mismo hilo, visarga escribió que los embeddings "make a mess of equivalent, antonymous and related things".
Dominio muy específico. Jerga y siglas internas que el modelo no vio al entrenar, con datos hasta septiembre de 2021. Mide con tus consultas antes de fiarte.
Cuando no puedes asumir el lock-in. Cada vector está atado a su modelo, y cambiar de proveedor obliga a reindexar el corpus entero. Si mañana quieres un pipeline multimodal como el de Gemini Embedding 2, no aprovechas ni un vector.
Qué modelo de embeddings elegir para empezar
Para empezar con vector embeddings, usa text-embedding-3-small con sus 1.536 dimensiones, guarda modelo y dimensiones junto a cada vector y monta 20 o 30 consultas reales con el resultado que esperas. Ese conjunto de prueba vale más que cualquier benchmark: te dirá si large compensa, qué umbral usar y si necesitas BM25.
Los vector embeddings son la pieza de recuperación de casi cualquier agente con RAG. Para ver cómo encajan con las tools, la memoria y la orquestación, tienes el ebook gratuito El Developer Agéntico, 194 páginas sobre arquitectura de agentes. Y para llevarlo a un producto completo, el curso Construye con IA.
Preguntas frecuentes
¿Puedo comparar embeddings de text-embedding-3-small y text-embedding-3-large?
No. Aunque recortes large a 1.536 dimensiones, cada modelo genera vectores en su propio espacio: la comparación no da error, pero no significa nada. Usa el mismo modelo y las mismas dimensiones para documentos y consulta.
¿Hace falta normalizar los embeddings de OpenAI?
No, si los usas tal como llegan o los acortas con el parámetro dimensions: OpenAI los devuelve normalizados a longitud 1. Sí, si los recortas con slice o los mezclas con vectores de otro origen.
¿Cuántos textos puedo embeber en una sola petición?
Hasta 2.048 inputs por petición, cada uno de 8.192 tokens como máximo y sin pasar de 300.000 tokens entre todos, según la referencia de la API. El límite que suele cortar primero es el de 300.000 tokens: con textos de 500 tokens caben 600 por petición.
¿Cuánto cuesta embeber todo mi corpus con OpenAI?
Multiplica los tokens del corpus, en millones, por el precio por millón. A noviembre de 2026, text-embedding-3-small cuesta 0,02 USD por millón y text-embedding-3-large, 0,13 USD. Un corpus de 50 millones de tokens sale por 1 USD con small y 6,50 USD con large.
¿Necesito una base de datos vectorial para usar vector embeddings?
No para empezar. Con unos pocos miles de documentos basta un bucle de coseno en memoria; la base vectorial compensa cuando recorrerlo todo se vuelve lento o necesitas filtrar y persistir a escala.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
