SRC-10: spracherkennung-ocr-qualitaetsbewertung

Spracherkennung für OCR-Texte und Qualitätsbewertung (Konfidenzwert), um
schlechte OCR-Ergebnisse kenntlich zu machen. Letztes Ticket vor QA-03.

- ocr/language.go: RecognizeWithLanguageAndConfidence erkennt ein Bild
  einzeln je Kandidatensprache (deu/eng) im Tesseract-TSV-Modus — die
  Sprache mit höherem Konfidenzwert gewinnt, derselbe Lauf liefert den
  Konfidenzwert direkt mit.
- search: neue Felder ocr_language/ocr_confidence (Migrationen 0006/0007,
  gleiches ALTER-Muster wie SRC-05), in Document/Result gespiegelt.
  Client.AttachmentsBelowConfidence filtert gezielt auf niedrige
  Konfidenz, schließt Dokumente ohne OCR-Anhang aus.
- Regressionsbug gefunden und behoben: reindex.go (SRC-09) kannte die
  neuen OCR-Spalten nicht, Reindex wäre mit "unknown column"
  fehlgeschlagen.

Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-10-PRUEFPROTOKOLL.md):
1. TestRecognizeWithLanguageAndConfidence_MultilingualCorpus: deutsches
   und englisches Testbild real korrekt als deu/eng erkannt.
2. TestRecognizeWithLanguageAndConfidence_DegradedImageLowersConfidence:
   künstliche Verschlechterung senkt Konfidenz real von 91,76 auf 28,21.
3. TestAttachmentsBelowConfidence_QueryReturnsExpectedResults: Abfrage
   unterhalb Schwelle liefert real genau die erwarteten 2 von 4 Treffern.

Kein Umbau: Search/Facets/SearchWithFilters/Index/Delete-Verhalten sonst
unverändert, dedup/indexworker/storage/crypto/encstorage/savedsearch
unverändert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
sysops
2026-08-31 22:16:26 +02:00
co-authored by Claude Sonnet 5
parent bd37de529f
commit 1a246abdb3
10 changed files with 427 additions and 6 deletions
+52
View File
@@ -142,6 +142,8 @@ func (c *Client) SearchWithFilters(ctx context.Context, tenantSlug, queryText st
Subject: hit.Source.Subject,
Score: hit.Score,
SentAtUnixEpoch: hit.Source.SentAtUnixEpoch,
OCRLanguage: hit.Source.OCRLanguage,
OCRConfidence: hit.Source.OCRConfidence,
})
}
return results, nil
@@ -230,6 +232,56 @@ func (c *Client) Facets(ctx context.Context, tenantSlug, queryText string, filte
return result, nil
}
// AttachmentsBelowConfidence liefert alle Dokumente eines Mandanten, deren
// OCR-Konfidenzwert UNTER threshold liegt (SRC-10 Akzeptanzkriterium 3:
// gezielt für manuelle Nachbearbeitung auffindbar). Dokumente ohne
// OCR-Anhang (ocr_confidence bleibt 0) tauchen hier NICHT auf — 0 ist
// "kein Wert", nicht "schlechtester Wert" (siehe Document-Feldkommentar) —
// daher zusätzlicher Filter ocr_confidence > 0.
func (c *Client) AttachmentsBelowConfidence(ctx context.Context, tenantSlug string, threshold float64) ([]Result, error) {
payload := map[string]any{
"index": IndexName,
"query": map[string]any{
"bool": map[string]any{
"must": []map[string]any{
{"equals": map[string]any{FieldTenantSlug: tenantSlug}},
{"range": map[string]any{FieldOCRConfidence: map[string]any{"gt": 0}}},
{"range": map[string]any{FieldOCRConfidence: map[string]any{"lt": threshold}}},
},
},
},
"sort": []map[string]any{{FieldOCRConfidence: "asc"}},
"limit": searchResultLimit,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("search: konfidenzanfrage serialisieren: %w", err)
}
respBody, err := c.doSearchWithSwapRetry(ctx, body)
if err != nil {
return nil, err
}
var parsed searchResponse
if err := json.Unmarshal(respBody, &parsed); err != nil {
return nil, fmt.Errorf("search: antwort parsen: %w", err)
}
results := make([]Result, 0, len(parsed.Hits.Hits))
for _, hit := range parsed.Hits.Hits {
results = append(results, Result{
MessageID: hit.Source.MessageID,
Subject: hit.Source.Subject,
Score: hit.Score,
SentAtUnixEpoch: hit.Source.SentAtUnixEpoch,
OCRLanguage: hit.Source.OCRLanguage,
OCRConfidence: hit.Source.OCRConfidence,
})
}
return results, nil
}
type facetResponse struct {
Aggregations map[string]struct {
Buckets []struct {