Capítulo 9 de 22 · intermedio
Embeddings
Qué cubre esta sesión
Todo hasta ahora ha tratado al modelo como algo con lo que hablas. Esta semana el modelo hace algo más callado y, para muchos sistemas en producción, más importante: convierte texto en números. Un embedding es un vector — unos cuantos cientos de floats — posicionado en el espacio de modo que el texto que significa cosas parecidas cae cerca. "Refund my order" y "how do I get my money back" no comparten ni una palabra, pero sus embeddings son vecinos. Una vez que el significado es geometría, "relacionado" deja de ser una idea difusa y se vuelve una distancia que puedes medir, ordenar y umbralizar.
Ese único primitivo es la base de los siguientes tres capítulos — búsqueda semántica, bases de datos vectoriales, retrieval-augmented generation se apoyan todos en él — y por sí solo sirve para búsqueda, clasificación, clustering y dedup. La mecánica es pequeña: una llamada a la API para embeber un batch, y cosine similarity, que son cuatro líneas de aritmética. El criterio está en qué haces con las distancias.
OpenAI
El ejemplo embebe seis oraciones y un query, y luego rankea las oraciones por cosine similarity contra el query. Fíjate en lo que encuentra: "a friendly canine companion" jala las oraciones de perros hasta arriba y deja las líneas del mercado de valores y la fotosíntesis hasta abajo, sin una sola keyword compartida haciendo el trabajo.
# One API call embeds a whole batch — cheaper and faster than one call per
# string. Each result is a vector (a list of floats); same model, same length
# every time, which is what makes them comparable.
def embed(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(model=_MODEL, input=texts)
return [item.embedding for item in response.data]
Una sola llamada embebe todo el batch — haz eso, no una llamada por string; es más barato y más rápido y cada vector regresa del mismo largo, así que son directamente comparables. Después, la única matemática que necesitas:
# Cosine similarity: the cosine of the angle between two vectors. 1.0 is the
# same direction (as similar as it gets), 0.0 is unrelated. Magnitude doesn't
# matter, only direction — which is why it beats raw distance for text.
def cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb) if na and nb else 0.0
Cosine similarity es el coseno del ángulo entre dos vectores: 1.0 significa misma dirección (tan relacionados como se puede), 0.0 significa sin relación. Ignora la magnitud y mira solo la dirección, que es exactamente lo correcto para texto — un documento largo y un query corto sobre lo mismo deberían seguir contando como cercanos.
Gemini
El modelo de embeddings de Gemini regresa una lista embeddings con un vector
.values por input. Cambia la llamada de embed y todo lo de aguas abajo — la
función de coseno, el ranking — es idéntico, porque una vez que tienes los
vectores el proveedor sale del cuadro.
# Gemini returns an `embeddings` list, one entry per input, each with a
# `.values` vector. Batch the inputs in one call, same as OpenAI.
def embed(texts: list[str]) -> list[list[float]]:
response = client.models.embed_content(model=_MODEL, contents=texts)
return [e.values for e in response.embeddings]
Una advertencia que vale la pena interiorizar temprano: los embeddings de modelos distintos no son comparables. Un vector del modelo de OpenAI y un vector del de Gemini viven en espacios distintos; no puedes hacerles coseno entre sí, y si cambias de modelo de embeddings tienes que re-embeber todo tu corpus. Elige uno por índice y quédate ahí.
Ponlo a trabajar
Tres apps de docker-compose bajo code/showcase/<slug>/, la misma rutina: bash bootstrap-secrets.sh, docker compose up --build, http://localhost:3000.
Ninguna llama a un modelo de chat — embeddings más aritmética es todo el motor,
que es por lo que son rápidas y baratas.
Showcase 1 — Búsqueda semántica
Un pequeño centro de ayuda buscado por significado. El corpus se embebe una vez y se cachea; cada query es una llamada de embedding, luego coseno contra cada doc. "How do I get my money back" regresa la política de reembolsos con la que no comparte ni una palabra. Esto es retrieval en miniatura — el loop de coseno de aquí se convierte en una base de datos vectorial el próximo capítulo, pero la idea no cambia.
Showcase 2 — Clasificador de etiquetas
Clasificación zero-shot sin entrenamiento. Cada categoría es una descripción de una oración, embebida una vez; un mensaje entrante se embebe y se asigna a la descripción más cercana. Agregar una categoría es escribir una oración — todo el paso de "reentrenamiento" es instantáneo. Es un baseline genuinamente fuerte, y es lo honesto que hay que probar antes de echar mano de un fine-tune.
Showcase 3 — Buscador de duplicados
Pega una lista y encuentra los pares que significan lo mismo pese a estar redactados distinto. Cada línea se embebe, todos los pares se comparan por coseno, se muestra el más cercano. El dedup por coincidencia exacta no puede ver "I can't log in" y "I'm locked out of my account" como el mismo problema; los embeddings sí. La comparación de todos los pares es O(n²) — bien para una caja de texto, imposible para un millón de filas, que es precisamente el problema que una base de datos vectorial existe para resolver.
Las tres corren el mismo backend/main.py sobre PROVIDER, así que Gemini es
una variable de entorno. Y las tres se apoyan en las mismas dos operaciones:
embeber un batch, comparar por coseno. Todo lo demás es lógica de aplicación.
Córrelo
El README de esta carpeta tiene la versión de Python, el comando de instalación,
las dos variables de entorno y los comandos exactos para los ejemplos básicos y
cada showcase. Las keys vienen del .env sin trackear en la raíz del curso.
Cosine similarity es puro standard library, así que los únicos installs son los
SDKs.
Lo que te llevas
Los embeddings convierten texto en geometría, y con la geometría puedes computar — ese es todo el desbloqueo. Dos operaciones cargan una cantidad enorme de software en producción: embeber un batch, comparar por coseno. Ten tres cosas en mente y evitarás las trampas comunes: batchea tus embeddings, nunca mezcles modelos dentro de un mismo índice (distintos modelos, distintos espacios), y recuerda que la similitud es un score continuo, no un sí/no — tú eliges el umbral, y el umbral correcto es empírico. Este capítulo hizo toda su búsqueda con un loop de Python sobre un puñado de documentos. La próxima semana ese loop llega a sus límites y le entregamos el trabajo a una base de datos vectorial construida para hacerlo sobre millones.