Embeddings en 2026: las tres familias (denso, esparso, multi-vector), el zoo de modelos y la decisión que importa en producción

Este post abre la subsaga de datos dentro del pipeline LLMOps de seis etapas entrando a la pieza que sostiene el retrieval de tres capas: el embedder. Si el bibliotecario de la curación decidía qué entra al índice y el comité de la cátedra decidía qué sale a la cara del modelo, este post mira al de en medio: el cartógrafo que dibuja el mapa por el que se busca.

TL;DR

La conversación sobre embeddings se ha simplificado en producción hasta el punto de que “qué embedder usas” recibe siempre la misma respuesta: OpenAI text-embedding-3-large en el demo, bge-m3 en la versión “sovereign-ready”. Bajo esa simplificación se esconde el hecho de que un embedder es tres modelos distintos a la vez —denso single-vector, esparso aprendido (SPLADE) y multi-vector late-interaction (ColBERT)— y que en 2026 los modelos punteros no compiten en la misma familia: gte-Qwen2-7B-instruct y NV-Embed-v2 rompen MTEB en single-vector denso, SPLADE-v3 y la cabeza esparsa de bge-m3 dominan el descriptor léxico aprendido, Jina-ColBERT-v2 y ColNomic-7B son lo más fuerte en multi-vector multilingüe, y Snowflake Arctic Embed L 2.0 se ha colado como el favorito multilingüe pequeño con Matryoshka decente. Este post desmonta las tres familias con sus matemáticas (InfoNCE con τ, MaxSim, FLOPS regularization de SPLADE, Matryoshka Representation Learning), repasa el zoo de modelos open source con dimensión, licencia y nicho de cada uno, plantea el problema específico del español multilingüe —que reduce la lista de modelos viables a menos de seis—, cuenta el coste real de almacenamiento por millón de chunks con int8 / binario / TurboQuant, describe cómo se sirven on-premise con TEI, Infinity y vLLM --task embed, fija el hardware mínimo en una RTX 4090 y el bueno en un cluster 4×H100, lista las siete trampas operativas que tiran la calidad sin aviso (drift del corpus, normalización olvidada, dimensión Matryoshka mal elegida, hard negatives ausentes, chat template colado en el embedder, tokenizer drift, MTEB overfit) y cierra con un stack license-clean para producción soberana.

La analogía: tres bibliotecarios fichando el mismo libro

Una biblioteca técnica recibe un libro nuevo y antes de meterlo en las estanterías tiene que generarle un identificador buscable. En la biblioteca conviven tres bibliotecarios con tres oficios distintos, y los tres fichan el mismo libro a la vez:

Bibliotecario A — el temático. Lee el libro entero y le pone una sola etiqueta RFID rica. Esa etiqueta es un vector de 1.024 números donde cada coordenada codifica un eje semántico latente (que el bibliotecario nunca verbaliza: lo ha aprendido leyendo cien millones de libros previos). Dos libros sobre Kubernetes en producción acabarán con etiquetas RFID muy cercanas en el espacio aunque uno hable de Linkerd y el otro de Cilium, porque comparten ejes temáticos. Para buscar, comparas la etiqueta de la pregunta con la del libro y devuelves los más próximos por coseno. Es rápido, escala a millones, y se pierde matices finos. Este es el embedder denso single-vector: bge-m3 en modo dense, gte-Qwen2-7B-instruct, Snowflake Arctic Embed L 2.0, multilingual-e5-large-instruct.

Bibliotecario B — el léxico. No resume nada. Lo que hace es escribir en una ficha la lista pesada de términos relevantes del libro, expandida con sinónimos del campo. El libro de Kubernetes lleva en su ficha “kubernetes 4.2, linkerd 3.8, cilium 3.7, service-mesh 4.1, sidecar 3.2, mtls 2.9, ebpf 2.6, k8s 4.0…” — cada término con un peso. La gracia es que la expansión la hace el modelo: si tu pregunta dice “service mesh” y el libro original solo decía “Linkerd”, la ficha del bibliotecario B sí registró “service-mesh 4.1” porque entendió la relación. Para buscar, intersectas la ficha de la pregunta con la del libro a la vieja usanza: índice invertido, posting lists. Es decisivo cuando el lector escribe pocas palabras muy concretas (nombres de producto, errores, jerga). Este es el embedder esparso aprendido: SPLADE-v3 o la cabeza sparse de bge-m3. Es el sucesor moderno de BM25, no su rival; veremos por qué.

Bibliotecario C — el copista. Se rinde a resumir. Coge cada palabra de cada página del libro y le pone una mini-RFID de 128 dimensiones. Acaba con un libro de 30.000 tokens convertido en 30.000 mini-RFIDs. Cuando buscas algo, el bibliotecario compara cada palabra de tu pregunta con cada palabra del libro y se queda con el máximo por cada palabra de la pregunta, sumándolos. Captura matices que los otros dos pierden por construcción (nombres propios, números, formulaciones específicas), pero su sistema de archivado es un orden de magnitud mayor en espacio en disco. Es ColBERT-v2 / Jina-ColBERT-v2 / ColNomic-7B: late interaction, MaxSim.

Y luego está el bibliotecario polivalente que hace los tres trabajos en una sola pasada: bge-m3. Un solo modelo de 568 M parámetros que devuelve simultáneamente la etiqueta RFID temática (1.024-d), la lista pesada de términos (esparsa) y el conjunto de mini-RFIDs por token (128-d). Esa es la razón por la que se ha consolidado como el embedder estándar de RAG on-premise multilingüe: un único model.encode(chunk) produce las tres salidas que alimentan el retrieval híbrido sin orquestar tres modelos distintos.

Chunk del corpus"Linkerd 3.8 introduce mTLS por defecto…"Denso (single-vector)1 vector × 1024 d (fp16)cosine similarity, HNSWbge-m3 · gte-Qwen2 · e5Esparso aprendido~80 términos pesados ⊂ vocab 30kíndice invertido, posting listsSPLADE-v3 · bge-m3 sparseMulti-vector (late int.)N tokens × 128 d (fp16)MaxSim por token de queryColBERT-v2 · Jina-ColBERT-v2Storage / 1 M docs (d=1024, fp16)denso fp16: 2 GB · int8: 1 GB · binario: 128 MBSPLADE: ~25 MB (posting lists)ColBERT fp16 (256 tok×128 d): 64 GBbge-m3 (3-en-1)un solo forward → dense + sparse + colbert568 M params · XLM-RoBERTa-large100+ idiomas · 8192 tokens · MITEl zoo open source 2026jina-v3/v4 · nomic-v2 MoE · Snowflake Arctic L 2.0gte-Qwen2-7B · multilingual-e5-large-instructSPLADE-v3 · Jina-ColBERT-v2 · ColNomic-7B

El mismo chunk indexado por tres bibliotecarios. El polivalente bge-m3 ejecuta los tres en una sola pasada de 568 M parámetros.

Qué es realmente un embedding

Un embedding de texto es una función f : texto → ℝᵈ entrenada para que dos textos “semánticamente parecidos” produzcan vectores cercanos en ese espacio. La parte importante no es “vector” —cualquier hash de longitud fija lo es— sino qué significa cercanía. La cercanía se define implícitamente por la pérdida con la que el modelo se entrena.

Casi todos los embedders modernos se entrenan con InfoNCE (también llamado Multiple-Negatives Ranking Loss en sentence-transformers):

\mathcal{L}_{\text{InfoNCE}} = -\log \frac{\exp(\text{sim}(q, d^+)/\tau)}{\sum_{d \in \mathcal{B}} \exp(\text{sim}(q, d)/\tau)}

Para cada pareja (query, doc⁺) el modelo tiene que asignar al positivo más similitud que a todos los demás documentos del batch B, donde los demás documentos hacen de in-batch negatives gratis. La temperatura τ (típica 0.020.07, casi siempre 0.05) controla cuánto se afila la distribución: τ baja → el modelo se vuelve más exigente con el positivo pero más inestable. Tamaño de batch |B| grande → muchos más negativos por gradiente → modelo más informado. Por eso los embedders se entrenan con batch ≥ 1.024 en clusters de H100 con AllGather entre nodos para apilar todos los negativos del cluster como un solo batch efectivo.

A los negativos in-batch se les añade hard negatives mining: documentos seleccionados a propósito porque están “casi en la respuesta” (típicamente los siguientes 10-100 vecinos de un retrieval BM25 / dense previo). Sin hard negatives el modelo aprende a discriminar lo trivial y la calidad real en BEIR / MTEB se hunde 5-10 puntos.

Esto importa porque la familia de embedder depende de qué entras a sim(·,·):

Familiasim(q, d)Salida del modelo
Denso single-vectorproducto escalar de dos vectores 1024-d normalizadosf(q), f(d) ∈ ℝ^d
Esparso aprendidoproducto escalar de dos vectores 30.522-d (≈80 no-cero cada uno)f(q), f(d) ∈ ℝ^V, `V =
Multi-vectorΣᵢ maxⱼ ⟨qᵢ, dⱼ⟩ (MaxSim)`f(q) ∈ ℝ^{

La pérdida es la misma en los tres casos —InfoNCE— pero la geometría del espacio cambia, y con ella la calidad, el coste de almacenamiento y la latencia de búsqueda.

Las tres familias en detalle

Denso single-vector — el cartógrafo

El embedder lee el chunk completo, lo pasa por un transformer codificador (XLM-RoBERTa, BERT, Mistral-decoder con prompt) y agrega las representaciones de tokens en un único vector mediante:

  • CLS pooling: usa el embedding del token [CLS]. Estándar en BERT-base.
  • Mean pooling: media simple de los embeddings de todos los tokens. Estándar en multilingual-e5-large-instruct, bge-m3 dense, Snowflake Arctic Embed L 2.0.
  • Last-token pooling: para embedders basados en decoder-LLM (e5-mistral-7b-instruct, gte-Qwen2-7B-instruct, NV-Embed-v2) que toman el token final como agregado. Funciona porque el modelo es causal y el último token “ha visto” todo el contexto.
  • Latent-attention pooling: novedad de NV-Embed-v2. Una capa de atención learnable que pondera tokens en vez de promediarlos. +2-3 puntos MTEB sobre mean pooling.

Tras el pooling se normaliza a norma 1: v ← v / ‖v‖₂. Con vectores normalizados, coseno y producto escalar coinciden:

\cos(q, d) = \frac{q \cdot d}{\|q\|\|d\|} = q \cdot d, \qquad \|q - d\|^2 = 2 - 2\,(q \cdot d)

Por eso casi todos los vector DB indexan por inner product y dejan al usuario que normalice antes (Qdrant, Faiss IP, Milvus). Si olvidas normalizar el vector de la query pero los del corpus sí están normalizados, el retrieval se degrada en silencio: la magnitud de la query distorsiona el ranking. Es el bug número uno en RAG en producción.

Ejemplo numérico mínimo, dos vectores 4-d normalizados:

q = [0.5, 0.5, 0.5, 0.5]          ‖q‖ = 1
d₁ = [0.6, 0.4, 0.5, 0.5]         ‖d₁‖ = 1.005, normalizado [0.597, 0.398, 0.498, 0.498]
q · d₁ = 0.5·0.597 + 0.5·0.398 + 0.5·0.498 + 0.5·0.498 = 0.995

Cercanía cuasi-1, como esperábamos. Si d₁ no estaba normalizado, q · d₁ = 1.0 y aparecería más cerca que cualquier d perfectamente alineado pero con norma < 1.005. La normalización no es un detalle: es el contrato del espacio.

Esparso aprendido — el descriptor léxico

SPLADE-v3 (Naver, marzo 2024) ha consolidado la versión moderna del bibliotecario léxico. Internamente es un BERT pequeño (~110 M parámetros, base DistilBERT/BERT) que produce, para cada token de entrada, una distribución sobre todo el vocabulario (30.522 dimensiones en BERT WordPiece), y luego hace max-pool sobre los tokens:

w_j = \max_{i \in \text{seq}} \log\bigl(1 + \text{ReLU}(W_{ij})\bigr)

donde Wᵢⱼ es el logit del token de entrada i para el término del vocabulario j. El log(1+ReLU) satura los logits altos (evita que una sola palabra domine el vector) y la ReLU corta los negativos. El resultado es un vector de 30.522 dimensiones del que típicamente quedan 50-200 entradas no nulas.

La parte clave es la regularización FLOPS que se añade a la pérdida durante el entrenamiento:

\mathcal{L}_{\text{FLOPS}} = \lambda \cdot \sum_{j=1}^{V} \bar{w}_j^2, \qquad \bar{w}_j = \frac{1}{|B|}\sum_{i \in B} w_{ij}

Penaliza el coste esperado de los posting lists: si una palabra del vocabulario aparece en promedio en muchos documentos, sumarla a un nuevo documento penaliza el doble. El modelo aprende a generar vectores esparsos por construcción.

¿Esto qué le pasa al texto “Linkerd 3.8 introduce mTLS por defecto”? Que el modelo no solo escribe los términos literales — escribe también, con peso menor pero no cero, “service-mesh”, “kubernetes”, “tls”, “sidecar”, “envoy”, “istio” (su competidor, también semánticamente relacionado), “encryption”, “k8s”. Esa expansión semántica del documento es lo que diferencia SPLADE de BM25. BM25 solo sabe lo que estaba literalmente en el texto; SPLADE sabe lo que un experto añadiría como descriptor.

En la práctica SPLADE-v3 vence a BM25 por 3-6 puntos MRR@10 en MS MARCO y domina BEIR zero-shot. El coste es ~2-4× la latencia de query de BM25 sobre el mismo índice invertido, mitigable con podas estáticas.

Para el caso multilingüe, bge-m3 en su cabeza sparse es la única opción mantenible: SPLADE-v3 está entrenado en inglés y los ports multilingües están en estado experimental.

Multi-vector — el copista

ColBERT-v2 (Stanford, NAACL 2022) introdujo el paradigma de late interaction. En vez de comprimir el documento a un solo vector, lo deja como una matriz (|d|, k) con un vector de k dimensiones por cada token. La similitud entre query y documento se calcula token-a-token y se agrega con MaxSim:

s(q, d) = \sum_{i \in q} \max_{j \in d} \langle q_i, d_j \rangle

Lo que se está computando: para cada palabra de la query, encuentra el token del documento que mejor le encaja y suma esa similitud. La suma es sobre la query, no sobre el documento. Esto permite que un documento de 30.000 tokens compita justo con otro de 200, porque la query siempre suma |q| términos.

¿Por qué le da más calidad que el dense single-vector? Porque el resumen a un vector pierde información sobre dónde estaba cada idea. Si la query es “qué versión introdujo mTLS por defecto en Linkerd”, el resumen denso del documento solo sabe que el chunk va de “Linkerd y mTLS”; el copista de ColBERT puede emparejar “qué versión” con “3.8” porque guarda el embedding del token 3.8 por separado. En BEIR / out-of-domain, late interaction supera a single-vector entre +2 y +6 nDCG@10 con el mismo backbone.

El precio es el almacenamiento. Por documento, un dense single-vector de 1024 dimensiones en fp16 ocupa 1024 × 2 = 2 KB. ColBERT-v2 con tokens de 128 dimensiones para un chunk de 256 tokens ocupa 256 × 128 × 2 = 65.536 B ≈ 64 KB: 32× más espacio. Con la compresión residual nbits=2 de ColBERT-v2 baja a ~16 KB (8×). Jina-ColBERT-v2 añade Matryoshka sobre las dimensiones del token (truncable a 128 / 96 / 64), bajando otro 50%.

Para 1 millón de chunks:

FamiliaPor docTotal
Denso fp32 (1024-d)4.096 B4,0 GB
Denso fp16 / halfvec2.048 B2,0 GB
Denso int8 (SQ)1.024 B1,0 GB
Denso binario (1 bit/d)128 B128 MB
SPLADE (≈ 80 términos × 8 B)~640 B~640 MB
ColBERT fp16 (256 tok × 128 d)65.536 B64 GB
ColBERT residual nbits=2~16.000 B~16 GB
Jina-ColBERT-v2 (MRL token 64)~8.000 B~8 GB

ColBERT en producción on-premise se reserva para corpus de hasta unos pocos millones de chunks, o se aplica solo como reranker sobre los top-100 del primer comité (denso + esparso), como se describe en el post de reranker.

Matryoshka — la dimensión truncable

Una palanca operativa que ha cambiado la conversación sobre embeddings entre 2024 y 2026 es Matryoshka Representation Learning (Kusupati et al., NeurIPS 2022). El truco: durante el entrenamiento, además de la pérdida sobre el vector completo de D dimensiones, se calcula la misma pérdida sobre prefijos del vector:

\mathcal{L}_{\text{MRL}} = \sum_{k \in \{64, 128, 256, 512, 1024\}} \alpha_k \cdot \mathcal{L}_{\text{InfoNCE}}\bigl(\text{emb}[:k]\bigr)

Las primeras k dimensiones del embedding se entrenan para ser, por sí solas, un embedding válido. En inferencia, si quieres un embedding más barato, truncas el vector: el primer cuarto es ya un embedding utilizable. Sin Matryoshka, truncar destroza la geometría: las primeras 256 dimensiones de un embedding entrenado solo en 1024-d no codifican nada coherente.

Degradación típica en MTEB nDCG@10 al truncar un embedder MRL:

TruncadoPérdida promedio
1024 → 512-1 a -2 puntos
1024 → 256-3 a -5 puntos
1024 → 128-5 a -8 puntos
1024 → 64-8 a -12 puntos

Modelos MRL nativos en 2026 (los que te permiten elegir la dimensión en runtime sin reentrenar):

  • jina-embeddings-v3 (1024 → 32, paso fino, CC-BY-NC-4.0)
  • jina-embeddings-v4 (2048 → 128, multimodal texto+imagen, CC-BY-NC-4.0)
  • nomic-embed-text-v2-moe (768 → 256, Apache 2.0)
  • Snowflake-arctic-embed-l-v2.0 (1024 → 256, Apache 2.0)
  • mxbai-embed-large-v1 y mxbai-embed-2d-large-v1 (este último también truncable en profundidad de capa)
  • Stella_en_1.5B_v5 (paso múltiple 512 / 768 / 1024 / 2048 / 4096 / 6144 / 8192, MIT, solo inglés)
  • text-embedding-3-large (OpenAI, 3072 → 256, API only)
  • voyage-3 family (1024 → 256 / 512 / 1024 / 2048, API only)

Recomendación de producción: usa siempre un modelo MRL aunque no truques al inicio, porque la decisión de cuantización futura te la simplifica. Y, sobre todo, evalúa el truncado con tu corpus real: la degradación promedio MTEB de “-3 puntos” se vuelve “-12 puntos” en un dominio nicho.

El zoo de modelos open source 2026

Lo que sigue es una ficha técnica por modelo. Verificación al 2026-06: HuggingFace cards + papers de referencia + leaderboard MTEB / MMTEB.

Denso single-vector

ModeloParamsDimTokensIdiomasLicenciaDistintivo
BAAI/bge-m3 (dense)568 M10248192100+MITTri-modo (dense + sparse + colbert) en un forward. Estándar de facto on-prem multilingüe.
Snowflake/snowflake-arctic-embed-l-v2.0568 M1024 (MRL → 256)8192~100Apache 2.0Entrenado para multilingüe + inglés sin degradar ninguno. MIRACL 55.8.
intfloat/multilingual-e5-large-instruct560 M1024512~100MITBaseline multilingüe veterano. Ventana corta.
intfloat/e5-mistral-7b-instruct7.1 B40964096inglésMITPrimer decoder-as-embedder en romper MTEB. Inglés.
Alibaba-NLP/gte-Qwen2-7B-instruct7 B358432.768100+Apache 2.0Único con contexto 32k. Top MTEB-en, fuerte multilingüe.
nvidia/NV-Embed-v27.85 B409632.768inglésCC-BY-NC-4.0Latent-attention pooling. Calidad top. License blocker en prod.
Linq-AI-Research/Linq-Embed-Mistral7 B40964096inglésCC-BY-NC-4.0Top retrieval MTEB ago-2024. No comercial.
NovaSearch/stella_en_1.5B_v51.54 B8192 (MRL múltiple)8192inglésMITPequeño + MRL rico. Inglés.
BAAI/bge-multilingual-gemma29 B35848192100+GemmaCalidad alta, licencia Gemma restringe redistribución.
BAAI/bge-en-icl7 B40968192inglésMIT-styleIn-context-learning de ejemplos en el prompt.
mixedbread-ai/mxbai-embed-large-v1335 M1024 (MRL)512inglésApache 2.0MRL + binary nativo. Ventana corta.
jinaai/jina-embeddings-v3570 M1024 (MRL → 32)819289CC-BY-NC-4.0LoRA por tarea. No comercial sin licencia.
nomic-ai/nomic-embed-text-v2-moe475 M / 305 M activos768 (MRL → 256)512~100Apache 2.0Primer MoE general-purpose en embeddings.

Esparso aprendido

ModeloParamsVocabTokensIdiomasLicenciaDistintivo
naver/splade-v3110 M30.522512inglésCC-BY-NC-SA-4.0SOTA sparse aprendido. No comercial.
BAAI/bge-m3 (sparse head)568 MXLM-R vocab8192100+MITLa única opción multilingüe license-clean.

Multi-vector (late interaction)

ModeloParamsDim/tokenTokensIdiomasLicenciaDistintivo
colbert-ir/colbertv2.0110 M128512inglésMITEl paper original, base de todo.
jinaai/jina-colbert-v2560 M128 / 96 / 64 (MRL)819289Apache 2.0El multi-vector multilingüe license-clean.
nomic-ai/colnomic-embed-multimodal-7b7 B3584 (Qwen2-VL)~100Apache 2.0Multi-vector multimodal texto+imagen. Vidore-v2 SOTA open.

El leaderboard, con cautela

El MTEB / MMTEB (Massive Multilingual Text Embedding Benchmark, Enevoldsen et al., arxiv 2502.13595) es el termómetro estándar. Top retrieval en MMTEB a mediados de 2026 está dominado por Qwen3-Embedding-8B (~70.6 multilingual avg) y Llama-Embed-Nemotron-8B. Por debajo, los modelos de 7B (gte-Qwen2-7B, NV-Embed-v2) y los de 568M (bge-m3, Snowflake-Arctic-L-2.0) compiten por tarea.

Trampa: MTEB se ha empezado a saturar por dataset contamination. Cuanto más alto el ranking, más probable es que el modelo haya visto en entrenamiento subconjuntos de los datasets de evaluación. La regla en producción: el leaderboard es para descartar modelos malos, no para elegir el mejor. La decisión final se toma sobre un eval set propio del dominio, generado con la receta de LLM-as-judge o de evals.

El problema concreto del español multilingüe

Para un cliente español que sirve documentación corporativa, jurídica o de soporte en castellano (y muchas veces catalán / portugués / inglés mezclados), el zoo de embedders se acota a menos de seis modelos viables. Las exclusiones operativas:

  1. Modelos solo en inglés: e5-mistral-7b-instruct, stella-en-1.5B-v5, Linq-Embed-Mistral, mxbai-embed-large-v1, NV-Embed-v2, SPLADE-v3. Reducen el rendimiento en castellano por debajo del nivel aceptable: traducir la query al inglés antes de buscar es una vía, pero introduce latencia, drift de tokenización y otra dependencia de modelo.
  2. Modelos con licencia no comercial: jina-embeddings-v3, jina-embeddings-v4, NV-Embed-v2, Linq-Embed-Mistral. Sirven para PoC, pero a producción comercial exigen acuerdo explícito con el vendor. Salvo que tengas la licencia firmada, hay que excluirlos.
  3. Modelos con licencia Gemma: bge-multilingual-gemma2. Permitido para uso interno, complicado redistribuir pesos a un cliente.

Los que quedan, ordenados por orden de elección práctica en producción soberana:

  1. BAAI/bge-m3 — MIT, 568 M, 100+ idiomas (incluyendo castellano y catalán, entrenados explícitamente), 8.192 tokens, tri-modo dense+sparse+colbert. Default razonable. Cabe en una RTX 4090. Lo sirve TEI y Infinity nativamente.
  2. Snowflake/snowflake-arctic-embed-l-v2.0 — Apache 2.0, mismo tamaño, Matryoshka explícito, mejor MIRACL/CLEF que bge-m3 en algunas tareas multilingües, sin cabeza sparse. Si la prioridad es MMTEB puro en castellano.
  3. intfloat/multilingual-e5-large-instruct — MIT, 560 M, baseline veterano. Ventana de 512 tokens es su gran limitación: documentos largos hay que partirlos antes. Si lo que ya tienes en producción funciona, no migres por moda.
  4. Alibaba-NLP/gte-Qwen2-7B-instruct — Apache 2.0, contexto 32k, calidad alta en castellano (Qwen2 está bien entrenado en español). Si los chunks son largos (más de 4k tokens) y dispones de GPU para servirlo (no entra en 4090; sí en H100). Cabe junto a un LLM en un H100 80GB con cuidado.
  5. nomic-ai/nomic-embed-text-v2-moe — Apache 2.0, 305 M activos, MRL, ~100 idiomas. Si la latencia y el coste por token mandan: el MoE le da throughput desproporcionado para su calidad.
  6. jinaai/jina-colbert-v2 — Apache 2.0, multi-vector multilingüe, como reranker o como retrieval principal en corpus pequeño (< 1 M chunks). El único multi-vector license-clean en castellano.

La regla del pulgar: bge-m3 como dense+sparse en primera línea, jina-colbert-v2 como tercera capa de reranking cuando lo amerita el caso de uso, y Snowflake Arctic L 2.0 como alternativa si el eval específico del corpus prefiere su geometría.

Servir embeddings on-premise

Tres motores se reparten el panorama de servir embeddings on-prem en 2026, con perfiles distintos.

Text Embeddings Inference (TEI) — el estándar

huggingface/text-embeddings-inference es un servidor escrito en Rust con backend Candle / ONNX, FlashAttention integrado y batching dinámico por tokens. Expone una API OpenAI-compatible /v1/embeddings y soporta los tres modos de bge-m3 simultáneamente desde la versión 1.5. Para producción multilingüe es el default obvio.

# values.yaml — TEI sirviendo bge-m3 multilingüe sobre RTX 4090
image: ghcr.io/huggingface/text-embeddings-inference:1.5
args:
  - --model-id=BAAI/bge-m3
  - --pooling=cls
  - --max-batch-tokens=16384
  - --max-concurrent-requests=512
  - --dtype=float16
resources:
  limits:
    nvidia.com/gpu: 1

Throughput orientativo con bge-m3, fp16, secuencia 512 tokens, batch 32:

  • RTX 4090 (24 GB): ~8–15 k tokens/s
  • A100 80 GB: ~60 k tokens/s sostenidos
  • H100 80 GB: ~40–80 k tokens/s, con fp8 ~50% adicional

(Los rangos son aproximados y dependen de batch real, longitud media de secuencia y compilación con FA2/FA3.)

Infinity — el flexible

michaelfeil/infinity (MIT) es un servidor FastAPI multi-modelo capaz de cargar bge-m3, Snowflake Arctic, Jina-v3, Nomic, ColPali, CLAP y rerankers simultáneamente desde la misma API estilo OpenAI. Backend PyTorch + Optimum (ONNX/TensorRT) o CTranslate2. Útil cuando necesitas servir varios embedders distintos (uno para texto, otro para código, otro para imágenes) detrás de un único endpoint, o cuando el modelo todavía no tiene soporte en TEI.

vLLM --task embed — para los embedders 7B

Cuando el embedder es realmente un LLM-decoder convertido en embedder (e5-mistral-7b-instruct, gte-Qwen2-7B-instruct, NV-Embed-v2, Stella-1.5B), el lugar natural para servirlo es vLLM, que ya tiene en producción la pila de PagedAttention y continuous batching:

vllm serve Alibaba-NLP/gte-Qwen2-7B-instruct \
  --task embed \
  --dtype bfloat16 \
  --max-model-len 32768 \
  --trust-remote-code

vLLM detecta el pooling correcto (last-token en los basados en Qwen / Mistral) y expone /v1/embeddings compatible con OpenAI. Para clusters de inferencia que ya están corriendo vLLM con un LLM en otro puerto, es la forma natural de servir el embedder sin levantar otro stack.

fastembed — el liviano

qdrant/fastembed carga bge-small, MiniLM, ColBERT y BM25/SPLADE sparse en ONNX-CPU. No es competitivo en throughput contra TEI/Infinity con GPU, pero es la opción correcta cuando hay que servir embeddings en un nodo NUC sin GPU (ver entornos mixtos NVIDIA + Intel) o cuando el embedder forma parte del cliente (preview en una UI, scoring previo en un edge).

Almacenamiento, cuantización y el cálculo del corpus

El embedding no se queda en memoria del embedder: vive en el índice del vector DB y se materializa cada vez que ingestas un nuevo chunk. El cálculo del coste de almacenamiento es lo que decide la dimensión final, no la calidad MTEB. Para 1 millón de chunks con embedder denso a 1024-d:

fp32        : 1.024 dims × 4 B × 1 M = 4.096 MB ≈ 4,0 GB
fp16/halfvec: 1.024 dims × 2 B × 1 M = 2.048 MB ≈ 2,0 GB
int8 (SQ)   : 1.024 dims × 1 B × 1 M = 1.024 MB ≈ 1,0 GB
binario     :   128  B × 1 M         ≈    128 MB

Las opciones de cuantización en orden de uso real (mid-2026):

  1. halfvec (fp16): el default en pgvector 0.7+ y en cualquier vector DB serio. Pérdida MTEB nula, 2× compresión. Siempre actívalo.
  2. Scalar Quantization int8 (SQ): cada componente del vector se mapea a int8 con un min/max global. Pérdida típica de recall@10: 0–1%. 4× compresión. El default de Qdrant, soportado en Milvus y Weaviate.
  3. Cuantización binaria: bit = sign(v_i - μ_i). 32× compresión bruta. Pérdida en frío 5–15%. Mitigada con rotación previa Hadamard / TurboQuant (Qdrant 1.18, dic 2025): pre-multiplica por una matriz ortogonal aleatoria que reparte la energía entre dimensiones antes de binarizar. Tras TurboQuant la pérdida cae a 1–3%. Combina además con rescoring sobre los fp16 originales para los top-100 candidatos.
  4. Product Quantization (PQ): el clásico de FAISS. Hasta 64× compresión, pérdida 2–5%. Más complejo de operar (requiere entrenar codebook); en 2026 ha cedido terreno a binary + rescoring.

Un corpus de 100 millones de chunks (cifra real de un RAG corporativo grande) con bge-m3 denso:

FormatoTotal
fp32400 GB
fp16200 GB
int8100 GB
binario + Hadamard12,5 GB

La diferencia entre 200 GB y 12,5 GB es la diferencia entre necesitar un nodo dedicado de vector DB con 8 NVMe en RAID y poder caber en RAM de un solo nodo. Para corpus grandes, la cuantización ya no es una optimización: es la única forma de operar.

El integration con vector DB

Los vector DB de 2026 se han convertido en DBs híbridos que indexan los tres tipos a la vez. El mapa rápido:

Vector DBHíbrido nativoMulti-vector / ColBERTCuantización
Qdrant ≥1.10RRF/DBSF en query_points con dense + sparse + colbert en una colecciónSí, nativo (one-shot MaxSim)SQ int8, binaria, TurboQuant 1.18
Weaviatehybrid(alpha=0.75) BM25 + dense, named vectors multi-targetSí, como named vector multi-vectorPQ, SQ, BBQ rotacional 8-bit
Milvus ≥2.4Multi-vector + sparse en schema; 2.5 añade BM25 full-text nativoMulti-vector field, MaxSim orquestado desde clienteSQ, PQ, CAGRA GPU
pgvector 0.7+/0.8halfvec, sparsevec, bit; HNSW para los tresNo nativo (workaround tabla separada)binary_quantize(), halfvec, rescoring con <#> exacto
Elasticsearch / OpenSearchsparse_vector (ELSER, SPLADE) + dense_vector HNSW; RRFOpenSearch 3.x síES 9: int8_hnsw por defecto, BBQ binary quantization

Para producción soberana on-prem en castellano, la combinación más fácil de operar en 2026 es Qdrant + bge-m3: una sola colección indexa los tres modos del mismo modelo, el query híbrido con RRF se hace en una llamada, la cuantización TurboQuant baja el corpus a niveles manejables, y el operador es un binario Go con backups simples a S3/MinIO. pgvector + bge-m3 es la otra opción razonable cuando ya tienes Postgres con HA y no quieres meter una segunda DB en el inventario operativo; pierdes multi-vector nativo, pero ganas SQL transversal sobre los chunks.

Los parámetros de HNSW que sí o sí hay que tocar:

  • M: conexiones por nodo en el grafo. 16–32 típico. Más alto → más recall, más RAM. Para corpus pequeños (<1 M) M=16; para corpus medianos (10 M) M=24; para corpus grandes M=32+IVF-PQ o M=32+binary.
  • ef_construction: ancho de búsqueda durante la construcción. 100–400. Más alto → grafo mejor, construcción más lenta. Construye con ef_construction=400 aunque sea lento; lo pagas una vez.
  • ef_search: ancho durante la query. 50–200. La perilla principal del trade-off recall/latencia en runtime. Empieza en 64 y mide.

Implicaciones para inferencia on-premise

El embedder no comparte hardware con el LLM tan cómodamente como podría parecer. Las cuentas:

  • bge-m3 (568 M) ocupa unos 568 × 2 = 1.136 MB en fp16 para los pesos, más el KV cache de batch, más activaciones temporales. En la práctica se sirve cómodamente en 6–8 GB de VRAM incluso a batch alto. Cabe junto a un LLM de 7B-Q4 en una RTX 4090.
  • gte-Qwen2-7B-instruct requiere ~14 GB fp16 solo de pesos. No cabe junto a un LLM 7B en una 4090; en una H100 80 GB sí, con cuidado en el batching simultáneo.
  • jina-colbert-v2 (560 M) ocupa ~1.1 GB de pesos, pero el almacenamiento del índice multi-vector es el coste real: 8 GB por millón de chunks aun con Matryoshka y compresión.

En la RTX 4090 (24 GB)

Stack mínimo realista para un RAG en castellano con corpus <1 M chunks:

GPU 24 GB ┐
          ├─ TEI bge-m3 (dense + sparse + colbert) │ ~6 GB VRAM, ~12 k tok/s
          └─ vLLM Qwen2.5-7B-Instruct AWQ Q4       │ ~8 GB VRAM, ~80 tok/s
CPU/RAM  ┐
          ├─ Qdrant con bge-m3 dense + sparse + colbert │ ~3 GB RAM por M chunks
          └─ FastAPI gateway (LiteLLM)

Sirve unas decenas de QPS de RAG con calidad multilingüe decente. Es la configuración de PoC y de despliegue para una sede pequeña.

En el cluster 4×H100 80 GB

Para el caso producción con varios millones de chunks y SLO de p99 < 500 ms:

H100 #1 (80 GB) ── vLLM Qwen3-72B-Instruct AWQ + Qwen2.5-7B speculative ┐
H100 #2 (80 GB) ── vLLM gte-Qwen2-7B-instruct (embedding 32k ctx)        │ LLM + embed grande
H100 #3 (80 GB) ── TEI bge-m3 multi-tenant + jina-colbert-v2 reranker    │ embed mediano
H100 #4 (80 GB) ── Hold-out para canary / shadow                          │ ver post canary
Qdrant cluster (3 nodos CPU + NVMe) ── 100 M chunks indexados (binary + TurboQuant + rescoring)

Esta configuración separa el LLM grande del embedder grande (que comparten arquitectura Qwen2 pero compiten por VRAM si se les pone en la misma GPU) y deja un H100 entero para variantes en canary. El bge-m3 cabe sobrado con el reranker en una sola H100, sirviendo decenas de miles de requests/min.

Las siete trampas operativas del embedder

  1. No normalizar el vector de la query. Coseno y producto escalar coinciden solo cuando ambos vectores son unitarios. Si en el cliente olvidas v ← v / ‖v‖₂, los resultados están “casi bien” — los top-1 siguen siendo correctos en queries triviales, los top-10 ya no — y nadie se da cuenta hasta que la calidad de RAG cae 8 puntos. Solución: bake la normalización en el adapter del embedder, no en el cliente.

  2. Chat template colado en el embedder. Algunos embedders basados en LLM (e5-mistral-7b-instruct, gte-Qwen2-7B) esperan un prompt instructivo concreto antes del texto a embeber ("Instruct: Retrieve relevant passages\nQuery: ..."). Olvidarlo deja el rendimiento ~5 puntos MTEB por debajo. Solución: leer el usage_template de la model card y meterlo en el wrapper del embedder.

  3. Dimensión Matryoshka mal elegida. El default de muchos clientes Qdrant / pgvector es dim=768. Si tu embedder es MRL nativo a 1024 → 768, OK. Si es 1024 sin MRL, truncar a 768 destroza el espacio (perdida típica -8 puntos MTEB). Solución: usar el dim nativo del modelo y truncar solo cuando el almacenamiento manda, y solo en modelos MRL.

  4. Hard negatives ausentes en fine-tuning. Cuando se fine-tunea el embedder con datos propios (lo cual debería ser práctica estándar para RAG corporativo), si la mini-batch solo lleva positivos y negativos in-batch del mismo dominio, el modelo aprende que cualquier cosa fuera del dominio es negativo, pero dentro del dominio no discrimina. Solución: minar hard negatives con BM25 / dense del propio corpus antes de fine-tunear.

  5. Drift del corpus sin reindexar. Cuando reentrenas o reemplazas el embedder pero solo aplicas el modelo nuevo a chunks nuevos, acabas con el índice mezclando dos geometrías incompatibles. Los chunks del modelo viejo y del nuevo no son comparables por coseno. Solución: cada cambio de embedder es una reindexación completa del corpus, planificada como un retrain operativo.

  6. Tokenizer drift entre cliente y modelo. El cliente Python que prepara las queries usa su propio tokenizer (a veces tiktoken por defecto) y trunca a 8.192 tokens. El embedder usa XLM-R con sentencepiece y trunca a 8.192 de su propio tokenizer. Las queries largas se truncan de manera diferente; los embeddings del corpus son consistentes pero los de query no. Solución: usar el tokenizer del modelo en el cliente o en el wrapper.

  7. MTEB overfit como guía de elección. El leaderboard MTEB se ha vuelto métrica contaminada: hay evidencia de que modelos punteros han visto en entrenamiento subconjuntos de los datasets de evaluación. El modelo +0,5 puntos sobre el segundo no es necesariamente mejor para tu dominio. Solución: un eval set propio del dominio (100-300 query-doc pairs etiquetadas) ejecutado con la receta de evals decide.

Stack license-clean para producción soberana

Pongamos por escrito el final recomendado. Para una organización española sirviendo RAG corporativo on-prem bajo ENS / ISO 42001 / EU AI Act, con corpus 1-50 M chunks en castellano + inglés + catalán:

CapaComponenteLicenciaJustificación
Embedder denseBAAI/bge-m3MITMultilingüe robusto, 8k tokens, license-clean, servido por TEI
Embedder sparsebge-m3 sparse headMITMisma pasada que el dense, no requiere segundo modelo
Reranker capa 2BAAI/bge-reranker-v2-m3MITCross-encoder multilingüe del mismo equipo
Reranker capa 3 (opcional)jinaai/jina-colbert-v2Apache 2.0Multi-vector multilingüe license-clean
Servidor embedTEI + Infinity para multi-modeloApache 2.0 / MITStack soportado
Vector DBQdrant (preferido) o pgvector 0.8Apache 2.0 / PostgreSQLHíbrido nativo + cuantización
Cuantizaciónint8 SQ + binary + TurboQuant + rescoringApache 2.0Reduce corpus 16×–32× con < 3% pérdida
Hardware mínimoRTX 4090 24 GBPara PoC y sedes pequeñas
Hardware producciónCluster 4×H100 80 GBPara RAG con SLO p99 < 500 ms

El stack alternativo, si MMTEB explícito en castellano pesa más que la tri-modalidad de bge-m3: sustituir bge-m3 por Snowflake/snowflake-arctic-embed-l-v2.0 (Apache 2.0, MRL → 256) y añadir explícitamente SPLADE-v3 o BM25 puro para la capa sparse. Pierde la elegancia del único forward, gana 1-2 puntos en MIRACL castellano.

Conclusión

El embedder es la pieza más fácil de simplificar mal en un RAG y la que más decide la calidad real cuando todo lo demás está en su sitio. Las tres familias (denso, esparso, multi-vector) no son tres opciones a elegir sino tres oficios que bge-m3 ejecuta en una sola pasada y que el retrieval híbrido consume en paralelo. La matemática que importa es modesta —InfoNCE con τ, MaxSim, FLOPS regularization, MRL— pero las trampas operativas son numerosas y silenciosas: normalización olvidada, chat template ausente, dimensión Matryoshka mal elegida, tokenizer drift. Para producción soberana en castellano la lista de modelos viables cabe en menos de una decena, y la decisión real se reduce a “bge-m3 o Snowflake Arctic L 2.0”, con jina-colbert-v2 añadido como capa tres cuando la calidad fina justifica el coste. El stack license-clean cabe en una RTX 4090 para PoC y se escala a un cluster 4×H100 para producción real.

Ver también

Referencias