7ba677e4b5
Manticore akzeptiert in `id`-bigint nur signed int64. Der mysql-Treiber serialisiert Parameter als Dezimal-String → uint64-Werte > int64.MaxValue führten zu "number ... is out of range". Fix: int64(h.Sum64()) verlustfreier Bit-Cast — bestehende Dokumente bleiben erreichbar. Auch: PROJ-35-Spec auf In Progress + Implementation Notes/Pitfalls/QA-Block, INDEX.md-Status-Update.
11 KiB
11 KiB
PROJ-35: OCR & Anhang-Volltext-Indexierung
Status: In Progress
Created: 2026-04-04 Last Updated: 2026-05-08
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
- PDF-Anhänge mit eingebettetem Text: Text via
pdftotextextrahieren (kein OCR nötig, schnell) - PDF-Anhänge ohne Text (Scan): OCR via Tesseract (deutsch + englisch)
- Bild-Anhänge (JPG, PNG, TIFF, BMP, WEBP): OCR via Tesseract
- OCR läuft asynchron — Mail wird sofort gespeichert, OCR-Text nachgeliefert
- OCR-Text wird in Manticore-Index im Feld
attachment_textgespeichert (neues Feld viaALTER TABLE) - Volltext-Suche findet Mails anhand von OCR-Text in Anhängen (
MATCH(?)ohne Field-Scoping inmanticore.go::Searchdeckt das Feld automatisch ab) - OCR-Status pro Mail in PostgreSQL:
ocr_status(pending/done/failed/skipped/disabled) - OCR kann per Mandant deaktiviert werden (
tenants.ocr_enabled BOOLEAN DEFAULT TRUE) archivmail ocr-reprocess— OCR für alle oder einzelne Mandanten nachholen (Flags:--tenant,--status,--limit)- 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
// 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
// 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
-- 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
-- Neues Feld im RT-Index
ALTER TABLE emails_tenant_1 ADD COLUMN attachment_text text;
In internal/index/manticore.go:
MailDocument.AttachmentText stringhinzufügenensureTable()— neues Feld im CREATE TABLEIndexSync()—attachment_textbefü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
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
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):
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 | 1–3s |
| JPG (300 DPI) | Tesseract | 0.5–2s |
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::SearchnutztMATCH(?)ohne Field-Scoping →attachment_textwird automatisch mit durchsucht. Keine Änderung an/api/searchoder/api/v1/mailsnötig. UpdateAttachmentTextstatt voller Reindex: Manticore RT erlaubt keinUPDATEauf Text-Spalten → wir lesen die Row, machen REPLACE INTO. Vermeidet Race mit dem normalenIndexWorker, 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 auftrue— auch bei DB-losen Modi, fehlendertenants.ocr_enabled-Spalte oder Lookup-Fehlern. - Tempdir umlenkbar: Die
archivmail.service-systemd-Unit hattePrivateTmp=yeso.ä., wasos.MkdirTemp("")zwar erlaubt, aber das gemeinsame/tmpversteckt.SetTempDir(cfg.Storage.StorePath + "/ocr-tmp")schreibt jetzt in einen vom Daemon explizit erlaubten Pfad. - Boot-Resume drosselt: Falls beim Restart >Queue-Capacity Mails
pendingsind, wird stapelweise enqueued (Commita252ad6).
Bekannte Pitfalls
- systemd-Tempdir-Restriktion (Commit
6d835ae): Beim ersten Run wurden 19.043 Mails fälschlich auffailedmarkiert, weilos.MkdirTemp("")in einer Unit-internen Sandbox lag. Fix:SetTempDir(<storage_dir>/ocr-tmp). hashMailIDmuss konsistent zu IndexSync sein:UpdateAttachmentTextmuss exakt dasselbe Bit-Muster nutzen wieIndexSync, sonst werden neue Rows angelegt statt überschrieben. Der int64-Cast wird vom MySQL-Treiber sauber serialisiert.- Sehr große PDFs (>50 MiB) werden über
MaxAttachmentSizeübersprungen → Statusskipped, Mail bleibt per Metadaten auffindbar. - Passwort-geschützte PDFs:
pdftotextliefertIncorrect password→ErrEncrypted→ Statusskipped.
QA Test Results
Test-Server 192.168.1.132 (Stand 2026-05-08)
ocr_status | count
done | 92
failed | 96
pending | 13.603 ← reprocess läuft
skipped | 1.227
DESC emails_globalenthältattachment_text text indexed stored✓archivmail ocr-reprocess --status failedreaktiviert diefailed-Marker korrekt- Service-Stack (manticore + archivmail + archivmail-web) durchgehend
active
Produktions-Server 192.168.1.131
Noch nicht deployed. Anstehend nach Abschluss des 132-Recoverys.
Deployment
| Server | Datum | Status |
|---|---|---|
| 192.168.1.132 (Test) | 2026-05-08 | Deployed, Recovery läuft (~13.600 Mails im Reprocess) |
| 192.168.1.131 (Prod) | ausstehend | Wartet auf grünes 132 |
Pre-Deployment-Schritte (auf Server)
# Bereits in update.sh enthalten:
apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils
Verifikation nach Deploy
# 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"