# Plan: Embedding-Service Separation

## Ziel
Trennung der ML/Embedding-Komponenten vom klausur-service in einen eigenständigen `embedding-service`, um die Build-Zeit von ~20 Minuten auf ~30 Sekunden zu reduzieren.

## Aktuelle Situation

| Service | Build-Zeit | Image-Größe | Problem |
|---------|------------|-------------|---------|
| klausur-service | ~20 min | ~2.5 GB | PyTorch + sentence-transformers werden bei jedem Build installiert |

## Ziel-Architektur

```
┌─────────────────┐     HTTP      ┌──────────────────┐
│ klausur-service │ ───────────→ │ embedding-service │
│   (FastAPI)     │              │    (FastAPI)      │
│   Port 8086     │              │    Port 8087      │
│   ~200 MB       │              │    ~2.5 GB        │
│   Build: 30s    │              │    Build: 15 min  │
└─────────────────┘              └──────────────────┘
         │                                │
         └────────────┬───────────────────┘
                      ▼
              ┌───────────────┐
              │    Qdrant     │
              │  Port 6333    │
              └───────────────┘
```

## Phase 1: Neuen Embedding-Service erstellen

### 1.1 Verzeichnisstruktur anlegen
```
klausur-service/
├── embedding-service/           # NEU
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── main.py                  # FastAPI App
│   ├── eh_pipeline.py           # Kopie
│   ├── reranker.py              # Kopie
│   ├── hyde.py                  # Kopie
│   ├── hybrid_search.py         # Kopie
│   ├── pdf_extraction.py        # Kopie
│   └── config.py                # Embedding-Konfiguration
├── backend/                     # Bestehend (wird angepasst)
└── frontend/                    # Bestehend
```

### 1.2 Dateien in embedding-service erstellen

**requirements.txt** (ML-spezifisch):
```
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
torch>=2.0.0
sentence-transformers>=2.2.0
qdrant-client>=1.7.0
unstructured>=0.12.0
pypdf>=4.0.0
httpx>=0.26.0
pydantic>=2.0.0
python-dotenv>=1.0.0
```

**main.py** - API-Endpoints:
- `POST /embed` - Generiert Embeddings für Text/Liste von Texten
- `POST /embed-single` - Einzelnes Embedding
- `POST /rerank` - Re-Ranking von Suchergebnissen
- `POST /extract-pdf` - PDF-Text-Extraktion
- `GET /health` - Health-Check
- `GET /models` - Verfügbare Modelle

### 1.3 Dockerfile (embedding-service)
```dockerfile
FROM python:3.11-slim

WORKDIR /app

# PyTorch CPU-only für kleinere Images
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Modelle vorab laden (Layer-Cache)
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-m3')"
RUN python -c "from sentence_transformers import CrossEncoder; CrossEncoder('BAAI/bge-reranker-v2-m3')"

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8087"]
```

## Phase 2: Klausur-Service anpassen

### 2.1 ML-Dependencies aus requirements.txt entfernen
Entfernen:
- `torch`
- `sentence-transformers`
- `unstructured`
- `pypdf`

Behalten:
- `fastapi`, `uvicorn`, `httpx`
- `qdrant-client` (für Suche)
- `cryptography` (BYOEH)
- Alle Business-Logic-Dependencies

### 2.2 Embedding-Client erstellen
Neue Datei `backend/embedding_client.py`:
```python
class EmbeddingClient:
    def __init__(self, base_url: str = "http://embedding-service:8087"):
        self.base_url = base_url

    async def generate_embeddings(self, texts: list[str]) -> list[list[float]]:
        async with httpx.AsyncClient() as client:
            response = await client.post(f"{self.base_url}/embed", json={"texts": texts})
            return response.json()["embeddings"]

    async def rerank(self, query: str, documents: list[str]) -> list[dict]:
        async with httpx.AsyncClient() as client:
            response = await client.post(f"{self.base_url}/rerank",
                                         json={"query": query, "documents": documents})
            return response.json()["results"]
```

### 2.3 Bestehende Aufrufe umleiten
Dateien anpassen:
- `backend/main.py`: `generate_single_embedding()` → `embedding_client.generate_embeddings()`
- `backend/admin_api.py`: Embedding-Aufrufe über Client
- `backend/qdrant_service.py`: Bleibt für Suche, Indexierung nutzt Client

## Phase 3: Docker-Compose Integration

### 3.1 docker-compose.dev.yml erweitern
```yaml
services:
  klausur-service:
    build:
      context: ./klausur-service
      dockerfile: Dockerfile
    ports:
      - "8086:8086"
    environment:
      - EMBEDDING_SERVICE_URL=http://embedding-service:8087
    depends_on:
      - embedding-service
      - qdrant

  embedding-service:
    build:
      context: ./klausur-service/embedding-service
      dockerfile: Dockerfile
    ports:
      - "8087:8087"
    environment:
      - EMBEDDING_BACKEND=local
      - LOCAL_EMBEDDING_MODEL=BAAI/bge-m3
      - LOCAL_RERANKER_MODEL=BAAI/bge-reranker-v2-m3
    volumes:
      - embedding-models:/root/.cache/huggingface  # Model-Cache
    deploy:
      resources:
        limits:
          memory: 4G

  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
    volumes:
      - qdrant-data:/qdrant/storage

volumes:
  embedding-models:
  qdrant-data:
```

## Phase 4: Tests und Validierung

### 4.1 Unit Tests für Embedding-Service
- Test Embedding-Generierung
- Test Re-Ranking
- Test PDF-Extraktion
- Test Health-Endpoint

### 4.2 Integration Tests
- Test klausur-service → embedding-service Kommunikation
- Test RAG-Query End-to-End
- Test EH-Upload mit Embedding

### 4.3 Performance-Validierung
- Build-Zeit klausur-service messen (Ziel: <1 min)
- Embedding-Latenz messen (Ziel: <500ms für einzelnes Embedding)
- Re-Ranking-Latenz messen (Ziel: <1s für 10 Dokumente)

## Implementierungsreihenfolge

1. **embedding-service/main.py** - FastAPI App mit Endpoints
2. **embedding-service/config.py** - Konfiguration
3. **embedding-service/requirements.txt** - Dependencies
4. **embedding-service/Dockerfile** - Container-Build
5. **backend/embedding_client.py** - HTTP-Client
6. **backend/requirements.txt** - ML-Deps entfernen
7. **backend/main.py** - Aufrufe umleiten
8. **backend/admin_api.py** - Aufrufe umleiten
9. **docker-compose.dev.yml** - Service hinzufügen
10. **Tests** - Validierung

## Zu bewegende Dateien (Referenz)

| Datei | Zeilen | Aktion |
|-------|--------|--------|
| eh_pipeline.py | 777 | Kopieren → embedding-service |
| reranker.py | 253 | Kopieren → embedding-service |
| hyde.py | 209 | Kopieren → embedding-service |
| hybrid_search.py | 285 | Kopieren → embedding-service |
| pdf_extraction.py | 479 | Kopieren → embedding-service |

## Umgebungsvariablen

### embedding-service
```
EMBEDDING_BACKEND=local
LOCAL_EMBEDDING_MODEL=BAAI/bge-m3
LOCAL_RERANKER_MODEL=BAAI/bge-reranker-v2-m3
OPENAI_API_KEY=  # Optional für OpenAI-Backend
PDF_EXTRACTION_BACKEND=auto
LOG_LEVEL=INFO
```

### klausur-service (neu)
```
EMBEDDING_SERVICE_URL=http://embedding-service:8087
```

## Risiken und Mitigationen

| Risiko | Mitigation |
|--------|------------|
| Netzwerk-Latenz zwischen Services | Model-Caching, Connection-Pooling |
| embedding-service nicht erreichbar | Health-Checks, Retry-Logik, Graceful Degradation |
| Inkonsistente Embedding-Modelle | Versionierung, Model-Hash-Prüfung |
| Erhöhter RAM-Bedarf (2 Container) | Memory-Limits in Docker, Model-Offloading |

## Erwartete Ergebnisse

| Metrik | Vorher | Nachher |
|--------|--------|---------|
| Build-Zeit klausur-service | ~20 min | ~30 sec |
| Build-Zeit embedding-service | - | ~15 min |
| Image-Größe klausur-service | ~2.5 GB | ~200 MB |
| Image-Größe embedding-service | - | ~2.5 GB |
| Entwickler-Iteration | Langsam | Schnell |

## Nicht vergessen (Task A)

Nach Abschluss der Service-Trennung:
- [ ] EH-Upload-Wizard mit Test-Klausur testen
- [ ] Security-Infobox im Wizard verifizieren
- [ ] End-to-End RAG-Query testen
