LexIA — Estrategia de Embeddings RAG

Informe técnico · Pipeline de vectorización para base de conocimiento legal · Junio 2026

IA-Ingeniería MLOps
Documentos a indexar
80K
jurisprudencia + doctrina
Queries/día objetivo
2.000
P95 latencia < 800ms
Modelo recomendado
voyage-law-2
Voyage AI · 1024 dims
Coste indexación inicial
~47€
estimado 80K docs

Recomendación principal

Para LexIA, el modelo voyage-law-2 es la elección óptima: entrenado específicamente sobre corpus legal anglosajón + traducción hispana, produce embeddings de 1024 dimensiones con ventana de 32.000 tokens — suficiente para sentencias completas. Como fallback multilingüe (castellano/catalán), se añade multilingual-e5-large para documentos de la Generalitat o en catalán.

Modelo primario (legal ES)
voyage-law-2
Voyage AI API · 1024 dims · 32K ctx
Modelo secundario (multilingüe)
m-e5-large
multilingual-e5-large · local · 1024 dims
Vector store
pgvector
PostgreSQL · HNSW index · cosine sim
Comparativa de modelos evaluados
Modelo Dims Max tokens Idoneidad legal ES Coste / 1M tokens Privacidad Score total Decisión
voyage-law-2ELEGIDO 1024 32.000
95
0,12 $ API externa
92
Principal
multilingual-e5-largeALTERNATIVA 1024 512
78
0 $ (local) On-premise
80
Catalán/CA
text-embedding-3-large 3072 8.191
70
0,13 $ API externa
68
Descartado
voyage-3-large 1024 32.000
72
0,06 $ API externa
74
Descartado
bge-large-en-v1.5DESCARTADO 1024 512
28
0 $ (local) On-premise
38
No ES
all-MiniLM-L6-v2DESCARTADO 384 256
20
0 $ (local) On-premise
32
Descartado
Pipeline de vectorización — LexIA
1

Ingesta

Documentos PDF, XML CENDOJ, DOCX y HTML (BOE)

parser: PyMuPDF
idioma: detect_langid()
encoding: UTF-8 forzado
2

Limpieza

Elimina cabeceras, pies de página, numeración y ruido OCR

strip: headers/footers
regex: ref. arts.
normalizer: unicodedata
3

Chunking

Por secciones semánticas (FJ, RA, Fallo) + fallback tokens

strategy: semantic
chunk_size: 800 tk
overlap: 120 tk
4

Enriquecimiento

Añade metadatos: tribunal, fecha, área, ECLI, número

metadata: 7 campos
source: CENDOJ JSON
hash: SHA256
5

Embedding

voyage-law-2 (ES) o m-e5-large (CA/multilingüe)

batch: 32 chunks
dims: 1024
normalize: True
6

Vector Store

pgvector + HNSW index; caché Redis para queries repetidas

index: HNSW m=16
metric: cosine
cache TTL: 1h

Estrategia de chunking para documentos legales

Los documentos legales tienen estructura predecible: Antecedentes → Fundamentos Jurídicos (FJ 1, FJ 2…) → Fallo. Se usa chunking semántico por sección; si la sección supera 800 tokens, se subdivide con solapamiento de 120 tokens.

Ejemplo: STS 412/2026 (cláusulas abusivas) — distribución de chunks
Ant.
FJ 1-2
chunk A
FJ 3-4
chunk B
FJ 5
Fallo
Solapamiento 120 tokens entre chunks FJ contiguos para no perder citas cruzadas
chunk_size
800 tokens
≈ 600 palabras español
overlap
120 tokens
15% — preserva contexto

Estimación de costes mensuales

Indexación inicial (80K docs × avg 4 chunks) ~47 €
Re-indexación semanal (800 docs nuevos) ~4 €/sem · 16 €/mes
Embedding queries (2K/día × 30 días) ~3 €/mes
m-e5-large (local, catalán) — AWS EC2 t3.medium ~28 €/mes
pgvector RDS (db.t3.medium, 100GB) ~55 €/mes
Redis caché (ElastiCache t3.micro) ~18 €/mes
Total operativo mes (sin indexación inicial) ~120 €/mes
✓ Dentro del presupuesto de 200 €/mes definido por LexIA
Pipeline de implementación — fragmento clave
python # lexia_embeddings.py — Pipeline de vectorización para LexIA from langchain_voyageai import VoyageAIEmbeddings from sentence_transformers import SentenceTransformer from dataclasses import dataclass import langid, re, hashlib class LexIAEmbeddingRouter: """Enruta documentos legales al modelo correcto según idioma.""" def __init__(self): # Modelo primario: entrenado en corpus jurídico self.legal_model = VoyageAIEmbeddings(model="voyage-law-2") # Modelo secundario: catalán / documentos multilingüe self.multi_model = SentenceTransformer("intfloat/multilingual-e5-large") def embed(self, chunks: list[str], lang: str = "auto") -> list[list[float]]: if lang == "auto": lang, _ = langid.classify(" ".join(chunks[:3])) if lang == "ca": # catalán → modelo local passages = [f"passage: {c}" for c in chunks] return self.multi_model.encode(passages, normalize_embeddings=True).tolist() return self.legal_model.embed_documents(chunks) def chunk_legal_doc(self, text: str) -> list[str]: # Chunking semántico por secciones FJ + fallback por tokens sections = re.split(r'(FUNDAMENTO[S]?\s+JURÍDICO[S]?\s+\d+|ANTECEDENTES|FALLO)', text) chunks, buf = [], [] for s in sections: tokens = len(s.split()) if tokens + len(" ".join(buf).split()) > 800: if buf: chunks.append(" ".join(buf)) buf = s.split()[-120:] # overlap 120 tokens else: buf += s.split() if buf: chunks.append(" ".join(buf)) return chunks # Uso: router = LexIAEmbeddingRouter() chunks = router.chunk_legal_doc(sentencia_text) vectors = router.embed(chunks, lang="auto") # → upsert a pgvector con metadatos: ecli, tribunal, fecha, area_derecho
Reglas de implementación — LexIA

Hacer

Usar voyage-law-2 para todo el corpus español — rendimiento superior en terminología jurídica y citas de artículos
Añadir siempre los metadatos: ecli, tribunal, fecha, area, jurisdiccion — imprescindibles para filtrado pgvector
Cachear embeddings de query en Redis (TTL 1h): las búsquedas de abogados se repiten mucho dentro de un mismo caso
Respetar los límites de ventana (32K tokens): dividir sentencias largas por FJ, no truncar
Normalizar embeddings a norma unitaria antes de insertar en pgvector (cosine similarity requiere vectores normalizados)
Detectar idioma automáticamente con langid y enrutar catalán a multilingual-e5-large local

No hacer

No mezclar vectores de voyage-law-2 y m-e5-large en el mismo índice — espacios vectoriales incompatibles; usar columnas separadas en pgvector
No truncar automáticamente chunks que superen el límite: reorganizar la división antes de llamar a la API
No enviar PDFs sin limpiar: el ruido OCR de los PDFs del CENDOJ degrada severamente la calidad del embedding
No usar all-MiniLM-L6-v2 (384 dims, 256 tokens) — ventana insuficiente para párrafos jurídicos, sin soporte español
No re-indexar documentos ya procesados sin verificar el hash SHA-256 del contenido — costoso e innecesario
No ignorar el solapamiento (overlap): sin él, preguntas que cruzan dos FJ contiguos pierden contexto y bajan el recall