fix(PROJ-35): hashMailID liefert int64 statt uint64

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.
This commit is contained in:
sysops
2026-05-08 22:40:25 +02:00
parent 6d835aefac
commit 7ba677e4b5
3 changed files with 112 additions and 18 deletions
+1 -1
View File
@@ -51,7 +51,7 @@
| PROJ-33 | IMAP-Modus: Gemeinsames Archiv vs. Persönlicher Posteingang | Deployed | [PROJ-33](PROJ-33-imap-modus-shared-personal.md) | 2026-03-31 | | PROJ-33 | IMAP-Modus: Gemeinsames Archiv vs. Persönlicher Posteingang | Deployed | [PROJ-33](PROJ-33-imap-modus-shared-personal.md) | 2026-03-31 |
| PROJ-34 | Retention-Policy + Löschsperre (GoBD-Compliance) | Deployed | [PROJ-34](PROJ-34-retention-policy.md) | 2026-03-31 | | PROJ-34 | Retention-Policy + Löschsperre (GoBD-Compliance) | Deployed | [PROJ-34](PROJ-34-retention-policy.md) | 2026-03-31 |
| PROJ-35 | OCR & Anhang-Volltext-Indexierung | Planned | [PROJ-35](PROJ-35-ocr-anhang-volltext.md) | 2026-04-04 | | PROJ-35 | OCR & Anhang-Volltext-Indexierung | In Progress | [PROJ-35](PROJ-35-ocr-anhang-volltext.md) | 2026-04-04 |
| PROJ-36 | gzip-Kompression + storage_objects-Tabelle | Deployed | [PROJ-36](PROJ-36-compression-storage-objects.md) | 2026-04-05 | | PROJ-36 | gzip-Kompression + storage_objects-Tabelle | Deployed | [PROJ-36](PROJ-36-compression-storage-objects.md) | 2026-04-05 |
| PROJ-37 | Attachment-Deduplication (Hash-basiert) | Deployed | [PROJ-37](PROJ-37-attachment-deduplication.md) | 2026-04-05 | | PROJ-37 | Attachment-Deduplication (Hash-basiert) | Deployed | [PROJ-37](PROJ-37-attachment-deduplication.md) | 2026-04-05 |
| PROJ-38 | Mail-Threading (In-Reply-To / References) | Deployed | [PROJ-38](PROJ-38-mail-threading.md) | 2026-04-05 | | PROJ-38 | Mail-Threading (In-Reply-To / References) | Deployed | [PROJ-38](PROJ-38-mail-threading.md) | 2026-04-05 |
+99 -14
View File
@@ -1,8 +1,8 @@
# PROJ-35: OCR & Anhang-Volltext-Indexierung # PROJ-35: OCR & Anhang-Volltext-Indexierung
## Status: Planned ## Status: In Progress
**Created:** 2026-04-04 **Created:** 2026-04-04
**Last Updated:** 2026-04-04 **Last Updated:** 2026-05-08
## Dependencies ## Dependencies
- Requires: PROJ-5 (Speicherung & Indexierung) — Mailparser + Manticore-Index - Requires: PROJ-5 (Speicherung & Indexierung) — Mailparser + Manticore-Index
@@ -21,16 +21,16 @@ PDF- und Bild-Anhänge (Rechnungen, Verträge, eingescannte Dokumente) sind bish
## Acceptance Criteria ## Acceptance Criteria
- [ ] PDF-Anhänge mit eingebettetem Text: Text via `pdftotext` extrahieren (kein OCR nötig, schnell) - [x] 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) - [x] PDF-Anhänge ohne Text (Scan): OCR via Tesseract (deutsch + englisch)
- [ ] Bild-Anhänge (JPG, PNG, TIFF): OCR via Tesseract - [x] Bild-Anhänge (JPG, PNG, TIFF, BMP, WEBP): OCR via Tesseract
- [ ] OCR läuft asynchron — Mail wird sofort gespeichert, OCR-Text nachgeliefert - [x] OCR läuft asynchron — Mail wird sofort gespeichert, OCR-Text nachgeliefert
- [ ] OCR-Text wird in Manticore-Index im Feld `attachment_text` gespeichert (neues Feld) - [x] 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 - [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)
- [ ] OCR-Status pro Mail in PostgreSQL: `ocr_status` (pending / done / failed / skipped) - [x] OCR-Status pro Mail in PostgreSQL: `ocr_status` (`pending` / `done` / `failed` / `skipped` / `disabled`)
- [ ] OCR kann per Mandant deaktiviert werden (`ocr_enabled` in tenants-Tabelle) - [x] OCR kann per Mandant deaktiviert werden (`tenants.ocr_enabled BOOLEAN DEFAULT TRUE`)
- [ ] `archivmail ocr-reprocess` — OCR für alle oder einzelne Mandanten nachholen - [x] `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`, `poppler-utils`) - [x] Keine neue externe Service-Abhängigkeit bei Laufzeit — nur System-Pakete (`tesseract-ocr`, `tesseract-ocr-deu`, `poppler-utils`)
## Edge Cases ## Edge Cases
@@ -152,8 +152,93 @@ Keine Blockierung des Mail-Eingangs — Submit() ist non-blocking.
## Tech Design ## Tech Design
_Vollständig oben beschrieben_ _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` muss konsistent zu IndexSync sein**: `UpdateAttachmentText` muss exakt dasselbe Bit-Muster nutzen wie `IndexSync`, sonst werden neue Rows angelegt statt überschrieben. Der int64-Cast wird vom MySQL-Treiber sauber serialisiert.
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`.
## QA Test Results ## QA Test Results
_To be added_
### 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_global` enthält `attachment_text text indexed stored`
- `archivmail ocr-reprocess --status failed` reaktiviert die `failed`-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 ## Deployment
_To be added_
| 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)
```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"
```
+12 -3
View File
@@ -368,11 +368,20 @@ func (idx *manticoreIndex) Close() error {
// ── helpers ──────────────────────────────────────────────────────────────── // ── helpers ────────────────────────────────────────────────────────────────
// hashMailID returns a stable uint64 row ID derived from the mail's SHA-256 string ID. // hashMailID returns a stable signed int64 row ID derived from the mail's
func hashMailID(id string) uint64 { // SHA-256 string ID. Manticore's `id` column is bigint (signed int64); the
// mysql driver formats parameters as decimal strings, and Manticore rejects
// values outside the signed range with "number ... is out of range".
//
// We FNV-64a-hash the string and reinterpret the resulting uint64 as int64
// (verlustfreier Bit-Cast). All bit patterns map 1:1, so already-indexed
// documents stay reachable — values whose top bit was set previously
// became negative IDs; their bit pattern is unchanged, only the SQL
// rendering differs and now matches what Manticore expects.
func hashMailID(id string) int64 {
h := fnv.New64a() h := fnv.New64a()
h.Write([]byte(id)) h.Write([]byte(id))
return h.Sum64() return int64(h.Sum64())
} }
// manticoreTableName returns the RT table name for a given tenant. // manticoreTableName returns the RT table name for a given tenant.