# ARCHITECTURE_RAG.md — Medical Hybrid RAG System
> v2.0 — Documento único (fusión de GUIDE + ARCHITECTURE)

---

## 1. ¿Qué es este proyecto?

Un **sistema de investigación médica** que convierte miles de TACs y rayos X en un espacio consultable por IA.

```
Tú escribes:  "Muéstrame TACs de pulmón con nódulos en cortes de 1.25mm"

El sistema:   1. Extrae "pulmón", "nódulos", "1.25mm" (SpaCy NER)
              2. Busca por keyword exacto: "1.25mm" (BM25)
              3. Busca por significado: "thin slice lung nodule" (Vector Search)
              4. Combina ambos rankings (RRF Fusion)
              5. Re-evalúa la relevancia (Cross-Encoder)
              6. LLaMA te explica qué encontró
```

### ¿Qué NO es?
- NO diagnostica cáncer ni enfermedades
- NO sustituye radiólogos
- NO es un producto médico regulado

### ¿Para qué sirve?
- Demostrar construcción de **pipelines ML de producción** sobre datos reales
- Investigar patrones en imágenes médicas a gran escala
- Prototipo funcional de IA médica con búsqueda híbrida + LLM local

---

## 2. ¿Por qué estas herramientas y no otras?

### ¿Por qué HuggingFace?

HuggingFace es la **librería #1 del ecosistema ML open-source**. Es el "npm de la IA".

| Sin HuggingFace | Con HuggingFace |
|---|---|
| Descargar pesos manualmente de papers | `SentenceTransformer("all-MiniLM-L6-v2")` — 1 línea |
| Escribir tokenizadores desde cero | Todo incluido: tokenizer + modelo + inference |
| Reentrenar modelos (semanas, GPU) | Miles de modelos pre-entrenados listos para usar |
| Pagar APIs cloud (OpenAI, etc.) | Modelos funcionan 100% local, sin internet |

**En nuestro sistema, HuggingFace provee:**

1. **Embeddings de texto** — `sentence-transformers/all-MiniLM-L6-v2`
   - Convierte texto en vectores de 384 dimensiones
   - "CT THORAX GE 1.25mm" → `[0.23, -0.11, 0.87, ...]`
   - Permite buscar por *significado*, no solo por palabras
   
2. **Reranker** — `cross-encoder/ms-marco-MiniLM-L-6-v2`  
   - Re-evalúa los resultados de búsqueda para mayor precisión
   - Mejora la calidad un 10-20% sobre la búsqueda base

3. **Embeddings de imagen** — `BiomedCLIP` (Microsoft via HuggingFace)
   - Modelo: `microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224`
   - Entrenado con 15M pares imagen-texto de PubMed (vs CLIP genérico: fotos de internet)
   - Convierte cada imagen TAC en un vector de 512 dimensiones
   - Significativamente mejor para imágenes médicas que CLIP genérico

**¿Por qué no OpenAI/GPT directo?**
- Coste: GPT-4 cuesta $30/1M tokens. Nuestros modelos: **$0**
- Privacidad: datos médicos NO deben salir del servidor
- Velocidad: una query local tarda ~200ms, API cloud ~2-5s
- Control: podemos cambiar/fine-tunear cualquier componente

**¿Por qué no solo FAISS/Elasticsearch?**
- FAISS/Elasticsearch son *solo búsqueda*. No entienden lenguaje natural
- Nosotros combinamos búsqueda exacta (BM25) + semántica (Vector) + razonamiento (LLM)

---

## 3. Componentes — Qué hace cada uno y POR QUÉ

### 3.1 BM25 (Best Match 25)

**Qué es:** Algoritmo de búsqueda por palabras clave. Lo usan Elasticsearch y Google.

**¿Por qué lo necesitamos?** Vector Search NO funciona bien con términos exactos:
- `"NER1006"` → vector search devuelve IDs parecidos pero incorrectos
- `"SIEMENS"` → vector confunde fabricantes
- `"1.25mm"` → vector no entiende valores numéricos exactos
- BM25 encuentra **EXACTAMENTE** esos términos ✅

**Cómo funciona (simplificado):**
```
BM25 = cuánto de raro es el término × cuántas veces aparece

Si buscas "SIEMENS" y solo 30 de 5,000 documentos lo tienen → muy relevante
Si buscas "CT" y 4,500 lo tienen → poco informativo
```

**Archivo:** `rag/hybrid_search.py` → clase `BM25Index`

---

### 3.2 Vector Search (Búsqueda Semántica)

**Qué es:** Buscar por *significado*, no por palabras exactas.

**¿Por qué lo necesitamos?** BM25 NO entiende sinónimos ni contexto:
- `"thin slices"` no matchea `"1.25mm"` en BM25, pero **vector sabe que son lo mismo**
- `"lung cancer scans"` matchea con `"chest CT"` por significado
- `"high resolution"` conecta con `"fine reconstruction"`

**Cómo funciona (simplificado):**
```
1. Tu pregunta → modelo MiniLM → vector de 384 números
2. Cada documento ya tiene su vector pre-calculado
3. Se calcula la "distancia" entre tu pregunta y cada documento
4. Los más cercanos = los más relevantes
```

> **Analogía:** Imagina que cada documento es un punto en un mapa de 384 dimensiones. Tu pregunta también es un punto. Buscamos los puntos más cercanos.

**Modelo:** `sentence-transformers/all-MiniLM-L6-v2` (HuggingFace)
**Archivo:** `rag/hybrid_search.py` → clase `VectorIndex`

---

### 3.3 RRF (Reciprocal Rank Fusion)

**Qué es:** Combinar los resultados de BM25 y Vector Search en un solo ranking.

**¿Por qué no sumar los scores directamente?**
- BM25 devuelve scores de 0 a 30+
- Vector devuelve scores de 0 a 1
- Si sumas: BM25 siempre gana por tener números más altos → injusto

**RRF lo resuelve usando posiciones, no scores:**
```
score(doc) = 1/(60 + posición_en_BM25) + 1/(60 + posición_en_Vector)

Doc A: BM25 posición 1, Vector posición 5 → 0.0164 + 0.0154 = 0.0318
Doc B: BM25 posición 3, Vector posición 1 → 0.0159 + 0.0164 = 0.0323 ← GANA
```

**Resultado:** Documentos que aparecen bien en AMBOS métodos suben al top.

**Archivo:** `rag/hybrid_search.py` → función `reciprocal_rank_fusion()`

---

### 3.4 Cross-Encoder Reranker

**Qué es:** Un modelo que re-evalúa los resultados con mucha más precisión.

**¿Por qué no usarlo para todo?**
- Es **5-10x más lento** que BM25/Vector
- Pero es **10-20% más preciso**
- Solución: usarlo SOLO en los Top 20 resultados → devuelve Top 10 refinado

**Diferencia clave con Vector Search:**
```
Vector (bi-encoder):    codifica pregunta y documento POR SEPARADO → rápido
Cross-Encoder:          codifica pregunta Y documento JUNTOS → preciso
```

**Modelo:** `cross-encoder/ms-marco-MiniLM-L-6-v2` (HuggingFace)
**Archivo:** `rag/hybrid_search.py` → función `rerank()`

---

### 3.5 SpaCy + NLTK (Preprocesado NLP)

**SpaCy** — procesamiento de lenguaje natural industrial:
- NER: extrae entidades (`"chest"` → body_part, `"SIEMENS"` → manufacturer)
- Chunking semántico: agrupa palabras con significado

**NLTK** — herramientas lingüísticas clásicas:
- Tokenización de frases
- Eliminación de stopwords antes de BM25
- Stemming: `"consolidation"` → `"consolid"`

**Archivo:** `rag/preprocessor.py`

---

### 3.6 scispaCy (Medical NER — Allen AI)

**Qué es:** Extensión de SpaCy entrenada en texto biomédico. Detecta enfermedades, fármacos y entidades UMLS.

**¿Por qué no basta SpaCy genérico?**
- `en_core_web_sm`: `"pneumonia"` → no detectado ❌
- `en_ner_bc5cdr_md` (scispaCy): `"pneumonia"` → DISEASE ✅
- `"methotrexate"` → CHEMICAL ✅

**Modelo:** `en_ner_bc5cdr_md` (BioCreative V CDR corpus)

**Cómo se usa en el sistema:**
```
Query: "adenocarcinoma with methotrexate"  
→ scispaCy extrae: DISEASE[adenocarcinoma], CHEMICAL[methotrexate]
→ Estas entidades se usan para refinar la búsqueda (query expansion)
```

**Archivo:** `rag/preprocessor.py` → `extract_entities_scispacy()`

---

### 3.7 LLaVA (Visual Language Model)

**Qué es:** LLaVA (Large Language and Vision Assistant) es un VLM que **ve** imágenes y genera descripciones textuales.

**Diferencia clave con CLIP/BiomedCLIP:**
```
BiomedCLIP:  imagen → vector (512 números) → para BUSCAR similares
LLaVA:       imagen → texto  ("Axial CT, GGO bilateral...") → para ENTENDER
```

**¿Por qué ambos?**
- BiomedCLIP es rápido (vector en ~10ms) pero no explica nada
- LLaVA es lento (~5s por imagen) pero genera descripciones clínicas
- Juntos: buscas con BiomedCLIP, describes con LLaVA

**Cómo funciona en nuestro sistema:**
```
1. Usuario busca "lung nodule 1.25mm"
2. BiomedCLIP + BM25 → encuentran las 5 imágenes más relevantes
3. LLaVA (via Ollama) → describe cada imagen:
   "Axial CT slice in lung window showing a 6mm solid
    nodule in the right lower lobe. No calcification."
4. LLaMA → sintetiza y explica al usuario
```

**Archivo:** `rag/vlm_caption_ollama.py` (llama via `ollama run llava`)
**Archivo:** `rag/caption_one.py` (caption individual)
**Archivo:** `rag/caption_batch.py` (caption en lote)

---

### 3.8 LangGraph (Orquestación Agentic)

**Qué es:** Define el "cerebro" del sistema — qué hacer en cada paso.

**¿Por qué no un RAG simple (buscar → generar)?**
Un RAG simple es frágil:
- ¿Si no encuentra nada? → el usuario no sabe por qué
- ¿Si los resultados son malos? → genera respuestas inventadas
- ¿Debería usar BM25 o Vector? → siempre usa el mismo

**LangGraph permite que el agente DECIDA:**
```
START → clasificar query → buscar → evaluar resultados
                                          ↓
                               ¿suficientes?
                              /              \
                           SÍ                 NO
                            ↓                  ↓
                      generar respuesta    refinar query → buscar otra vez
                            ↓                  (máximo 2 intentos)
                           END
```

**Archivo:** `graph/workflow.py`

---

### 3.7 Ollama + LLaMA3 (LLM Local)

**Qué es:** LLaMA3 es un modelo de lenguaje de Meta, ejecutado localmente via Ollama.

**¿Por qué local y no ChatGPT?**
- **Privacidad:** datos médicos no salen del servidor
- **Coste:** $0 (vs ~$30/1M tokens con GPT-4)
- **Sin internet:** funciona offline
- **Sin límites:** sin rate limits ni APIs caídas

**Archivo:** `rag/ask_ollama.py`

---

## 4. Arquitectura Visual

```
┌─────────────────────────────────────────────────────────────────┐
│                    INGESTION PIPELINE (una vez)                  │
│                                                                 │
│  DICOM (.dcm)──► PNG (slices)──► BiomedCLIP ──► image_emb       │
│       │                                                         │
│       └──► Metadata JSONL ──► MiniLM-L6-v2 ──► text_emb        │
│                                        │                        │
│                              join_image_text.py                  │
│                                        │                        │
│                                  pairs.jsonl                     │
│                          (corazón del sistema)                   │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                     QUERY PIPELINE (en vivo)                     │
│                                                                 │
│  Pregunta ──► Preprocessor (SpaCy/NLTK)                         │
│       │              │                                          │
│       │         NER + tokenize + expand                         │
│       │              │                                          │
│       ├──► BM25 Search ──────────────┐                          │
│       │   (keyword, exacto)         │                          │
│       │                              │ RRF Fusion               │
│       └──► Vector Search ────────────┘                          │
│            (semántico, significado)  │                          │
│                                      ▼                          │
│                              Cross-Encoder Reranker             │
│                              (HuggingFace)                      │
│                                      │                          │
│                                      ▼                          │
│                         ┌── VLM (LLaVA) ── captions ──┐         │
│                         │                              │         │
│                         ▼                              ▼         │
│                     LLM (Ollama/LLaMA3)                         │
│                         │                                        │
│                   Respuesta + Fuentes + Descripciones             │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                   AGENTIC LAYER (LangGraph)                     │
│                                                                 │
│  START → classify_query → hybrid_search → evaluate_results      │
│                                                │                │
│                                  ┌─────────────┤               │
│                                  ▼             ▼                │
│                             need_more     sufficient             │
│                                  │             │                │
│                                  ▼             ▼                │
│                           refine_query  generate_answer → END   │
│                                  │                              │
│                                  └──► hybrid_search (loop ×2)   │
└─────────────────────────────────────────────────────────────────┘
```

---

## 5. Archivos del Proyecto

```
agentic-rag/
├── ARCHITECTURE_RAG.md     ← ESTE ARCHIVO
├── app.py                  ← Streamlit demo interactiva (puerto 8501)
├── main.py                 ← Entry point pipeline
├── run_pipeline.sh         ← Pipeline runner
├── requirements.txt        ← Dependencias
│
├── rag/                    ← QUERY PIPELINE
│   ├── hybrid_search.py    ← BM25 + Vector + RRF + Reranker
│   ├── preprocessor.py     ← SpaCy NER + NLTK tokenization
│   ├── retrieve.py         ← Vector retrieval
│   ├── ask_ollama.py       ← LLM query (Ollama)
│   └── caption_*.py        ← VLM captioning (futuro)
│
├── graph/                  ← AGENTIC LAYER
│   └── workflow.py         ← LangGraph multi-step agent
│
├── pipelines/              ← INGESTION PIPELINE
│   ├── dicom/              ← DICOM → PNG + metadata
│   ├── embeddings/         ← CLIP + MiniLM embeddings
│   └── ingest/             ← Join image ↔ text
│
├── scripts/                ← Utilities
│   ├── download_datasets.py
│   └── pseudo_label.py
│
└── data/                   ← DATA (no en git)
    ├── images/             ← PNG convertidas
    ├── metadata/           ← pairs.jsonl, metadata.jsonl
    └── embeddings/         ← .npz vectors
```

---

## 6. Datasets Médicos

### Activos en TSC (`/data/datasets/`)

| Dataset | Colección | Modalidad | Tamaño | Estado |
|---|---|---|---|---|
| **CMB-LCA** | TCIA | CT Lung | ~14 GB | ⬇️ Descargando |
| **COVID-19-AR** | `COVID-19-AR` (TCIA) | CT Chest | 19 GB | ✅ Completo |
| **COVID-19-NY-SBU** | `COVID-19-NY-SBU` (TCIA) | CT Chest | 38 GB | ⬇️ Descargando |
| **LIDC-IDRI** | `LIDC-IDRI` (TCIA) | CT Lung | 16 GB (parcial) | ⬇️ En cola |

### Para añadir (futuro)

| Dataset | Modalidad | Tamaño | Para qué |
|---|---|---|---|
| **BraTS 2023** | MRI Brain | ~30 GB | Segmentación tumores cerebrales |
| **ChestX-ray14** | X-ray Chest | ~45 GB | 112k X-rays, 14 patologías |
| **PadChest** | X-ray Chest | ~50 GB | Hospital español, informes en español |

### Cómo descargar

```bash
# TCIA datasets (via Python)
source /data/envs/ai/bin/activate
python3 -c "
from tcia_utils import nbia
series = nbia.getSeries(collection='NOMBRE_COLECCION')
nbia.downloadSeries(series, path='/data/datasets/NOMBRE', number=0)
"

# Monitorizar
tail -f /data/download_tcia.log
du -sh /data/datasets/*/
```

---

## 7. Setup Completo (desde cero)

### 7.1 Instalar en local (Mac)

```bash
cd ~/Desktop/SW_AI/rag-medical/agentic-rag

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m spacy download en_core_web_sm

# Verificar
python -c "from rank_bm25 import BM25Okapi; print('✅ BM25 OK')"
python -c "from sentence_transformers import SentenceTransformer; print('✅ SentenceTransformer OK')"
python -c "from sentence_transformers import CrossEncoder; print('✅ CrossEncoder OK')"
python -c "from langgraph.graph import StateGraph; print('✅ LangGraph OK')"
```

### 7.2 Instalar en TSC (servidor)

```bash
ssh tsc

# Ollama (binary en /, modelos en /data)
curl -fsSL https://ollama.com/install.sh | sh
mkdir -p /data/ollama/models
echo 'export OLLAMA_MODELS=/data/ollama/models' >> ~/.bashrc
OLLAMA_MODELS=/data/ollama/models ollama pull llama3

# Verificar
OLLAMA_MODELS=/data/ollama/models ollama list
```

### 7.3 Pipeline de ingesta (por cada dataset)

```bash
cd /data/rag-medical && source /data/envs/ai/bin/activate

# Paso 1: DICOM → PNG
python pipelines/dicom/convert_dcm2png.py \
  --in_dir /data/datasets/COVID-19-AR \
  --out_dir data/images/COVID-19-AR \
  --only_chest --skip_existing_series

# Paso 2: Extraer metadata
python -c "
from pipelines.dicom.extract_metadata import extract_metadata
extract_metadata(
    in_dir='/data/datasets/COVID-19-AR',
    out_file='data/metadata/covid_ar_metadata.jsonl',
    only_chest=False, modality='CT'
)"

# Paso 3: Text embeddings
python pipelines/embeddings/text_embed.py \
  --metadata data/metadata/covid_ar_metadata.jsonl \
  --out_embeddings data/embeddings/text_embeddings.npz \
  --out_index data/embeddings/text_index.jsonl \
  --batch_size 64 --device cpu

# Paso 4: Generar pairs.jsonl
python pipelines/ingest/join_image_text.py

# Paso 5: Lanzar app
streamlit run app.py --server.port 8501 --server.address 0.0.0.0
```

---

## 8. Variables de Entorno

```env
# LLM
OLLAMA_MODEL=llama3:latest
OLLAMA_MODELS=/data/ollama/models

# Embeddings (HuggingFace)
TEXT_MODEL=sentence-transformers/all-MiniLM-L6-v2
IMAGE_MODEL=hf-hub:microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224
RERANKER_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2

# TSC Server
DATA_SERVER_HOST=100.104.65.25
DATA_SERVER_DATASETS=/data/datasets
DATA_SERVER_MODELS=/data/models

# HuggingFace cache
HF_HOME=/data/huggingface
TRANSFORMERS_CACHE=/data/models

# Data paths
PAIRS_PATH=data/metadata/pairs.jsonl
TEXT_EMB_PATH=data/embeddings/text_embeddings.npz

# Kaggle
KAGGLE_USERNAME=alebronlobo81
```

---

## 9. Probar el sistema

### Hybrid Search (terminal)

```bash
# BM25 — keywords exactos
python -m rag.hybrid_search --query "SIEMENS 1.25mm" --k 5 --mode bm25

# Vector — significado semántico
python -m rag.hybrid_search --query "lung scans thin slices" --k 5 --mode vector

# Hybrid — ambos combinados (recomendado)
python -m rag.hybrid_search --query "chest CT SIEMENS 1.25mm" --k 10 --mode hybrid --rerank
```

### Agente LangGraph

```bash
python -m graph.workflow --question "Compare GE vs SIEMENS scan protocols"
python -m graph.workflow --question "What lung window protocols are most common?"
```

### Streamlit (demo web)

```bash
# Lanzar (o ya corriendo en tsc:8501)
streamlit run app.py --server.port 8501 --server.address 0.0.0.0

# Acceder: http://100.104.65.25:8501
```

---

## 10. Troubleshooting

```bash
# Ollama no arranca
OLLAMA_MODELS=/data/ollama/models ollama serve &

# Streamlit no arranca
kill $(pgrep -f streamlit)
/data/envs/ai/bin/streamlit run app.py --server.port 8501 --server.address 0.0.0.0

# Ver estado de descargas
tail -f /data/download_tcia.log
tail -f /data/download_cmb_lca.log
du -sh /data/datasets/*/
df -h /data

# Verificar procesos
ps aux | grep -E "download|streamlit|ollama" | grep -v grep

# Reiniciar todo
kill $(pgrep -f "download_tcia|download_cmb|streamlit")
```

---

## 11. RWE / Ejemplos Prácticos — Cómo Usar el Sistema

### 11.1 ¿Qué es la interfaz? (http://100.104.65.25:8501)

Es una **aplicación web** (Streamlit) que te permite buscar entre miles de imágenes médicas escribiendo preguntas en lenguaje natural. Funciona en el navegador — no necesitas instalar nada.

### 11.2 Los controles explicados

Cuando abres la interfaz ves 4 controles:

| Control | Qué es | Para qué |
|---|---|---|
| **🔍 Search query** | El campo de texto | Escribes tu pregunta aquí |
| **Mode** | `hybrid` / `bm25` / `vector` | Elige el método de búsqueda (ver abajo) |
| **Results (K)** | Número de 3 a 20 | Cuántos resultados quieres ver |
| **Use Reranker** | Checkbox | Activa el re-evaluador de precisión |

#### ¿Qué es la K?

**K = número de resultados que quieres.** Si pones K=10, te devuelve los 10 documentos más relevantes. Si pones K=3, solo los 3 mejores.

- K bajo (3-5): cuando buscas algo muy específico
- K alto (10-20): cuando quieres explorar o comparar

#### ¿Qué es el Reranker?

El Reranker es un **segundo filtro de precisión**. Funciona así:

```
Sin Reranker:  BM25/Vector encuentran los mejores 10 → te los muestra
Con Reranker:  BM25/Vector encuentran los mejores 20 → el Reranker los re-evalúa 
               con más profundidad → te muestra los 10 realmente mejores
```

**¿Cuándo activarlo?** Cuando quieras resultados MÁS PRECISOS (tarda un poco más, ~1-2 segundos extra).

### 11.3 BM25 vs Vector vs Hybrid — ¿Cuál uso?

Esto es CLAVE. Los 3 modos buscan de formas completamente distintas:

#### 🟡 BM25 (keywords)

**Busca por PALABRAS EXACTAS.** Como Google de los 2000 — si la palabra está en el documento, lo encuentra.

```
✅ Bueno para:   "SIEMENS 1.25mm"        → encuentra exacto
✅ Bueno para:   "NER1006"               → encuentra el ID exacto  
✅ Bueno para:   "GE MEDICAL SYSTEMS"    → match literal

❌ Malo para:    "thin slices"           → NO encuentra "1.25mm" 
❌ Malo para:    "lung cancer scans"     → NO conecta con "chest CT"
```

#### 🟣 Vector (semántico)

**Busca por SIGNIFICADO.** Entiende que "thin slices" y "1.25mm" son lo mismo, aunque las palabras sean diferentes.

```
✅ Bueno para:   "thin slices"           → encuentra "1.25mm"
✅ Bueno para:   "lung cancer scans"     → conecta con "chest CT"
✅ Bueno para:   "compare protocols"     → entiende el concepto

❌ Malo para:    "NER1006"               → devuelve IDs parecidos pero incorrectos
❌ Malo para:    "SIEMENS"               → puede confundir fabricantes
```

#### 🟢 Hybrid (recomendado)

**Combina ambos.** Usa BM25 + Vector + RRF Fusion para obtener lo mejor de los dos.

```
✅ "SIEMENS 1.25mm lung scans"  → BM25 encuentra "SIEMENS" y "1.25mm" exacto
                                 → Vector entiende "lung scans" semánticamente
                                 → RRF los fusiona → resultados completos
```

> **REGLA SIMPLE:** Usa siempre **Hybrid**. Solo cambia a BM25/Vector si quieres entender QUÉ aporta cada uno.

### 11.4 La sección "Compare" (parte inferior de la interfaz)

En la parte de abajo de la página hay un **comparador de los 3 modos**. Escribes UNA query y ves los 3 resultados lado a lado:

```
🟡 BM25          🟣 VECTOR         🟢 HYBRID
#1 score=12.3    #1 score=0.89     #1 score=0.033
#2 score=8.7     #2 score=0.85     #2 score=0.031
...              ...               ...
```

Esto sirve para **entender y demostrar** por qué Hybrid es mejor. Por ejemplo:

| Query | BM25 | Vector | Hybrid |
|---|---|---|---|
| `NER1006 SIEMENS` | ✅ top 1 | ❌ mal | ✅ top 1 |
| `thin lung scans` | ❌ nada | ✅ top 1 | ✅ top 1 |
| `SIEMENS 1.25mm chest` | ⚠️ parcial | ⚠️ parcial | ✅ completo |

### 11.5 Preguntas que haría un equipo médico

#### Búsquedas clínicas

| Pregunta del médico | Qué escribir en la interfaz | Modo |
|---|---|---|
| "Quiero TACs de pulmón de cortes finos" | `lung CT thin slice 1.25mm` | Hybrid |
| "Busco estudios con contraste" | `CT contrast enhanced` | Hybrid |
| "¿Qué escáneres se usaron?" | `manufacturer SIEMENS GE` | BM25 |
| "Imágenes de nódulos pulmonares" | `lung nodule chest CT` | Hybrid + Reranker |
| "Estudios de COVID con ventana pulmonar" | `COVID lung window` | Hybrid |
| "Protocolos de screening de alta resolución" | `screening high resolution thin` | Vector |

#### Análisis de investigación

| Pregunta | Qué escribir | Qué te dice |
|---|---|---|
| "¿Qué grosor de corte domina?" | `slice thickness` | Distribución: 1.25mm vs 2.5mm vs 5mm |
| "¿Hay sesgo de fabricante?" | `manufacturer` | Proporción GE vs SIEMENS vs Philips |
| "¿Protocolos COVID vs cáncer?" | `COVID protocol` vs `cancer protocol` | Compara resultados entre queries |
| "¿Estudios con bajo kVp?" | `kvp 100` o `kvp 80` | Encuentra protocolos pediátricos/low-dose |

#### Cómo interpretar los resultados

Cada resultado muestra:

```
#1 | score: 0.0328 | doc: LIDC-IDRI-0240|1.3.6...|17
   CT THORAX W/CONTRAST | sex= age= | thickness=2.500000 | kvp=120 mfr=GE MEDICAL SYSTEMS
   📁 1.3.6.1.../1-017.dcm
```

- **score**: cuánto de relevante es (más alto = más relevante)
- **doc**: identificador (paciente | serie | slice número)
- **texto**: la metadata del DICOM (protocolo, grosor, fabricante, kVp...)
- **📁**: ubicación del archivo DICOM original

### 11.6 Estado de las descargas actuales

| Dataset | Descargado | Tamaño estimado | Restante |
|---|---|---|---|
| **COVID-19-AR** | 19 GB | ~19 GB | ✅ Completo |
| **COVID-19-NY-SBU** | 47 GB | ~50-60 GB | ~10 GB |
| **CMB-LCA** | 22 GB | ~30-40 GB | ~10-20 GB |
| **LIDC-IDRI** | 16 GB | ~124 GB | ~108 GB (el más grande) |

**Próximas descargas** (cuando terminen las actuales):
- **LIDC-IDRI XML Annotations** (~500 MB) — anotaciones de 4 radiólogos
- **MIDRC-RICORD** (~15 GB) — COVID con hallazgos anotados

---

## 12. Acrónimos

| Acrónimo | Significado |
|---|---|
| **RAG** | Retrieval-Augmented Generation — buscar contexto relevante + generar respuesta con LLM |
| **BM25** | Best Match 25 — algoritmo de búsqueda por palabras clave exactas |
| **RRF** | Reciprocal Rank Fusion — método para combinar rankings de distintos buscadores |
| **NER** | Named Entity Recognition — extraer entidades (nombres, códigos, anatomía) del texto |
| **CLIP** | Contrastive Language-Image Pre-training — modelo que conecta imágenes con texto |
| **LLM** | Large Language Model — modelo de lenguaje grande (GPT, LLaMA, etc.) |
| **VLM** | Visual Language Model — LLM que puede "ver" y describir imágenes |
| **DICOM** | Digital Imaging and Communications in Medicine — formato estándar de imagen médica |
| **TCIA** | The Cancer Imaging Archive — archivo público de imágenes de cáncer |
| **LIDC-IDRI** | Lung Image Database Consortium & Image Database Resource Initiative — 1,018 CTs de pulmón con nódulos anotados por 4 radiólogos. El gold standard en nódulos pulmonares |
| **MIDRC** | Medical Imaging and Data Resource Center — consorcio creado durante COVID-19 para centralizar datos de imagen médica |
| **RICORD** | RSNA International COVID-19 Open Radiology Database — base de datos de COVID anotada por radiólogos internacionales |
| **MIMIC-CXR** | Medical Information Mart for Intensive Care - Chest X-Ray — 377K radiografías + 227K informes clínicos (MIT/PhysioNet) |
| **CMB-LCA** | Cancer Moonshot Biobank - Lung Cancer Associated — dataset de cáncer de pulmón del programa Moonshot (NIH) |
| **NLP** | Natural Language Processing — procesamiento de lenguaje natural |
| **FAISS** | Facebook AI Similarity Search — librería de búsqueda vectorial rápida |
| **HF** | HuggingFace — plataforma #1 de modelos ML open-source |
| **CT** | Computed Tomography — tomografía computarizada (TAC en español) |
| **GGO** | Ground Glass Opacity — opacidad en vidrio esmerilado (hallazgo radiológico) |
| **kVp** | kilovoltage peak — voltaje del tubo de rayos X (afecta dosis/contraste) |
| **HU** | Hounsfield Units — unidad de densidad en CT (aire=-1000, hueso=+1000, agua=0) |
| **LoRA** | Low-Rank Adaptation — técnica para fine-tunear LLMs con poca GPU |
| **MONAI** | Medical Open Network for AI — framework de NVIDIA para deep learning médico |

---

## 13. Preguntas Clínicas — Qué Buscarían Oncólogos e Investigadores

### 13.1 Cómo interpretar un resultado

Cuando haces una búsqueda, cada resultado te muestra metadata del DICOM:

```
#1 | score: 0.3332
CT THORAX W/CONTRAST | | | sex= age= | thickness=2.500000 | kvp=120 mfr=GE MEDICAL SYSTEMS
```

**Qué significa cada campo:**

| Campo | Ejemplo | Qué indica |
|---|---|---|
| **Protocolo** | `CT THORAX W/CONTRAST` | TAC de tórax **con contraste** intravenoso. El contraste resalta vasos y tumores |
| **Series desc** | `AX LUNG WC` | Corte axial con ventana pulmonar — optimizado para ver nódulos en pulmón |
| **sex/age** | `sex=F age=061Y` | Mujer de 61 años — útil para análisis demográficos |
| **thickness** | `thickness=2.500000` | Grosor de corte: **2.5mm**. Los cortes finos (1.25mm) detectan nódulos más pequeños |
| **kvp** | `kvp=120` | 120 kilovoltios — estándar adulto. 100 kVp = protocolo low-dose (menos radiación) |
| **mfr** | `mfr=GE MEDICAL SYSTEMS` | Fabricante del escáner. Cada fabricante tiene protocolos diferentes |

### 13.2 Preguntas de oncólogos (con los findings explicados)

#### "Búscame TACs con nódulos sospechosos"
```
Query: lung nodule suspicious thick 1.25
Modo: Hybrid + Reranker

Finding esperado:
  CT THORAX W/CONTRAST | AX LUNG WC | thickness=1.250000 | mfr=GE
  
¿Por qué es relevante?
  - Corte de 1.25mm → detecta nódulos desde 3mm 
  - Con contraste → el tumor capta más contraste que el tejido sano
  - Ventana pulmonar (LUNG WC) → máxima visibilidad del parénquima
  - Relevancia: Un oncólogo usaría este protocolo para screening de cáncer
```

#### "¿Hay estudios de seguimiento (follow-up)?"
```
Query: chest CT follow up screening
Modo: Vector (busca concepto, no keyword exacto)

Finding esperado:
  CT CHEST WO CONTRAST | STANDARD | thickness=5.000000

¿Por qué es relevante?
  - Sin contraste (WO = without) → típico de seguimiento rutinario
  - 5mm thickness → menos resolución pero menos dosis (ALARA principle)
  - El oncólogo compararía este estudio con uno previo para medir crecimiento
```

#### "Compara protocolos de diferentes hospitales"
```
Query: thickness 1.25 2.5 GE SIEMENS protocol
Modo: BM25 (keywords exactos)

Relevancia para investigación:
  - ¿Los hospitales con GE usan cortes más finos que los de SIEMENS?
  - ¿Hay sesgo en la detección de nódulos por protocolo?
  - Publicable: "Impact of acquisition protocol on nodule detection rates"
```

#### "Estudios pediátricos con baja dosis"
```
Query: kvp 100 80 pediatric low dose
Modo: Hybrid

Finding esperado:
  CT CHEST | | thickness=2.500000 | kvp=100

¿Por qué kvp=100 importa?
  - 100 kVp vs 120 kVp = 30% menos dosis de radiación
  - Crítico en pacientes jóvenes/pediátricos
  - El sistema detectará automáticamente estudios low-dose
```

### 13.3 Preguntas de equipos de investigación

| Pregunta de investigación | Query | Qué descubriría |
|---|---|---|
| "¿Qué resolución detecta mejor nódulos?" | `thin slice nodule 1.25 detection` | Si hay correlación entre thickness y detección |
| "¿Hay diferencia entre fabricantes?" | `manufacturer GE SIEMENS protocol` | Sesgos en protocolos por fabricante |
| "¿COVID afecta los protocolos de pulmón?" | `COVID chest protocol` vs `cancer chest protocol` | Si COVID cambió las prácticas clínicas |
| "¿Cuántos estudios tienen metadata completa?" | `sex age thickness kvp` | Calidad del dataset para ML training |
| "¿Qué proporción es con/sin contraste?" | `contrast` vs `without contrast` | Distribución — afecta al entrenamiento de modelos |

---

## 14. Costes LLM — ¿Cuánto cuesta este sistema?

### 14.1 Nuestro stack (100% local)

| Componente | Modelo | Coste | Donde corre |
|---|---|---|---|
| **Embeddings** | MiniLM-L6-v2 | **$0 / €0** | CPU (tsc) |
| **BM25** | rank_bm25 (Python) | **$0 / €0** | CPU (tsc) |
| **Reranker** | Cross-Encoder MiniLM | **$0 / €0** | CPU (tsc) |
| **LLM** | LLaMA3 8B (Ollama) | **$0 / €0** | CPU (tsc, 16GB RAM) |
| **Image Emb** | CLIP ViT-B/32 | **$0 / €0** | CPU |
| **Streamlit** | Streamlit 1.55 | **$0 / €0** | tsc:8501 |
| **Servidor** | tsc (propio) | ~€10/mes elec. | On-premise |
| **TOTAL** | — | **~€10/mes** | — |

### 14.2 Si usáramos APIs cloud (coste estimado por 10,000 queries/mes)

| Componente | Modelo cloud | Coste/query | 10K queries/mes |
|---|---|---|---|
| **Embeddings** | OpenAI text-embedding-3-small | $0.00002/query | $0.20 / €0.18 |
| **Reranker** | Cohere Rerank | $0.001/query | $10 / €9.20 |
| **LLM (respuesta)** | GPT-4o | $0.01/query | $100 / €92 |
| **LLM (respuesta)** | GPT-4o-mini | $0.001/query | $10 / €9.20 |
| **LLM (respuesta)** | Claude 3.5 Sonnet | $0.015/query | $150 / €138 |
| **VLM (imagen)** | GPT-4 Vision | $0.03/imagen | $300 / €276 |
| **Hosting** | AWS/GCP VM (16GB) | — | $50/mes / €46 |

### 14.3 Comparativa total

| Escenario | Coste mensual | Coste anual |
|---|---|---|
| **🟢 Nuestro stack (local)** | **~€10** | **~€120** |
| **🟡 Cloud con GPT-4o-mini** | ~€70 | ~€840 |
| **🔴 Cloud con GPT-4o** | ~€200 | ~€2,400 |
| **🔴 Cloud con GPT-4 Vision** | ~€400 | ~€4,800 |

> **Conclusión:** Ejecutar todo local con Ollama + LLaMA3 es **20-40x más barato** que usar APIs cloud. Para datos médicos, también es OBLIGATORIO por privacidad (RGPD/HIPAA).

---

## 15. Further Work — Próximos Pasos e Investigación

### 15.1 Datasets adicionales para descargar

| Dataset | Modalidad | Tamaño | Link | Para qué |
|---|---|---|---|---|
| **LIDC-IDRI Annotations** | XML | ~500 MB | [TCIA](https://www.cancerimagingarchive.net/collection/lidc-idri/) | Contornos de nódulos por 4 radiólogos |
| **MIDRC-RICORD-1A** | CT Chest | ~15 GB | [TCIA](https://www.cancerimagingarchive.net/collection/midrc-ricord-1a/) | COVID anotado por radiólogos |
| **LNDb** | CT Lung | ~8 GB | [LNDb.org](https://lndb.grand-challenge.org) | 294 CTs con nódulos (alternativa europea) |
| **DeepLesion** | CT Multiorgan | ~55 GB | [NIH Box](https://nihcc.app.box.com/v/DeepLesion) | 32K lesiones anotadas (hígado, riñón, pulmón...) |
| **MIMIC-CXR Reports** | Texto | ~2 GB | [PhysioNet](https://physionet.org/content/mimic-cxr/) | 227K informes radiológicos (requiere cuenta) |
| **BraTS 2023** | MRI Brain | ~30 GB | [Synapse](https://www.synapse.org/brats2023) | Tumores cerebrales |
| **ChestX-ray14** | X-ray | ~45 GB | [NIH Box](https://nihcc.app.box.com/v/ChestXray-NIHCC) | 112K radiografías, 14 patologías | 
| **PadChest** | X-ray | ~50 GB | [BIMCV](https://bimcv.cipf.es/bimcv-projects/padchest/) | Hospital español, informes en español |
| **CheXpert** | X-ray | ~12 GB | [Stanford](https://stanfordmlgroup.github.io/competitions/chexpert/) | 224K imágenes con labels automáticas |

### 15.2 Tareas de investigación pendientes

| Tarea | Prioridad | Impacto | Dificultad |
|---|---|---|---|
| Correr ingestion pipeline para COVID-AR (19GB listo) | 🔴 Alta | Más datos para buscar | Fácil |
| Instalar BiomedCLIP en vez de CLIP genérico | 🔴 Alta | Mejor búsqueda de imágenes médicas | Media |
| Añadir LLaVA para describir imágenes | 🟡 Media | "¿Qué veo en esta imagen?" | Media |
| Medical NER con scispacy | 🟡 Media | Mejor extracción de entidades clínicas | Media |
| LIDC Annotations XML → metadata JSONL | 🟡 Media | Ground truth de 4 radiólogos | Fácil |
| Pseudo-labels con LLaMA | 🟡 Media | Etiquetas automáticas para entrenamiento | Media |
| Chunking por serie (no por slice) | 🟢 Baja | Búsquedas a nivel paciente | Media |
| Fine-tuning LoRA para radiología | 🟢 Baja | LLM que habla radiología | Difícil |
| DICOM viewer con cornerstone.js | 🟢 Baja | Frontend tipo radiólogo | Difícil |

### 15.3 Recursos y links útiles

| Recurso | URL | Para qué |
|---|---|---|
| **TCIA** (repositorio principal) | https://www.cancerimagingarchive.net | Todos los datasets de imagen médica |
| **PhysioNet** | https://physionet.org | MIMIC, informes clínicos |
| **Kaggle Medical** | https://www.kaggle.com/search?q=medical+imaging | Datasets + competiciones |
| **HuggingFace Medical** | https://huggingface.co/models?search=medical | Modelos pre-entrenados médicos |
| **Papers with Code** | https://paperswithcode.com/task/medical-image-segmentation | State-of-the-art en segmentación |
| **Grand Challenges** | https://grand-challenge.org | Competiciones de imagen médica |
| **MONAI** | https://monai.io | Framework NVIDIA para DL médico |
| **scispacy** | https://allenai.github.io/scispacy/ | NER biomédico |

