Files
archivmail/features/PROJ-35-ocr-anhang-volltext.md
T
sysops 88e9d0c08c docs(PROJ-35): Status auf Deployed + Bekannte Pitfalls dokumentiert
- 132 + 131 deployed
- hashMailID-Bit-Mask-Fix dokumentiert (negative IDs Problem)
- OCR-Tempo-Constraint (CPU-bound) erklärt
- Boot-Resume-Loop als bekannter, unkritischer Bug vermerkt
2026-05-08 23:10:59 +02:00

262 lines
13 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.
# PROJ-35: OCR & Anhang-Volltext-Indexierung
## Status: Deployed
**Created:** 2026-04-04
**Last Updated:** 2026-05-08
**Deployed:** 2026-05-08 (192.168.1.131 + 192.168.1.132)
## Dependencies
- Requires: PROJ-5 (Speicherung & Indexierung) — Mailparser + Manticore-Index
- Requires: PROJ-30 (Manticore Migration) — Volltext-Index als Basis
## Motivation
PDF- und Bild-Anhänge (Rechnungen, Verträge, eingescannte Dokumente) sind bisher nicht durchsuchbar — nur der Dateiname wird indexiert. OCR macht den Inhalt dieser Anhänge volltext-durchsuchbar ohne den normalen Mail-Eingang zu verlangsamen.
## User Stories
- Als Nutzer möchte ich in archivierten Rechnungs-PDFs nach Beträgen oder Kundennummern suchen können.
- Als Nutzer möchte ich eingescannte Dokumente (JPG/PNG/TIFF) im Volltext durchsuchen können.
- Als Admin möchte ich OCR per Mandant aktivieren oder deaktivieren können.
- Als Nutzer soll der Mail-Eingang nicht durch OCR verlangsamt werden (asynchrone Verarbeitung).
## Acceptance Criteria
- [x] PDF-Anhänge mit eingebettetem Text: Text via `pdftotext` extrahieren (kein OCR nötig, schnell)
- [x] PDF-Anhänge ohne Text (Scan): OCR via Tesseract (deutsch + englisch)
- [x] Bild-Anhänge (JPG, PNG, TIFF, BMP, WEBP): OCR via Tesseract
- [x] OCR läuft asynchron — Mail wird sofort gespeichert, OCR-Text nachgeliefert
- [x] OCR-Text wird in Manticore-Index im Feld `attachment_text` gespeichert (neues Feld via `ALTER TABLE`)
- [x] Volltext-Suche findet Mails anhand von OCR-Text in Anhängen (`MATCH(?)` ohne Field-Scoping in `manticore.go::Search` deckt das Feld automatisch ab)
- [x] OCR-Status pro Mail in PostgreSQL: `ocr_status` (`pending` / `done` / `failed` / `skipped` / `disabled`)
- [x] OCR kann per Mandant deaktiviert werden (`tenants.ocr_enabled BOOLEAN DEFAULT TRUE`)
- [x] `archivmail ocr-reprocess` — OCR für alle oder einzelne Mandanten nachholen (Flags: `--tenant`, `--status`, `--limit`)
- [x] Keine neue externe Service-Abhängigkeit bei Laufzeit — nur System-Pakete (`tesseract-ocr`, `tesseract-ocr-deu`, `poppler-utils`)
## Edge Cases
- Sehr große PDFs (>50 MB): Timeout nach 60s, Status `failed`, Mail bleibt auffindbar per Metadaten
- Passwortgeschützte PDFs: OCR überspringen, Status `skipped`
- Anhänge ohne OCR-fähiges Format (ZIP, EXE, ...): überspringen
- Tesseract nicht installiert: OCR deaktiviert, Warnung beim Start, kein Absturz
## Tech Design
### Neue Komponenten
**`internal/ocr/ocr.go`**
```go
// ExtractText extrahiert Text aus einem Anhang.
// Für PDFs: pdftotext zuerst, dann Tesseract als Fallback.
// Für Bilder: direkt Tesseract.
// Gibt leeren String zurück wenn Format nicht unterstützt oder Fehler.
func ExtractText(data []byte, contentType string, langs []string) (string, error)
// IsAvailable prüft ob Tesseract installiert ist.
func IsAvailable() bool
```
**`internal/ocr/worker.go`** — Async-Worker
```go
// Worker liest aus einem Channel und verarbeitet OCR-Jobs.
// Läuft als Goroutine im Hintergrund.
type Worker struct { ... }
func NewWorker(store *storage.Store, idxMgr index.TenantIndexer) *Worker
func (w *Worker) Submit(mailID string, tenantID *int64)
func (w *Worker) Start(ctx context.Context)
```
### DB-Schema
```sql
-- OCR-Status pro Mail
ALTER TABLE emails ADD COLUMN IF NOT EXISTS ocr_status TEXT DEFAULT 'pending';
-- Werte: pending | done | failed | skipped | disabled
-- OCR pro Tenant konfigurierbar
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS ocr_enabled BOOLEAN DEFAULT TRUE;
-- Index für OCR-Queue
CREATE INDEX IF NOT EXISTS idx_emails_ocr_status ON emails (ocr_status) WHERE ocr_status = 'pending';
```
### Manticore-Schema-Erweiterung
```sql
-- Neues Feld im RT-Index
ALTER TABLE emails_tenant_1 ADD COLUMN attachment_text text;
```
In `internal/index/manticore.go`:
- `MailDocument.AttachmentText string` hinzufügen
- `ensureTable()` — neues Feld im CREATE TABLE
- `IndexSync()``attachment_text` befüllen
### Verarbeitungs-Ablauf
```
Mail eingehend (SMTP/IMAP/Import)
→ mailparser.Parse() → Anhänge erkannt
→ mailStore.Save() → Mail gespeichert, ocr_status = 'pending'
→ Manticore-Index ohne attachment_text
→ OCR-Worker.Submit(mailID)
OCR-Worker (async, Goroutine):
→ mailStore.Load(mailID) → Rohdaten
→ mailparser.Parse() → Anhänge
→ für jeden Anhang:
→ ocr.ExtractText(data, contentType, ["deu","eng"])
→ idxMgr.ForTenant(tenantID).UpdateAttachmentText(mailID, text)
→ mailStore.SetOCRStatus(mailID, "done")
```
### Neues CLI-Subkommando
```bash
archivmail ocr-reprocess --config /etc/archivmail/config.yml
archivmail ocr-reprocess --config /etc/archivmail/config.yml --tenant 1
archivmail ocr-reprocess --config /etc/archivmail/config.yml --status failed
```
### Installation auf Server
```bash
apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils
# Prüfen
tesseract --version
pdftotext -v
```
`update.sh` — optionale Installation (kein Abbruch wenn nicht verfügbar):
```bash
apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils 2>/dev/null || true
```
### Performance-Überlegungen
| Format | Tool | Dauer (A4-Seite) |
|--------|------|-----------------|
| PDF mit Text | pdftotext | < 100ms |
| PDF als Scan | Tesseract | 13s |
| JPG (300 DPI) | Tesseract | 0.52s |
OCR-Worker mit konfigurierbarer Worker-Anzahl (Standard: 2 Goroutinen).
Keine Blockierung des Mail-Eingangs — Submit() ist non-blocking.
## Nicht in Scope
- Microsoft Word / Excel / PowerPoint direkt (nur wenn als PDF geliefert)
- Layout-Analyse oder Tabellen-Extraktion
- Sprachen außer Deutsch und Englisch (erweiterbar via Config)
- Cloud-OCR-Services (bewusst: nur lokale Tools)
## Tech Design
_Vollständig oben beschrieben_
---
## Implementation Notes (2026-05-08)
### Neue Dateien
| Datei | Aufgabe |
|---|---|
| `internal/ocr/ocr.go` | `ExtractText(ctx, data, contentType, filename, langs)` mit `pdftotext``pdftoppm` + `tesseract` Fallback. Klassifizierung über MIME + Datei-Endung. Timeouts (60 s Default), Größenlimit (50 MiB), Sentinel-Errors `ErrUnsupported`/`ErrTooLarge`/`ErrEncrypted`/`ErrUnavailable`. Tempdir per `SetTempDir()` umlenkbar (wegen systemd-Sandbox). |
| `internal/ocr/worker.go` | `Worker` mit `Submit(mailID, *tenantID)`-Queue, N Goroutinen (Default 2). Lädt Mail aus Storage, parst, ruft `ExtractText` für jeden Anhang, schreibt kombinierten Text via `AttachmentTextUpdater` in Manticore und setzt `ocr_status` final. Submit ist non-blocking — volle Queue → Warning + Drop. |
| `internal/storage/ocr.go` | `SetOCRStatus`, `OCREnabled` (opt-out), `GetPendingOCRMails`, `GetMailsByOCRStatus` mit optionalem Tenant-Filter via `email_refs`/`emails.tenant_id`. `PendingOCRMail{ID, *TenantID}`-Rückgabetyp. |
| `cmd/archivmail/cmd_ocr_reprocess.go` | CLI `archivmail ocr-reprocess [--config] [--tenant N] [--status pending\|done\|failed\|skipped\|disabled\|all] [--limit N]`. Setzt `failed`/`skipped`-Mails auf `pending` zurück, queued sie im Worker, blockiert bis Drain. |
### Geänderte Dateien
| Datei | Änderung |
|---|---|
| `internal/index/index.go` | `MailDocument.AttachmentText string` ergänzt; neues optionales Interface `AttachmentTextUpdater { UpdateAttachmentText(mailID, text string) error }`. |
| `internal/index/manticore.go` | `ensureTable()`: legt `attachment_text text` an + ruft `ensureColumn()` für ALTER auf bestehende Tabellen (Manticore kennt kein `ALTER … IF NOT EXISTS`, daher DESC-Probe + ALTER). `IndexSync()`: neues Feld in `REPLACE INTO`-Spaltenliste. `UpdateAttachmentText()`: lädt Row, REPLACE INTO mit überschriebenem `attachment_text`. |
| `internal/storage/storage.go` | `initSchema`: `ALTER TABLE emails ADD COLUMN IF NOT EXISTS ocr_status TEXT DEFAULT 'pending'` + `CREATE INDEX … WHERE ocr_status = 'pending'`. |
| `internal/tenantstore/store.go` | DDL-Block: `ALTER TABLE tenants ADD COLUMN IF NOT EXISTS ocr_enabled BOOLEAN NOT NULL DEFAULT TRUE`. |
| `cmd/archivmail/main.go` | OCR-Worker beim Boot starten (`ocr.NewWorker(...).Start(ctx)`), `defer Stop()`. Boot-Resume: `GetPendingOCRMails(ctx, nil, 5000)` → Submit. Im laufenden Mail-Eingang nach `worker.Submit(doc)` zusätzlich `ocrWorker.Submit(id, tenantID)`. |
| `update.sh` | `apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils 2>/dev/null \|\| true` (idempotent, kein Abbruch). |
### Architektur-Entscheidungen
- **Suche bleibt unverändert**: `manticore.go::Search` nutzt `MATCH(?)` ohne Field-Scoping → `attachment_text` wird automatisch mit durchsucht. Keine Änderung an `/api/search` oder `/api/v1/mails` nötig.
- **`UpdateAttachmentText` statt voller Reindex**: Manticore RT erlaubt kein `UPDATE` auf Text-Spalten → wir lesen die Row, machen REPLACE INTO. Vermeidet Race mit dem normalen `IndexWorker`, wenn beide gleichzeitig auf dieselbe Mail arbeiten — der OCR-Worker greift erst nach erfolgter Erst-Indexierung.
- **OCR ist opt-out, kein opt-in**: `OCREnabled()` defaultet auf `true` — auch bei DB-losen Modi, fehlender `tenants.ocr_enabled`-Spalte oder Lookup-Fehlern.
- **Tempdir umlenkbar**: Die `archivmail.service`-systemd-Unit hatte `PrivateTmp=yes` o.ä., was `os.MkdirTemp("")` zwar erlaubt, aber das gemeinsame `/tmp` versteckt. `SetTempDir(cfg.Storage.StorePath + "/ocr-tmp")` schreibt jetzt in einen vom Daemon explizit erlaubten Pfad.
- **Boot-Resume drosselt**: Falls beim Restart >Queue-Capacity Mails `pending` sind, wird stapelweise enqueued (Commit `a252ad6`).
## Bekannte Pitfalls
1. **systemd-Tempdir-Restriktion** (Commit `6d835ae`): Beim ersten Run wurden 19.043 Mails fälschlich auf `failed` markiert, weil `os.MkdirTemp("")` in einer Unit-internen Sandbox lag. Fix: `SetTempDir(<storage_dir>/ocr-tmp)`.
2. **`hashMailID` darf nicht negativ sein** (Commits `7ba677e` + `d71d20d`): Manticore RT erlaubt nur positive int64-IDs. Initial-Implementierung nutzte uint64 → Treiber meldete „number out of range". Erster Fix-Versuch mit `int64(h.Sum64())` produzierte negative Werte für die obere uint64-Hälfte → Manticore wies INSERT/REPLACE ab („Negative document ids are not allowed"). Finaler Fix: Bit-Mask `0x7FFFFFFFFFFFFFFF` zwingt das Top-Bit auf 0, Result garantiert in `[0, 2^63-1]`.
3. **Sehr große PDFs (>50 MiB)** werden über `MaxAttachmentSize` übersprungen → Status `skipped`, Mail bleibt per Metadaten auffindbar.
4. **Passwort-geschützte PDFs**: `pdftotext` liefert `Incorrect password``ErrEncrypted` → Status `skipped`.
5. **OCR-Tempo ist CPU-gebunden**: Auf 192.168.1.132 (2 GB RAM, 2 vCPU) schafft tesseract bei 2 Worker-Goroutinen ~37 OCR-Events/Min — hauptsächlich durch große Smartphone-JPGs (45+ s pro Datei) und mehrseitige PDFs. Ein vollständiger Reprocess von 15.000 Mails dauert dort ~12 Tage. Auf stärkeren Servern entsprechend weniger. Mehr Worker-Goroutinen bringen nichts solange CPU bei 100 % steht.
6. **Boot-Resume-Loop bei großen Backlogs**: Wenn beim Start sehr viele Mails `pending` sind, kann derselbe Batch mehrfach via `GetPendingOCRMails` geholt und re-enqueued werden, weil der Worker den Status erst nach Verarbeitung setzt. Submit ist non-blocking (Drop bei voller Queue), funktional unkritisch — aber DB-Last und Log-Rauschen. Mitigation: kleines `seen`-Set im Boot-Resume oder Status-Vorsetzung auf `processing` beim Submit. Nicht behoben in 0.x — siehe Folgearbeit.
## QA Test Results
### Test-Server 192.168.1.132
Nach Bit-Mask-Fix-Deploy + TRUNCATE + frischem Reindex:
```
reindex: complete total=15.191 indexed=15.191 errors=0
```
OCR-Suche live verifiziert (Beispiel):
```sql
SELECT mail_id, attachment_text
FROM emails_tenant_1
WHERE MATCH('@attachment_text rechnung')
LIMIT 3;
-- 3 Treffer: Cleverbridge-Rechnungen mit IBAN/Beträgen vollständig indexiert
```
OCR-Reprocess läuft im Hintergrund weiter (~5 Events/Min, ~12 Tage Gesamtlaufzeit).
### Produktions-Server 192.168.1.131
```
reindex: complete total=86 indexed=86 errors=0
ocr worker: started workers=2 queue=1000
```
- DESC `emails_global` enthält `attachment_text text indexed stored`
- PostgreSQL `emails.ocr_status` Spalte vorhanden ✓
- 86 Mails alle `skipped` (kein Anhang) — korrektes Verhalten ✓
- Service-Stack (`manticore` + `archivmail` + `archivmail-web`) durchgehend `active`
## Deployment
| Server | Datum | Status |
|---|---|---|
| 192.168.1.132 (Test) | 2026-05-08 | Deployed — Reindex grün, OCR-Reprocess läuft im Hintergrund (~5/min, mehrere Tage Gesamtlaufzeit) |
| 192.168.1.131 (Prod) | 2026-05-08 | Deployed — 86 Mails reindexiert, alle ohne Anhang → `skipped` |
### Pre-Deployment-Schritte (auf Server)
```bash
# Bereits in update.sh enthalten:
apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils
```
### Verifikation nach Deploy
```bash
# Schema-Check
curl -s 'http://127.0.0.1:9308/sql?mode=raw' \
--data-urlencode 'query=DESC emails_global' | grep attachment_text
# OCR-Worker-Start im Log
journalctl -u archivmail --since '2 min ago' | grep -i 'ocr worker'
# Status-Verteilung
sudo -u postgres psql archivmail -c \
"SELECT ocr_status, COUNT(*) FROM emails GROUP BY ocr_status;"
# Such-Test (Begriff aus bekanntem PDF)
curl -s 'http://127.0.0.1:9308/sql?mode=raw' \
--data-urlencode "query=SELECT mail_id FROM emails_global WHERE MATCH('rechnungsnummer') LIMIT 5"
```