# RAG Pipeline: Lessons Learned & Hardening

## Übersicht

Dieses Dokument beschreibt die Erkenntnisse aus dem Aufbau der RAG-Pipeline und die daraus abgeleiteten Maßnahmen zur Härtung. Es dient als Referenz für zukünftige Ingestion-Runs und Pipeline-Erweiterungen.

## Architektur: Wann brauchen wir RAG vs. Direct PDF?

### RAG ist nötig für:

| Use Case | Warum RAG? |
|---|---|
| **Compliance Advisor (Chat)** | Semantische Suche über 38+ Dokumente in Echtzeit |
| **Cross-Regulation Mapping** | "Zeige alle Anforderungen zu Verschlüsselung" über alle Quellen |
| **Customer Scope-Filtering** | Nur Chunks aus relevanten Regulations für den Kunden |
| **Inkrementelle Updates** | Neues Dokument → nur neue Chunks verarbeiten |

### RAG ist NICHT nötig für:

| Use Case | Besser: Direct PDF |
|---|---|
| **Control-Generierung (Batch)** | PDF → PyMuPDF → Strukturparser → Artikel-Index → API |
| **PDF-QA/Verifizierung** | Substring-Match direkt im PDF (schneller, exakter) |
| **Artikel/§-Extraktion** | Regex-basierte Extraktion aus PDF-Text |

### Hybrid-Ansatz (Empfehlung)

```
Control-Generierung:    PDF → Strukturparser → Artikel-Index → Anthropic API
                        (KEIN RAG nötig, direkt aus PDF)

Runtime-Betrieb:        Qdrant-RAG für semantische Suche, Chat, Scope-Analyse
                        (RAG mit angereicherten Chunks + Struktur-Metadaten)
```

## Fehler und Root Causes

### 1. Doppelte Ingestion = Doppelte Controls

**Problem:** Gleiche PDFs unter verschiedenen Namen ingestiert (z.B. "Maschinenverordnung" und "Verordnung (EU) 2023/1230") → unterschiedliche Chunks (anderes Chunking) → anderer Hash → doppelt verarbeitet → doppelte Controls.

**Root Cause:**
- `regulation_name` aus Chunk-Metadaten statt aus kanonischer Quelle
- UNIQUE-Constraint nur `(chunk_hash, collection, document_version)` — nicht global
- Kein Check ob `regulation_code` bereits in einer Collection existiert

**Fix (implementiert):**
- `REGULATION_LICENSE_MAP` enthält jetzt kanonische `name`-Werte die den DB-Einträgen entsprechen
- `source_citation.source` wird aus `REGULATION_LICENSE_MAP.name` genommen, NICHT aus `chunk.regulation_name`
- Phase 5 Cleanup: 3.301 Duplikate hart gelöscht

**Fix (noch offen):**
- Chunk-Hash UNIQUE Constraint global machen: `(chunk_hash, document_version)` statt `(chunk_hash, collection, document_version)`
- Vor Ingestion: Check ob `regulation_code` bereits in einer Collection existiert

### 2. Chunks verlieren Strukturinformation

**Problem:** Chunks werden mitten im Absatz abgeschnitten. § und Artikelnummern fehlen in den Chunk-Metadaten. Kontext des Kapitels/Abschnitts geht verloren.

**Root Cause:**
- `chunk_strategy=recursive` mit `chunk_size=512, chunk_overlap=50` — zu kleine Chunks
- Chunking beachtet keine Dokumentstruktur (Artikel-/Paragraphengrenzen)
- Keine Einleitung/Kapitelkontext als Prefix

**Empfehlung für Re-Ingestion:**
- **Strukturiertes Chunking:** Chunks an Artikel-/Paragraphengrenzen schneiden
- **Kontext-Prefix:** Kapiteleinleitung und übergeordnete Struktur mitliefern
- **Metadaten anreichern:** `article`, `paragraph`, `article_type`, `section_hierarchy`
- **Größere Chunks:** Mindestens 1024 Tokens, besser volle Artikel/Paragraphen

### 3. Cross-Collection-Duplikate

**Problem:** `nist_csf_2_0` in `bp_compliance_ce` (67 Chunks) UND `bp_compliance_datenschutz` (162 Chunks). EU-Verordnungen sowohl in `bp_compliance_ce` als auch `bp_compliance_gesetze`.

**Root Cause:** Keine Collection-Zuordnungsregeln. Manuelle Zuweisung bei Ingestion.

**Fix:** `cleanup-qdrant-duplicates.py` Script bereinigt Cross-Collection-Duplikate.

**Empfehlung:** Klare Collection-Zuordnungsregeln:
- `bp_compliance_ce` = EU-Verordnungen + internationale Standards
- `bp_compliance_gesetze` = Deutsche + österreichische Gesetze (NUR nationale Gesetze)
- `bp_compliance_datenschutz` = EDPB/WP29 Leitlinien + Privacy Frameworks

### 4. OWASP Multilingual Controls

**Problem:** 324 OWASP Top 10 Controls in ZH, AR, ID, FR, ES, PT — Übersetzungen derselben 10 Kategorien. Kein Mehrwert, aber 324 doppelte Controls generiert.

**Root Cause:** Multilingual PDFs/GitHub-Quellen ohne Spracherkennung ingestiert.

**Fix:** 324 als `duplicate` markiert und gelöscht.

**Empfehlung:** Bei Ingestion Spracherkennung + Deduplizierung. Nur DE + EN behalten.

### 5. Fehlende Artikel/Paragraph-Extraktion

**Problem:** Chunks haben `article` und `paragraph` oft leer oder falsch. Die LLM-basierte Extraktion bei der Control-Generierung ist unzuverlässig.

**Root Cause:** Ingestion-Pipeline extrahiert keine Strukturinformation aus dem PDF.

**Fix (implementiert):** PDF-QA-Pipeline (`pdf_qa_all.py`) matched `source_original_text` gegen Original-PDFs und extrahiert korrekte Artikel/Paragraphen — 86% Match-Rate.

**Empfehlung:** Bei Re-Ingestion direkt in den Chunk-Metadaten speichern.

### 6. Job-Tracking nicht persistent

**Problem:** Generation-Jobs laufen als Background-Tasks. Kein Logging, welche Chunks verarbeitet, Status nur über API abfragbar. Bei API-Timeout oder Restart geht der Fortschritt verloren.

**Root Cause:** `asyncio.create_task()` hat keinen Recovery-Mechanismus.

**Fix (teilweise):** `canonical_generation_jobs` Tabelle trackt Jobs. `canonical_processed_chunks` markiert verarbeitete Chunks.

**Empfehlung:**
- Job-Log in DB persistieren (nicht nur stdout)
- Fortschritt in `canonical_generation_jobs.progress` als JSONB speichern
- Chunk-Level-Status: verarbeitet / übersprungen / Fehler
- Recovery-Fähigkeit: Job kann von letztem Checkpoint fortgesetzt werden

## Empfohlene Metadaten für Re-Ingestion

### Chunk-Level Metadaten (Qdrant Payload)

```json
{
  "chunk_text": "...",
  "regulation_code": "eu_2016_679",
  "regulation_name_de": "DSGVO (EU) 2016/679",
  "regulation_name_en": "GDPR (EU) 2016/679",
  "article": "25",
  "article_title": "Datenschutz durch Technikgestaltung und datenschutzfreundliche Voreinstellungen",
  "article_type": "article",
  "paragraph": "1",
  "section_hierarchy": ["Kapitel IV", "Abschnitt 2", "Artikel 25"],
  "chapter_context": "Kapitel IV — Verantwortlicher und Auftragsverarbeiter",
  "pages": [45, 46],
  "effective_date": "2018-05-25",
  "publication_date": "2016-04-27",
  "document_version": "2016-04-27",
  "source_language": "de",
  "source_url": "https://eur-lex.europa.eu/...",
  "celex": "32016R0679",
  "license": "EU_LAW",
  "license_rule": 1,
  "source_type": "law",
  "category": "datenschutz",
  "chunk_position": 42,
  "total_chunks": 423
}
```

### Dokument-Level Metadaten (Corpus Version)

```json
{
  "regulation_code": "eu_2016_679",
  "canonical_name_de": "DSGVO (EU) 2016/679",
  "canonical_name_en": "GDPR (EU) 2016/679",
  "document_type": "eu_regulation",
  "effective_date": "2018-05-25",
  "publication_date": "2016-04-27",
  "supersedes": null,
  "superseded_by": null,
  "source_pdf": "gdpr_regulation_eu_2016_679.pdf",
  "source_pdf_sha256": "abc123...",
  "total_articles": 99,
  "total_recitals": 173,
  "total_annexes": 0,
  "ingestion_date": "2026-03-20",
  "ingestion_version": "v2"
}
```

## Pipeline-Härtung Checkliste

### Vor Ingestion

- [ ] Prüfen ob `regulation_code` bereits in einer Collection existiert
- [ ] PDF-SHA256 gegen bekannte PDFs prüfen (Duplikat-Erkennung)
- [ ] `regulation_name` aus `REGULATION_LICENSE_MAP` verwenden, NICHT aus Chunk-Metadaten
- [ ] Spracherkennung: Nur DE + EN ingestieren
- [ ] Dokument-Metadaten (effective_date, publication_date) recherchieren

### Während Ingestion

- [ ] Strukturiertes Chunking an Artikel-/Paragraphengrenzen
- [ ] Kontext-Prefix mit Kapiteleinleitung
- [ ] Chunk-Metadaten anreichern (article, paragraph, article_type, section_hierarchy)
- [ ] Fortschritt in DB loggen

### Nach Ingestion

- [ ] Chunk-Count pro `regulation_code` prüfen (Sanity Check)
- [ ] PDF-QA gegen Original-PDF laufen lassen
- [ ] Cross-Collection-Duplikat-Check
- [ ] Corpus-Version in DB eintragen

### Control-Generierung

- [ ] `source_citation.source` aus `REGULATION_LICENSE_MAP.name`, NICHT aus Chunk-Metadaten
- [ ] Harmonisierung: Threshold 0.85 für Duplikate innerhalb gleicher `regulation_code`
- [ ] Cross-Regulation-Harmonisierung bei ähnlichen Themen (z.B. DSGVO Art. 25 ↔ NIS2 Art. 21)
- [ ] Job-Fortschritt persistent in DB speichern

## Workflow: Mac Mini → Production Sync

```
1. Mac Mini: PDF → Qdrant (lokal, http://macmini:6333)
2. Mac Mini: Control-Generierung → PostgreSQL (shared, 46.225.100.82:54321)
3. QA: PDF-Match, Dedup, Source-Normalisierung
4. Qdrant Migration: macmini:6333 → qdrant-dev.breakpilot.ai (scripts/migrate-qdrant.py)
5. Deploy: git push gitea → Coolify Build + Deploy
```

**WICHTIG:** PostgreSQL ist SHARED — Änderungen auf Mac Mini sind sofort in Production sichtbar. Qdrant hat getrennte Instanzen (lokal + production) und muss manuell synchronisiert werden.

## Scripts

| Script | Beschreibung |
|---|---|
| `scripts/ingest-phase-h.sh` | Haupt-Ingestion: 38 Dokumente → Qdrant |
| `scripts/cleanup-qdrant-duplicates.py` | Qdrant Duplikat-Cleanup (8 Schritte) |
| `scripts/migrate-qdrant.py` | Qdrant Migration: lokal → production |
| `scripts/qa/phase5_normalize_and_cleanup.py` | DB Normalisierung + Hard Delete |
| `scripts/qa/pdf_qa_all.py` | PDF-Match QA |
