Files
archivmail/features/PROJ-35-ocr-anhang-volltext.md
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

13 KiB
Raw Permalink Blame History

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

  • PDF-Anhänge mit eingebettetem Text: Text via pdftotext extrahieren (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_text gespeichert (neues Feld via ALTER TABLE)
  • Volltext-Suche findet Mails anhand von OCR-Text in Anhängen (MATCH(?) ohne Field-Scoping in manticore.go::Search deckt 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 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

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 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 pdftotextpdftoppm + 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 passwordErrEncrypted → 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):

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)

# 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"