volver al blog

Embeddings Matryoshka: recorta dimensiones sin perder el hilo

embeddingsragia

El vector que pagas de más

Montas un buscador semántico, eliges text-embedding-3-large porque es el bueno, y cada documento se convierte en un vector de 3072 números. Multiplícalo por medio millón de chunks y ya tienes una factura de memoria y una latencia de búsqueda que no habías presupuestado.

La reacción normal es bajar a un modelo más pequeño. Hay otra salida, menos obvia: quedarte con el modelo bueno y tirar más de la mitad de las dimensiones. Suena a sabotaje, pero si el modelo se entrenó al estilo Matryoshka, el vector recortado sigue funcionando. A veces sorprendentemente bien.

Muñecas rusas, pero con números

La idea la bautizaron por las muñecas rusas: cada una contiene otra más pequeña que sigue siendo una muñeca entera. Un embedding Matryoshka funciona igual. Dentro del vector de 3072 dimensiones vive uno útil de 1024, y dentro de ese, uno de 512, y dentro, uno de 256.

La clave está en el orden. En un embedding normal las dimensiones no tienen jerarquía: la número 5 no es más importante que la 2000. Si cortas por la mitad, cortas información a ciegas. En un embedding entrenado con Matryoshka Representation Learning (MRL) la información se coloca por importancia: las primeras dimensiones cargan con el grueso del significado y las últimas afinan los detalles. Recortar por el final es quedarte con el resumen en vez del texto completo, no romper la frase a la mitad.

Por eso text-embedding-3-small y -large de OpenAI dejan pedir menos dimensiones con el parámetro dimensions: no es un truco de compresión posterior, es que el modelo se entrenó para que sus prefijos fueran válidos.

Por qué funciona y por qué no es magia

Un modelo normal se entrena optimizando una sola función de pérdida sobre el vector completo. MRL entrena optimizando varias a la vez, una por cada tamaño objetivo. El de OpenAI usó pérdidas agregadas en 512, 1024, 1536 y 3072 dimensiones. El modelo aprende a que el prefijo de 512 sirva por sí solo, no solo el vector entero.

El resultado es más contundente de lo que parece: un embedding de text-embedding-3-large recortado a 256 dimensiones aún supera a un ada-002 completo de 1536 en el benchmark MTEB. Menos de una sexta parte del tamaño, y mejor resultado que el modelo de la generación anterior.

No es gratis, ojo. Recortar siempre pierde algo de precisión; MRL solo hace que la pérdida sea pequeña y controlada hasta cierto punto. Y solo funciona con modelos entrenados así: si truncas un embedding cualquiera, obtendrás basura con la misma seguridad con la que corta un cuchillo sin filo.

El detalle que casi todo el mundo olvida

Si dejas que OpenAI te devuelva el vector ya recortado con dimensions, viene normalizado y no tienes que hacer nada. Pero si guardas el vector completo y lo truncas tú en cliente —una estrategia muy razonable, porque te permite elegir el tamaño después— hay un paso que no puedes saltarte: renormalizar.

La similitud del coseno asume vectores de norma 1. Al quedarte con un prefijo, esa norma se rompe y las distancias dejan de ser comparables. Un [:256] a secas mete un sesgo silencioso en todo el ranking.

import numpy as np

def recortar(embedding: list[float], dims: int) -> np.ndarray:
    v = np.array(embedding[:dims], dtype=np.float32)  # prefijo Matryoshka
    return v / np.linalg.norm(v)                       # renormalizar: paso clave

# vector de 3072 dims del modelo -large; nos quedamos con los primeros 256
completo = client.embeddings.create(
    model="text-embedding-3-large",
    input="¿cuánto me cuesta poner la lavadora?",
).data[0].embedding

corto = recortar(completo, 256)   # listo para indexar y comparar por coseno

Un solo create por documento, y decides el tamaño en el momento de indexar. Si mañana necesitas más precisión, reindexas con un prefijo mayor sin volver a llamar a la API.

¿Cuánto puedes recortar?

Depende de cuánto te duela cada punto de precisión frente a cada gigabyte. Como punto de partida con text-embedding-3-large:

DimensionesTamaño relativoUso típico
3072100 %máxima precisión; corpus pequeño o crítico
102433 %equilibrio por defecto para RAG en producción
51217 %catálogos grandes donde la latencia manda
2568 %primer filtro rápido a gran escala

Y aquí viene el patrón que exprime la idea: recuperación adaptativa en dos pasos. Indexas dos versiones —una corta y una larga— y buscas primero con la corta sobre todo el corpus, barata y veloz. Recuperas, pongamos, los 200 candidatos más cercanos y solo a esos les aplicas el vector largo para reordenar. Barres millones de vectores al precio de 256 dimensiones y pagas la precisión de 3072 únicamente sobre un puñado. Es la misma lógica del reranking, pero sin un segundo modelo: te la da el propio embedding.

Cuándo usarlo y cuándo no

Recorta cuando la memoria del índice o la latencia de búsqueda sean tu cuello de botella, cuando trabajes con cientos de miles de vectores o más, o cuando quieras un primer filtro rápido antes de afinar. Es dinero real: menos RAM en la base vectorial, consultas más cortas, misma API.

Piénsatelo si tu corpus es pequeño —con 10.000 chunks el ahorro es ruido y no merece la complejidad— o si estás en un dominio muy fino donde cada matiz cuenta y no te sobra ni una dimensión. Y no lo intentes con un modelo que no se entrenó con MRL: ahí truncar no recorta, mutila.

El titular para llevarse a casa: antes de bajar de modelo para ahorrar, prueba a bajar de dimensiones. Suele salir más a cuenta quedarte con el prefijo del modelo bueno que con el vector entero de uno peor. Mídelo en tu propio corpus con una batería de consultas reales, compara recall a 256, 512 y 1024, y quédate con el escalón más pequeño que aún te sirva. La muñeca más chica que sigue siendo una muñeca.

Fuentes