Files
graph_explorer/issues/0042-gliner2-unified-extractor.md
T
egutierrez 5e6023f639 docs: issues 0041 (split thresholds) + 0042 (GLiNER2), supersedes mREBEL
Cierra el ciclo del analysis gliner_glirel_tuning: documenta en app.md el
pipeline NER+RE disponible en el registry y abre los dos issues que faltan
para cablearlo en extract_graph_hybrid + panel paste_extract. Archiva el
0042 original (mREBEL) tras la decision a favor de GLiNER2 (Apache 2.0,
joint NER+RE, 20-30x mas rapido en CPU).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:07:43 +02:00

192 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: 0042
title: GLiNER2 — extractor unificado NER+RE (Apache 2.0) sustituye GLiREL en extract_graph_hybrid
status: pending
priority: high
created: 2026-05-04
parent: 0013
depends_on: [0041]
supersedes: 0042-mrebel-relation-extractor (archivado, ver decisions/2026-05-04-gliner2-over-mrebel.md)
---
## Contexto
El issue 0042 inicial (sustituir GLiREL por mREBEL) queda superado por el descubrimiento de **GLiNER2** (`fastino/gliner2-large-v1`):
- **Apache 2.0** — sin restriccion comercial.
- **NER + RE en un solo modelo** — un solo forward pass.
- **20-30× mas rapido** que mREBEL en CPU.
- **Funciona en OSINT castellano** — captura IPs, dominios defanged, CVEs, malware.
Datos en `vaults/osint_nlp_models/models/gliner2.md`, decision en `vaults/osint_nlp_models/decisions/2026-05-04-gliner2-over-mrebel.md`, validacion empirica en `analysis/gliner_glirel_tuning/notebooks/04_gliner2_winner.ipynb`.
## Objetivo
Anadir GLiNER2 como motor principal de NER+RE en el pipeline `extract_graph_hybrid`. GLiREL queda como engine secundario en el panel `paste_extract`. mREBEL queda disponible via las funciones del registry pero no por defecto.
## Diseño
### 1. Funciones nuevas en el registry
```
python/functions/datascience/
├── gliner2_load_model.py # carga + cache GLiNER2 (Apache 2.0)
└── extract_graph_gliner2.py # ejecuta schema entity+relation, normaliza a EntityCandidate/RelationCandidate
```
Plantilla `gliner2_load_model.py`:
```python
"""Carga (y cachea) un modelo GLiNER2 que hace NER+RE conjunto.
LICENCIA: Apache 2.0 — uso libre incluido comercial.
"""
from typing import Any
_MODEL_CACHE: dict[tuple[str, str], Any] = {}
def gliner2_load_model(
model_name: str = "fastino/gliner2-large-v1",
device: str = "auto",
) -> Any:
"""Returns GLiNER2 instance ready for .extract() with schemas."""
resolved = _resolve_device(device)
key = (model_name, resolved)
if key in _MODEL_CACHE: return _MODEL_CACHE[key]
from gliner2 import GLiNER2
m = GLiNER2.from_pretrained(model_name)
if hasattr(m, "to"): m.to(resolved)
_MODEL_CACHE[key] = m
return m
```
Plantilla `extract_graph_gliner2.py`:
```python
def extract_graph_gliner2(
text: str,
entity_labels: list[str],
relation_labels: list[str],
model,
chunk_by_sentences: bool = False,
) -> tuple[list, list]:
"""Returns (entities, relations) as list[EntityCandidate], list[RelationCandidate].
If chunk_by_sentences=True, splits text by sentence and aggregates.
Recommended for texts > 20 sentences (recall improves).
"""
```
### 2. Cambio en `extract_graph_hybrid.py`
Anadir parametro `relation_engine` con cuatro valores:
```python
def extract_graph_hybrid(
chunks, entity_schema, relation_types,
relation_engine: str = "gliner2", # 'gliner2' | 'glirel' | 'mrebel' | 'none'
gliner_model=None, # capa 1 NER (legacy stack)
glirel_model=None, # capa 4 RE (legacy)
mrebel_models=None, # tuple (tok, mdl) si engine=mrebel
gliner2_model=None, # NUEVO — sustituye gliner+glirel
llm_chat_json=None,
entity_threshold=0.5,
relation_threshold=0.15,
chunk_by_sentences=False,
):
if relation_engine == "gliner2":
ents, rels = extract_graph_gliner2(text=..., entity_labels=..., relation_labels=...,
model=gliner2_model, chunk_by_sentences=chunk_by_sentences)
# NO se llama a GLiNER ni GLiREL, GLiNER2 hace ambos.
elif relation_engine == "glirel":
# legacy: GLiNER + GLiREL como antes
...
elif relation_engine == "mrebel":
# GLiNER + mREBEL via las funciones del registry (extract_relations_mrebel)
...
elif relation_engine == "none":
rels = []
```
### 3. `enrichers/paste_extract/run.py`
Manifest:
```yaml
params:
text: { type: string, required: true }
use_hybrid: { type: bool, default: true }
relation_engine: { type: enum, values: [gliner2, glirel, mrebel, none], default: gliner2 }
entity_threshold: { type: float, default: 0.5 }
relation_threshold: { type: float, default: 0.15 }
chunk_by_sentences: { type: bool, default: false }
```
### 4. Panel C++ (`extract_panel.cpp`)
- Combo: `Engine: [GLiNER2 (recomendado) | GLiNER+GLiREL (rapido EN) | GLiNER+mREBEL (no comercial) | None]`
- Banner amarillo solo si `mREBEL` seleccionado (recordatorio de licencia CC BY-NC-SA).
- Checkbox `chunk_by_sentences` (recomendado para texto largo).
- Sliders del issue 0041 (entity_threshold, relation_threshold).
### 5. `app.md`
```yaml
notes: |
Motor de extraccion por defecto: GLiNER2 (Apache 2.0, NER+RE conjunto, ES/EN/FR).
Engines alternativos: GLiNER+GLiREL (rapido EN), GLiNER+mREBEL (no comercial,
18 idiomas). Ver vaults/osint_nlp_models/ para benchmarks.
dependencies:
- "gliner2 >= 1.3.0 (Apache 2.0)"
optional_dependencies:
- "gliner + glirel (Apache 2.0, legacy)"
- "transformers + sentencepiece (mREBEL/REBEL — non-commercial mREBEL)"
```
### 6. Tests pytest
`tests/test_extract_graph_gliner2.py`:
- Test smoke con stub de modelo (mock que retorna shape oficial).
- Test que normaliza output a `EntityCandidate` / `RelationCandidate` correctamente.
- Test que `chunk_by_sentences=True` agrega correctamente.
`tests/test_paste_extract.py`: anadir test del path `relation_engine=gliner2` (default).
## Definicion de hecho
- Pego el corpus es_corporate_short, dejo engine=GLiNER2, pulso Extract.
- Tras ~1.5s veo 14 entidades y 8 relaciones, mayoria correctas.
- Cambio engine a GLiREL → resultado pobre conocido (validacion contraria).
- Cambio engine a mREBEL → veo banner de licencia + 5 relaciones lentas pero limpias.
- Pego corpus es_osint con label set OSINT → veo IPs, CVEs, malware, hashes correctamente.
- Apply solo guarda las marcadas; dedupe sigue funcionando.
- Tests verdes en WSL.
## Latencia / UX
- GLiNER2: ~1-5s para texto medio (4-30 frases). UX **fluida**, no necesita progress bar.
- GLiNER2 chunked: ~150ms/frase. Para 30 frases = 4.5s, sigue fluido.
- mREBEL queda con barra de progreso obligatoria (issue separado dentro de este).
## Out of scope (issues separados)
- Threshold tuning interno de GLiNER2 (si la API lo expone).
- Mapeo de tipos de entidad GLiNER2 a vocabulario controlado custom (ej. `persona``Person`).
- Extension a Frances (declarado pero sin probar).
- Comparativa con `gliner2-base-v1` (mas pequeño) — issue de optimizacion.
## Riesgos
- **Recall bajo en texto largo** sin chunking. Mitigacion: `chunk_by_sentences=True` por defecto si texto > 20 frases.
- **Errores semanticos puntuales** (e.g. `Inditex acquired Pablo Isla`). El panel los muestra con checkbox — el humano filtra antes de Apply.
- **Spans sucios ocasionales** en OSINT. Mitigacion: aplicar `align_relations_to_entities_py_datascience` (funcion ya en el registry) post-extract para descartar relaciones cuyas puntas no son entidades reales.
## Relacionado
- `vaults/osint_nlp_models/models/gliner2.md`
- `vaults/osint_nlp_models/decisions/2026-05-04-gliner2-over-mrebel.md`
- `analysis/gliner_glirel_tuning/notebooks/04_gliner2_winner.ipynb`
- Issue 0041 (split confidence_threshold) — sigue valido.
- Issue 0042-mrebel-relation-extractor.md.superseded (archivado).