Skip to content
This page has been auto-translated and may contain errors.View in English

Embeddings

docs.scrimba.com

Primero, la palabra. Un embedding es una lista de números que representa una pieza de texto y captura su significado, de modo que textos que significan cosas similares se convierten en números similares. Este capítulo trata sobre cómo convertir palabras en algo que una computadora pueda comparar, y es lo que permite que la búsqueda coincida con el significado en lugar de palabras clave.

Texto como vectores

¿Cómo sabe una computadora que "perro" y "cachorro" están relacionados, mientras que "perro" y "grapadora" no? Las palabras no comparten letras que sean importantes.

El truco es convertir el texto en números que capten su significado, de modo que significados similares terminen como números similares. Esa lista de números es un embedding, también llamado vector. Un modelo lee el texto y genera el vector.

No interpretas los números tú mismo. Lo que importa es la relación entre ellos.

Una imagen mental aproximada: imagina cada pieza de texto colocada como un punto en el espacio, donde las cosas que significan cosas similares están cerca una de la otra. "Perro" y "cachorro" aterrizan en el mismo vecindario. "Grapadora" está al otro lado de la ciudad. Los embeddings reales utilizan cientos o miles de dimensiones en lugar de dos o tres que puedas visualizar, pero la intuición se mantiene: la cercanía significa similitud.

¿De dónde vienen esos números y por qué se alinean con el significado? Un modelo de embedding es un primo cercano del modelo predictor de Cómo funcionan los LLM, entrenado en una enorme cantidad de texto. Para ser bueno en su tarea, tuvo que aprender qué palabras e ideas aparecen en contextos similares, la observación antigua de que conoces una palabra por la compañía que frecuenta.

"Perro" y "cachorro" aparecen en el mismo tipo de oraciones, así que el modelo aprendió a representarlos de manera similar. "Grapadora" frecuenta compañía diferente, así que se ubica en otro lugar. Los cientos de números son direcciones aprendidas de significado, cada una capturando algún eje de cómo varía el texto, ninguno de ellos etiquetado de una manera que un humano leyera. Nunca decodificas los números, solo mides qué tan cerca están dos vectores.

JunoTexto como vectores Un embedding es una lista de números, un vector, que representa el significado de una pieza de texto, producido por un modelo entrenado con enormes cantidades de texto. Aprendió a colocar palabras e ideas que aparecen en contextos similares cerca una de la otra, de modo que significados similares obtengan vectores similares. Nunca lees los números, solo mides qué tan cerca están dos de ellos, y la cercanía refleja significado.

Quieres una búsqueda que coincida con el significado en lugar de la ortografía, así que "quiero mi dinero de vuelta" encuentra una línea sobre reembolsos. El movimiento es convertir cada pieza de texto en un vector, una lista de longitud fija de números donde significados similares se sientan cerca. Un modelo de embedding lee el texto y emite ese vector, y el punto completo es que la distancia entre vectores representa la similitud de significado.

El vector tiene una longitud establecida, su dimensión, a menudo algunos cientos a un par de miles de números. Cada texto que incrustes con el mismo modelo sale de la misma longitud, lo que te permite comparar cualquiera de ellos. Esas dimensiones son direcciones de significado aprendidas, no categorías etiquetadas por humanos, así que no lees la dimensión 47 como "formalidad". Tratas el vector como un punto opaco y mides distancias.

Antes de incrustar algo real, hay un paso que decide si todo funciona: chunking. Rara vez incrustas un documento completo como un vector, porque un vector para un manual de 20 páginas difumina cada tema en él en un único punto promedio que no coincide bien con nada. En su lugar, divides el documento en chunks, pasajes de aproximadamente un párrafo a una página, e incrustas cada chunk por su cuenta. Incrusta chunks, no documentos completos, para que cada vector contenga una idea enfocada.

El tamaño del chunk es un compromiso real que estableces deliberadamente. Los chunks demasiado grandes promedian varias ideas juntas y recuperan de manera floja. Los chunks demasiado pequeños pierden el contexto circundante que hizo que una oración tuviera sentido. Un punto de partida común es algunos cientos de tokens por chunk con un pequeño solapamiento entre vecinos, de modo que una oración dividida en un límite aún aparezca completa en uno de ellos, luego ajustas según lo que tu recuperación realmente devuelve.

JunoTexto como vectores Un embedding convierte texto en un vector de longitud fija donde la distancia representa la similitud de significado, y cada vector del mismo modelo comparte una longitud para que puedas compararlos. Las dimensiones son direcciones aprendidas, no etiquetas, así que trata el vector como un punto opaco. Aquí está el movimiento que más importa: divide tus documentos en pasajes de tamaño de párrafo e incrusta cada chunk, porque un vector para un documento completo promedia todo en una papilla.

La búsqueda que coincide con el significado se basa en una idea: mapear texto en un espacio vectorial (un espacio de coordenadas de dimensión fija donde cada punto es una lista de números) de modo que la similitud semántica se convierta en proximidad geométrica. Un modelo de embedding es el mapa: lee texto y devuelve un vector.

Todo lo que construyes encima, indexación, recuperación, ranking, es geometría sobre esos vectores. Si obtienes el mapeo incorrecto, nada después lo recupera.

El primer mecanismo de diseño es qué incrustas, y eso es chunking: dividir documentos fuente en pasajes antes de incrustar en lugar de incrustar archivos completos. La razón es que un embedding es una compresión con pérdidas de su entrada en un punto, así que cuantas más ideas distintas contiene un chunk, más se promedian en un centroide vago que no recupera nada afiladamente. Ajusta el tamaño del chunk a la unidad que un usuario preguntará, generalmente un pasaje de algunos cientos de tokens, con un pequeño solapamiento para que un hecho en el límite sobreviva en al menos un chunk.

La estrategia de chunking es donde se gana o se pierde la calidad de recuperación, más que la elección del modelo:

  • Ventanas de tamaño fijo son económicas, pero cortan oraciones a mitad de pensamiento.
  • División consciente de la estructura (en encabezados, párrafos o bloques de código) mantiene cada chunk coherente y tiende a recuperar mejor, al costo de un pipeline más complicado.

Cualquiera que elijas, almacena la fuente y posición del chunk junto a su vector, porque en el momento de la recuperación necesitas apuntar de vuelta a dónde vino un hecho, y un vector desnudo no puede decirte.

Una propiedad más para interiorizarte: las dimensiones de embedding no son ejes interpretables, por lo que no puedes depurar recuperación leyendo un vector. Depuras embeddings inspeccionando lo que recuperan, nunca leyendo los números. Cuando una consulta extrae el chunk incorrecto, la solución está en el chunking, el modelo o la consulta, examinados a través de los resultados recuperados, no a través de las coordenadas en sí.

JunoTexto como vectores Un modelo de embedding mapea texto en un espacio vectorial donde la similitud semántica se convierte en distancia geométrica, y cada paso posterior es geometría sobre esos puntos. Chunking es el mecanismo que decide la calidad de recuperación: dimensiona los chunks a la unidad que un usuario pregunta, divide en estructura donde puedas, y almacena la fuente y posición de cada chunk junto a su vector para que puedas rastrear un hecho de vuelta. No puedes leer un vector para depurarlo, así que cuando la recuperación va mal, inspecciona lo que volvió, no los números.

Generando un embedding

Pides a un modelo de embedding el vector, muy similar a una llamada normal de modelo pero con un punto final diferente y un modelo diferente.

python
from openai import OpenAI

client = OpenAI()

EMBED_MODEL = "text-embedding-3-small"  # punto de intercambio por el modelo de embedding

def embed(text):
    response = client.embeddings.create(model=EMBED_MODEL, input=text)
    return response.data[0].embedding  # una lista de números

vector = embed("un perro pequeño y juguetón")
print(len(vector))  # p. ej. 1536 números

El resultado es una lista de números. Una regla importa más que el resto: siempre compara embeddings hechos por el mismo modelo. Los vectores de diferentes modelos viven en espacios diferentes y no son comparables, como mediciones en diferentes unidades. Mezclar modelos da puntuaciones sin sentido, cada vez.

Y como cada llamada de modelo, se te factura por tokens, así que incrustar una gran colección de documentos tiene un costo que vale la pena estimar por adelantado antes de ejecutarlo en miles de documentos.

JunoGenerando un embedding Generas un embedding llamando a un modelo de embedding, que te devuelve una lista de números para tu texto. Siempre compara vectores hechos por el mismo modelo, porque vectores de diferentes modelos no son comparables, como centímetros contra pulgadas. Incrustar se factura por tokens, así que una colección grande tiene un costo real que vale la pena verificar primero.

La llamada en sí es un punto final diferente con la misma forma que una llamada de chat: envías texto, obtienes un vector de vuelta.

python
from openai import OpenAI

client = OpenAI()

EMBED_MODEL = "text-embedding-3-small"

def embed_batch(texts):
    # envía muchos textos en una solicitud, obtén un vector por texto de vuelta
    response = client.embeddings.create(model=EMBED_MODEL, input=texts)
    return [item.embedding for item in response.data]

chunks = ["un perro pequeño y juguetón", "un perro joven", "un artículo de oficina"]
vectors = embed_batch(chunks)  # un vector por chunk, mismo orden en

La forma varía según el proveedor, pero la idea de solicitud y respuesta es la misma en todas partes. Dos puntos prácticos más allá de la llamada básica. Primero, incrusta en lotes: la mayoría de los puntos finales aceptan una lista de textos en una sola solicitud y devuelven un vector cada uno, lo que es más rápido y económico que una llamada por chunk porque pagas menos gastos de solicitud y golpeas límites de velocidad menos. Cuando indexas un corpus grande, los lotes es lo que mantiene el costo y el tiempo de reloj cuerdo.

Segundo, la regla del mismo modelo tiene un compañero: modelo y dimensión son una opción, no un valor por defecto. Los modelos de embedding más grandes con más dimensiones capturan distinciones más finas pero cuestan más de calcular, almacenar y comparar. Elige el modelo más pequeño y la dimensión que siga separando tus datos.

Algunos modelos incluso te permiten solicitar un vector más corto para comerciar un poco de precisión por almacenamiento más económico y búsqueda más rápida. Lo que elijas se convierte en un contrato: cambiar el modelo después y cada vector almacenado tiene que ser regenerado.

JunoGenerando un embedding Incrustar es una solicitud que toma texto y devuelve un vector, y la misma forma funciona en todos los proveedores. Lotes tus textos en una sola llamada cuando puedas, porque reduce gastos de solicitud y presión de límites de velocidad en un corpus grande. Modelo y dimensión son una opción real: elige el más pequeño que siga separando tus datos, y recuerda que el modelo se convierte en un contrato, ya que cambiarlo significa re-incrustar todo lo que almacenaste.

La superficie de la API es pequeña. Envías texto, obtienes un vector flotante, y las decisiones interesantes se sientan alrededor de esa llamada en lugar de en ella.

python
from openai import OpenAI

client = OpenAI()

EMBED_MODEL = "text-embedding-3-small"

def embed_batch(texts):
    # una solicitud, muchos vectores de vuelta, en orden de entrada
    response = client.embeddings.create(model=EMBED_MODEL, input=texts)
    return [item.embedding for item in response.data]

La forma de solicitud y respuesta varía según el proveedor, pero cada punto final de embedding es este mismo contrato de lote-dentro, vectores-fuera. Los lotes no son una comodidad a escala, es la diferencia entre un trabajo de índice que termina y uno que acelera toda la tarde: cada solicitud lleva gastos fijos y cuenta contra un límite de velocidad, así que amortiza ambos enviando lotes grandes, luego retrocede e intenta de nuevo en los fracasos parciales que un trabajo grande golpeará.

Modelo y dimensión son un compromiso arquitectónico con una cola larga. Vectores de dimensión más alta almacenan más distinción pero cuestan más en cada eje que importa en producción: almacenamiento por vector, memoria en el índice y tiempo por comparación, todo se escala con la dimensión. Algunos modelos soportan embeddings Matryoshka (vectores que puedes truncar a una longitud más corta y seguir usándolos), lo que te permite mantener vectores completos para precisión y servir un prefijo truncado para búsqueda de primer paso económica. Vale la pena cuando tu corpus es lo suficientemente grande como para que el almacenamiento y la latencia de consulta, no la calidad del modelo, sean la restricción vinculante.

El compromiso que muerde después es el versionado. Un embedding almacenado está vinculado a una versión exacta de modelo para siempre.

Los vectores de dos versiones de modelo no son comparables, así que no puedes incrustar una nueva consulta con un modelo más nuevo y compararla contra un índice construido en el anterior. El resultado es silencioso: las puntuaciones se ven plausibles y los rankings son incorrectos. Trata la versión del modelo como parte del esquema de tu índice, almacénalo junto a los vectores, y planifica re-incrustar como una migración, cubierta en la discusión de frescura más adelante en este capítulo.

JunoGenerando un embedding La llamada de embedding es un contrato de lote-dentro, vectores-fuera que varía en forma según el proveedor pero no en idea, y los lotes con reintento es lo que hace que un trabajo de índice grande termine en lugar de acelerar. Modelo y dimensión escalan almacenamiento, memoria y latencia de consulta, así que alcanza vectores truncables cuando la escala, no la calidad, es la restricción. La versión es un compromiso para siempre: vectores de diferentes versiones de modelo fallan silenciosamente para comparar, así que almacena la versión en tu esquema y trata re-incrustar como una migración planeada.

Midiendo similitud

Para encontrar qué tan relacionadas son dos piezas de texto, mides qué tan cerca están sus vectores. La medida estándar es similitud del coseno, que devuelve un número de -1 a 1: más cerca de 1 significa más similar, cerca de 0 significa no relacionado. No necesitas las matemáticas detrás para usarla. Piénsalo como comparar la dirección en que apuntan dos vectores.

La dirección es lo que lleva el significado aquí, por eso el coseno compara ángulo en lugar de distancia bruta. Dos textos sobre reembolsos apuntan de la misma manera ya sea una frase corta o un párrafo largo.

python
import math

def cosine_similarity(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    mag_a = math.sqrt(sum(x * x for x in a))
    mag_b = math.sqrt(sum(x * x for x in b))
    return dot / (mag_a * mag_b)

dog = embed("un perro pequeño y juguetón")
puppy = embed("un perro joven")
stapler = embed("un artículo de oficina para encuadernar papel")

print(cosine_similarity(dog, puppy))    # más alto, los dos están cerca en significado
print(cosine_similarity(dog, stapler))  # más bajo, los dos no están relacionados

Los números confirman la intuición: perro y cachorro puntúan más alto, perro y grapadora puntúan más bajo. Comparar un vector de consulta contra vectores almacenados es el corazón de la búsqueda semántica. Esa única función es el movimiento en el que el resto del capítulo se construye.

JunoMidiendo similitud La similitud del coseno puntúa qué tan cerca están dos vectores, de -1 a 1, donde más cerca de 1 significa más similar. Puedes usarla sin las matemáticas, compara la dirección en que apuntan dos vectores, es por eso que una frase corta y un párrafo largo sobre lo mismo siguen coincidiendo. Comparar un vector de consulta contra vectores almacenados es el movimiento central de la búsqueda semántica.

Tienes vectores, ahora necesitas un número que diga qué tan cerca están dos de ellos. El estándar es similitud del coseno: mide el ángulo entre dos vectores, ignorando su longitud, y devuelve un valor de -1 a 1 donde mayor significa más parecido. Ángulo, no distancia, es lo que quieres, porque un pasaje largo y una frase corta sobre el mismo tema deberían puntuar como similar aunque sus vectores tengan magnitudes diferentes.

python
def dot(a, b):
    return sum(x * y for x, y in zip(a, b))

# si los vectores están normalizados a longitud 1, similitud del coseno es igual a su producto punto
score = dot(query_vector, doc_vector)  # sin división necesaria

Aquí está el movimiento que paga a escala: normaliza tus vectores a longitud 1 cuando los almacenas. Una vez que cada vector tiene la misma longitud, la división en similitud del coseno desaparece y la puntuación se convierte en un producto punto simple, que es una multiplicación-y-suma por dimensión y mucho más rápida de ejecutar en un conjunto grande. Normaliza una vez al almacenar, luego compara con un producto punto. Muchos modelos de embedding devuelven vectores ya normalizados, pero no lo supongas, verifica.

El otro cambio del ejemplo de juguete: raramente quieres la coincidencia única más cercana. Quieres recuperación top-k, los k resultados con puntuación más alta, generalmente tres a diez. Un vector puede ser un casi-fallo mientras que el segundo y tercero juntos contienen la respuesta, y devolver un conjunto pequeño da al siguiente paso espacio para trabajar. Devolver solo el primero bota contexto que desearías haber mantenido.

JunoMidiendo similitud La similitud del coseno compara el ángulo entre dos vectores de -1 a 1, ignorando longitud, así que una frase corta y un pasaje largo sobre el mismo tema siguen coincidiendo. Normaliza tus vectores a longitud 1 al momento del almacenamiento y la comparación colapsa a un producto punto rápido, que se suma en un conjunto grande. Y recupera top-k, tres a diez resultados, no el único mejor, porque la respuesta a menudo está dispersa en los primeros.

Necesitas una métrica de similitud, y similitud del coseno (el coseno del ángulo entre dos vectores, que varía de -1 a 1) es el predeterminado porque ignora la magnitud, así que la longitud del documento no abruma el tema. Esa opción tiene una consecuencia de desempeño que vale la pena diseñar alrededor: el coseno sobre vectores brutos cuesta un producto punto más dos cálculos de magnitud por comparación, y lo pagas por cada vector en el índice en cada consulta.

python
import numpy as np

# normaliza al momento de escritura, una vez por vector
def normalize(v):
    v = np.asarray(v, dtype=np.float32)
    return v / np.linalg.norm(v)

# al momento de consulta, coseno sobre vectores normalizados es un único producto matriz-vector
def search(query_vec, matrix):
    return matrix @ query_vec  # una puntuación por fila, vectorizado

La optimización es normalizar cada vector a longitud unitaria al momento de escritura, lo que hace que la similitud del coseno sea idéntica a un producto punto y te permite puntuar el corpus completo como un único producto matriz-vector en lugar de un bucle de Python. Normaliza al momento de escritura y la similitud es un producto punto sobre todo el índice a la vez. Este es el punto base de fuerza bruta: exacto, directo, y lineal en tamaño del corpus, que está bien hasta aproximadamente los pocos cientos de miles de vectores y se desmorona por encima de eso, la señal para pasar a un índice aproximado cubierto después.

Dos realidades de producción que la métrica de juguete oculta completamente. Primero, casi nunca quieres el argmax, quieres top-k con un umbral de puntuación, porque la brecha entre el primer y segundo resultado te dice qué tan segura es la coincidencia, y un resultado superior que aún puntúa bajo no significa nada en tu corpus respondió realmente la consulta. Devuelve eso como "sin coincidencia buena" en lugar de entregar la fila menos-mala. Segundo, la similitud del coseno es relativa, no absoluta: una puntuación de 0,4 es significativa solo contra la distribución de puntuaciones que tu corpus y modelo producen, así que calibra umbrales empíricamente por índice en lugar de copiar un número mágico de una entrada de blog.

JunoMidiendo similitud La similitud del coseno ignora magnitud así que la longitud no ahoga el tema, pero el coseno de fuerza bruta muere a escala, así que por encima de pocos cientos de miles de vectores ejecutas búsqueda aproximada de vecinos más cercanos, HNSW para baja latencia con alta memoria o IVF para lo inverso, y ajustas recall contra latencia sabiendo que recall más alto cuesta velocidad. Normaliza al momento de escritura y la métrica se convierte en un producto punto que puedes ejecutar como un único producto matriz-vector sobre el corpus completo. Recupera top-k con un umbral, trata una puntuación superior baja como sin coincidencia en lugar de la fila menos-mala, y calibra ese umbral por índice porque los números son relativos, no absolutos.

Búsqueda semántica

Junta las piezas y puedes buscar por significado en lugar de palabras clave. Incrusta tus documentos una vez y almacena los vectores. Cuando llega una consulta, incrústala también, luego devuelve los documentos cuyos vectores están más cerca.

python
docs = [
    "Restablece tu contraseña desde la página de configuración de cuenta.",
    "Nuestra política de reembolso permite devoluciones dentro de 30 días.",
    "Contacta soporte por correo electrónico para preguntas de facturación.",
]

# incrusta cada documento una vez, por adelantado
index = [{"text": text, "vector": embed(text)} for text in docs]

def search(query):
    query_vector = embed(query)
    scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
    return sorted(scored, key=lambda x: x["score"], reverse=True)[0]  # la coincidencia más cercana

print(search("¿cómo obtengo mi dinero de vuelta?"))
# devuelve la línea de política de reembolso, aunque no comparta palabras clave con la consulta

La consulta "¿cómo obtengo mi dinero de vuelta?" encuentra la línea de reembolso a pesar de no comparten palabras importantes con ella. La búsqueda semántica coincide por significado, no por ortografía. La búsqueda por palabras clave la habría extrañado completamente, porque la consulta y el documento no tienen palabras importantes en común.

La forma es siempre la misma: incrusta una vez y mantén los vectores, incrusta la consulta en tiempo de solicitud, puntúa, ordena, devuelve el más cercano. Ese es el bucle de búsqueda semántica, y lo reutilizarás para el resto de este manual.

JunoBúsqueda semántica La búsqueda semántica incrusta tus documentos una vez y almacena los vectores, luego incrusta cada consulta entrante y devuelve los documentos más cercanos por similitud del coseno. Coincide por significado, así que "obtengo mi dinero de vuelta" encuentra una línea de "política de reembolso" que no comparte palabras clave. Esa coincidencia basada en significado es lo que la búsqueda por palabras clave no puede hacer.

La versión de juguete almacena vectores en una lista de Python y puntúa cada uno en cada consulta. Eso funciona para una demostración y se rompe en el momento en que tu corpus crece, porque un escaneo lineal vuelve a ejecutar la comparación completa sobre cada vector para cada consulta, y también estás reconstruyendo el índice en memoria en cada reinicio.

La solución es una base de datos vectorial: una tienda construida para mantener vectores y responder "encuentra los k más cercanos a este vector de consulta" rápidamente y desde el disco. En lugar de tu propia lista y bucle, escribes vectores una vez y la base de datos maneja la búsqueda, persistencia y actualización de entradas individuales.

python
# la forma varía según DB vectorial, pero el patrón es el mismo en todas partes
index.upsert([
    {"id": "doc-1", "vector": embed(chunk), "metadata": {"source": "faq.md", "section": "refunds"}}
    for chunk in chunks
])

results = index.query(vector=embed(user_question), top_k=5)  # los 5 más cercanos, no solo 1

Dos cosas que la base de datos te da que la lista no:

  • Devuelve resultados top-k en una llamada, los varios más cercanos en lugar de un único mejor, que es lo que realmente alimentas al siguiente paso.
  • Almacena metadatos junto a cada vector, el archivo fuente, sección y marca de tiempo, así que un resultado puede apuntar de vuelta a dónde vino y puedes filtrar por ello.

Almacena vectores en una DB vectorial, no en una lista de Python, en el momento en que tengas datos reales. La lista es para aprender el mecanismo, no para ejecutarlo.

JunoBúsqueda semántica La búsqueda semántica es incrusta-una-vez, almacena, incrusta-la-consulta, devuelve-la-más-cercana, pero una lista de Python y un bucle solo sobreviven a una demostración, porque escanear cada vector por consulta y reconstruir al reinicio no escala. Pasar a una base de datos vectorial en el momento en que tengas datos reales: hace búsqueda rápida de top-k desde el disco y almacena metadatos junto a cada vector para que los resultados apunten de vuelta a su fuente. La lista enseña el mecanismo, la base de datos lo ejecuta.

El bucle de referencia, incrusta una vez, almacena, incrusta la consulta, puntúa, ordena, devuelve, es correcto y no sobrevive al contacto con la escala. Un escaneo de fuerza bruta es lineal en tamaño del corpus por consulta, así que en un millón de vectores y cualquier tasa de consulta real estás gastando un segundo de CPU por búsqueda que no tienes. Todo en búsqueda de producción se trata de no comparar la consulta contra cada vector.

Eso es lo que búsqueda aproximada de vecinos más cercanos te compra (ANN: algoritmos que encuentran los vectores más cercanos rápido aceptando una pequeña posibilidad de perder uno de los verdaderamente más cercanos). Dos familias dominan:

  • HNSW (Pequeño Mundo Navegable Jerárquico) construye un gráfico en capas de vectores y lo camina de áspero a fino, dando baja latencia de consulta al costo de alta memoria.
  • IVF (archivo invertido) agrupa vectores y busca solo los grupos más cercanos, usando menos memoria pero necesitando que sus centros de grupo se reentrenen a medida que los datos cambian.

Por encima de algunos cientos de miles de vectores, ANN no es opcional, es el índice. El compromiso que estás ajustando es recall (la fracción de los verdaderos vecinos más cercanos que el índice realmente devuelve) contra latencia y memoria: empuja recall hacia 1.0 y las consultas se ralentizan, relájalo y silenciosamente pierdes chunks relevantes.

El modo de fallo que el bucle de juguete oculta completamente es la frescura. Un índice es una instantánea, así que cuando un documento fuente cambia, su vector almacenado ahora está obsoleto y sigue siendo recuperado como si fuera actual. Necesitas un pipeline de re-incrustar disparado por cambio de documento: detecta la edición, re-chunifica y re-incrusta ese documento, y haz upsert de los nuevos vectores mientras eliminas los viejos, idealmente rastreados por un hash de contenido para que re-incrustes solo lo que realmente cambió en lugar del corpus completo en cada edición. Los vectores obsoletos son la forma silenciosa en que un sistema de recuperación comienza a mentir.

La última brecha es la coincidencia exacta. La búsqueda vectorial está construida para perderla: una consulta para orden A-10293 o un código de error se incrusta a una región difusa y puede no extraer el único documento que contiene esa cadena. Búsqueda híbrida ejecuta búsqueda por palabras clave (que clava tokens exactos) y búsqueda vectorial (que clava significado) juntas y fusiona los rankings, así que identificadores, nombres y términos raros no se pierden en difusión semántica. Alcanza por ella siempre que tu corpus contiene cosas que los usuarios escribirán exactamente.

JunoBúsqueda semántica Un escaneo de fuerza bruta muere a escala, así que por encima de algunos cientos de miles de vectores ejecutas búsqueda aproximada de vecinos más cercanos, HNSW para baja latencia con alta memoria o IVF para lo inverso, y ajustas recall contra latencia sabiendo que recall más alto cuesta velocidad. Construye un pipeline de re-incrustar disparado por cambio de documento con un hash de contenido, porque un índice es una instantánea y los vectores obsoletos recuperan como si fueran actuales. Y agrega búsqueda híbrida de palabras clave más vector siempre que los usuarios escriban cadenas exactas como números de orden, ya que la búsqueda vectorial pura se desenfoca justo pasándolos.

Para qué no sirven los embeddings

Los embeddings son una herramienta de similitud, no una herramienta de pensamiento. Unos cuantos límites para tener en mente así alcanzas la herramienta correcta:

  • No hacen coincidencia exacta. Para encontrar un número de orden exacto o un código de error, la búsqueda por palabras clave o base de datos simple es mejor.
  • No razonan. Te dicen que dos cosas están relacionadas, no por qué, y no si una lógicamente sigue de la otra.
  • Se vuelven obsoletos. Si tus documentos cambian, debes re-incrustarlos, o tu búsqueda apuntará a contenido antiguo.

Los embeddings encuentran lo que está relacionado, no juzgan qué es verdadero o actual. Cada una de estas brechas tiene una herramienta mejor, y saber cuál usar es la mitad de la habilidad.

JunoPara qué no sirven los embeddings Los embeddings miden similitud, no razonan y no hacen coincidencia exacta. Para un número de orden exacto o código de error, usa búsqueda por palabras clave o base de datos en su lugar. Y cuando tus documentos cambian, re-incrustralos, o tu búsqueda apuntará a contenido obsoleto.

La trampa con una herramienta que funciona tan bien es alcanzarla en todas partes. Tres límites marcan dónde detenerse y elegir algo más.

La coincidencia exacta es la primera. Los embeddings mapean un ID de orden o un código de error al mismo espacio semántico difuso que todo lo demás, así que A-10293 aterrizaría cerca de otras cadenas de forma de orden, no exactamente en sí mismo. Para identificadores, códigos y cualquier valor que un usuario escriba literalmente, la búsqueda por palabras clave o base de datos es la herramienta correcta, y búsqueda híbrida (búsqueda por palabras clave y vectorial ejecutadas juntas) es cómo obtienes ambas a la vez.

El razonamiento es el segundo. Similitud no es inferencia: dos chunks puntuando alto significa que son sobre cosas relacionadas, no que uno responda una pregunta o lógicamente siga del otro. El razonamiento ocurre en el modelo al que alimentas los chunks recuperados, no en la recuperación. La recuperación encuentra candidatos, el modelo hace el pensamiento.

La frescura es el tercero y el que muerde silenciosamente. Tu índice es una instantánea de siempre que lo incrustaste, así que los documentos editados mantienen devolución de su ortografía vieja hasta que la re-incrustes. Trata re-incrustar al cambio como parte del sistema, no algo que hagas a mano cuando alguien nota que las respuestas se volvieron incorrectas.

JunoPara qué no sirven los embeddings Los embeddings son una herramienta de similitud, así que pierden coincidencias exactas como IDs de orden, que quieren búsqueda por palabras clave o un híbrido de ambas, y no razonan, el modelo al que alimentas los chunks hace eso. La silenciosa es frescura: un índice es una instantánea, así que documentos editados siguen devolviendo ortografía obsoleta hasta que los re-incrustes. Construye re-incrustar en el sistema en lugar de esperar a que alguien note respuestas incorrectas.

Saber dónde se detienen los embeddings es lo que mantiene un sistema de recuperación confiable, y tres límites importan en producción.

La coincidencia exacta es un punto ciego estructural, no un problema de ajuste. Los embeddings comprimen la entrada hacia vecindarios semánticos, así que identificadores, códigos, SKU y nombres propios raros se esparcen hacia cadenas de aspecto similar en lugar de aterrizar en sí mismos. La búsqueda vectorial pura clasificará un casi-fallo sobre la coincidencia exacta. La solución es recuperación híbrida que fusiona un índice léxico (búsqueda por palabras clave o BM25, un ranking clásico de frecuencia de término) con el índice vectorial, así que tokens exactos se coinciden exactamente mientras el significado se coincide semánticamente, y no apuestas una búsqueda de orden en geometría.

La deriva es el límite que se erosiona silenciosamente. Deriva de embedding es el desajuste que obtienes cuando la versión del modelo detrás de tu índice cambia: vectores de dos versiones ocupan espacios diferentes y no son comparables, así que una consulta incrustada con un modelo nuevo contra un índice construido en el anterior devuelve tonterías de aspecto plausible. Fija la versión del modelo en tu esquema de índice, y trata cualquier actualización de modelo como una migración de re-incrustar completa, no una sustitución fácil, porque no hay error lanzado, solo resultados silenciosamente peores.

La evaluación es el límite que la gente se salta hasta que la recuperación ya está fallando. No puedes examinar si la recuperación es buena, la mides: retén un conjunto de consultas con chunks conocidos como relevantes y puntúa recall@k (¿el chunk correcto aterrizó en el top k) y precisión (cuánto de lo que devolviste fue relevante). Si no estás midiendo recall@k, no sabes si la recuperación funciona.

Ese conjunto de evaluación es también lo que te dice si una estrategia de chunking, modelo o ponderación híbrida nueva realmente ayudó, en lugar de intercambiar un conjunto de fallos por otro que aún no has notado. La calidad de recuperación es la fundación en la que se para RAG, así que se gana una prueba real, no una comprobación de vibra.

JunoPara qué no sirven los embeddings Los embeddings tienen un punto ciego estructural para coincidencias exactas, así que fusiona un índice léxico con el vectorial para identificadores y códigos en lugar de confiar en la geometría. La deriva de embedding es la silenciosa: vectores de diferentes versiones de modelo no se comparan, así que fija la versión y trata las actualizaciones como una re-incrustar completa. Y mide recuperación con recall@k en un conjunto retenido, porque no puedes examinar si funciona, y esa evaluación también es cómo distingues una mejora real de un movimiento lateral.

En la práctica

El flujo completo vive en la función search anterior: incrusta un pequeño conjunto de entradas de FAQ una vez, luego responde consultas por significado. El patrón es el mismo en cualquier tamaño, solo el almacenamiento cambia a medida que creces.

En el próximo capítulo tomas las coincidencias más cercanas y las alimentas a un modelo, así que responde desde tus documentos en lugar de desde memoria. Esa técnica es RAG, y está construida directamente sobre lo que escribiste aquí.

JunoEn la práctica La búsqueda semántica es el flujo completo: incrusta tus documentos una vez, luego para cada consulta incrustalas y devuelve el más cercano por similitud del coseno. Alimenta esas coincidencias más cercanas a un modelo y puede responder desde tus documentos en lugar de memoria. Esa es recuperación, el próximo capítulo, construido directamente sobre esto.

El mecanismo está resuelto: chunificar, incrustar en lotes, almacenar en una DB vectorial, recuperar top-k por similitud. Lo que cambia cuando vas a producción es la fontanería circundante, la re-incrustar al cambio, los metadatos en los que filtras, la versión de modelo que fijas.

Los chunks recuperados no son el fin. En el próximo capítulo los alimentas a un modelo como contexto para que responda desde tus documentos en lugar de su entrenamiento congelado. Eso es RAG, y la calidad de sus respuestas está limitada por la recuperación que construyes aquí.

JunoEn la práctica El bucle es chunificar, lote-incrustar, almacenar en una DB vectorial, recuperar top-k, y producción es la fontanería alrededor: re-incrustar al cambio, filtros de metadatos, una versión de modelo fijada. Esos chunks recuperados se convierten en contexto para un modelo en el próximo capítulo, que es RAG. Sus respuestas son tan buenas como la recuperación que estableciste aquí.

Todo aquí, chunificación, normalización, indexación ANN, recuperación híbrida, frescura y evaluación recall@k, es la capa de recuperación, y es la parte de un sistema de IA que silenciosamente determina si las respuestas son buenas. El modelo en la cima solo puede razonar sobre lo que le entrega la recuperación, así que una respuesta confidente incorrecta es usualmente un fallo de recuperación disfrazado de generación.

Ese es el puente hacia el próximo capítulo. RAG es esta capa de recuperación alimentando contexto fundamentado a un modelo, y cada modo de fallo aquí, vectores obsoletos, coincidencias exactas extrañadas, recall bajo, aparece allá como una respuesta incorrecta. Construye la recuperación para ser medida y fresca, y RAG tiene algo sólido en lo que pararse.

JunoEn la práctica La capa de recuperación, de chunificación a través de recall@k, es lo que decide si las respuestas son buenas, porque el modelo solo razona sobre lo que la recuperación le entrega. Una respuesta confidente incorrecta es usualmente recuperación fallando disfrazada, así que el vector obsoleto o la coincidencia exacta extrañada es el bug real. RAG en el próximo capítulo es esta capa alimentando un modelo, y es tan sólida como lo que construyas aquí.