Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
3.8 KiB
SQL
64 lines
3.8 KiB
SQL
-- Volltext über den *Inhalt* – die zweite Hälfte der Suche.
|
||
--
|
||
-- Migration 000013 machte Datei*namen* durchsuchbar. Das reicht, solange
|
||
-- Dateien sprechende Namen haben. Ein Scanner nennt seine Ablage aber
|
||
-- "scan_0042.pdf", und genau die Belege sollen auffindbar werden.
|
||
|
||
-- ── Der erkannte Text ─────────────────────────────────────────────────────────
|
||
-- Eigene Tabelle statt einer Spalte an files: Der Text ist um Größenordnungen
|
||
-- länger als der Rest der Zeile, und files wird bei jedem Auflisten gelesen.
|
||
-- Postgres lagert lange Werte zwar aus (TOAST), aber die tsvector-Spalte und
|
||
-- ihr GIN-Index gehörten dann trotzdem an eine Tabelle, die ständig in
|
||
-- Bewegung ist.
|
||
CREATE TABLE file_text (
|
||
file_id UUID PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
|
||
content TEXT NOT NULL,
|
||
-- Woher der Text stammt. 'layer' = das PDF hatte bereits eine Textebene
|
||
-- (pdftotext, verlustfrei); 'ocr' = aus dem Bild erkannt, kann Fehler
|
||
-- enthalten. Der Unterschied ist für die Fehlersuche wichtig: Findet die
|
||
-- Suche ein Wort nicht, sagt die Herkunft, ob OCR es verlesen hat.
|
||
source TEXT NOT NULL CHECK (source IN ('layer', 'ocr')),
|
||
-- Die Sprachen, mit denen erkannt wurde ("deu+eng"). Bei 'layer' leer.
|
||
lang TEXT NOT NULL DEFAULT '',
|
||
-- Zeichenzahl des Originals vor dem Kürzen – siehe maxTextBytes im Worker.
|
||
chars INTEGER NOT NULL DEFAULT 0,
|
||
pages INTEGER NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
||
-- Berechnete Spalte statt eines Triggers: Sie kann nie mit dem Inhalt
|
||
-- auseinanderlaufen, weil Postgres sie bei jedem Schreiben selbst füllt.
|
||
--
|
||
-- 'german' fest verdrahtet, nicht 'simple': Ohne Stammformen fände
|
||
-- "Rechnung" die "Rechnungen" nicht. Deutsch ist die Sprache der Belege,
|
||
-- um die es hier geht; englische Wörter darin verlieren nur ihre
|
||
-- Stammformbildung, sie bleiben wörtlich auffindbar. Eine Änderung wäre
|
||
-- eine Migration – der Preis dafür, dass der Ausdruck IMMUTABLE sein muss.
|
||
tsv tsvector GENERATED ALWAYS AS (to_tsvector('german', content)) STORED
|
||
);
|
||
|
||
CREATE INDEX idx_file_text_tsv ON file_text USING gin (tsv);
|
||
|
||
-- ── Die Warteschlange ─────────────────────────────────────────────────────────
|
||
-- Warum eine Tabelle und nicht einfach ein Goroutine-Aufruf wie bei den
|
||
-- Vorschaubildern: OCR dauert Sekunden bis Minuten. Startet der Server in
|
||
-- dieser Zeit neu – Update, Absturz, Neustart des Wirts –, ist ein Auftrag im
|
||
-- Speicher verloren, und die Datei bleibt für immer unauffindbar, ohne dass es
|
||
-- jemand merkt. Bei einem Vorschaubild fällt das auf; bei fehlendem Text nicht.
|
||
CREATE TABLE ocr_jobs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
-- Ein Auftrag je Datei. Wird dieselbe Datei erneut hochgeladen, ersetzt der
|
||
-- neue Auftrag den alten (ON CONFLICT), statt sich daneben zu stapeln.
|
||
file_id UUID NOT NULL UNIQUE REFERENCES files(id) ON DELETE CASCADE,
|
||
status TEXT NOT NULL DEFAULT 'pending'
|
||
CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
||
attempts INTEGER NOT NULL DEFAULT 0,
|
||
last_error TEXT NOT NULL DEFAULT '',
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Der Arbeiter fragt nur nach offenen Aufträgen. Ein Teilindex bleibt klein,
|
||
-- auch wenn sich zehntausend erledigte Zeilen ansammeln.
|
||
CREATE INDEX idx_ocr_jobs_pending ON ocr_jobs(created_at)
|
||
WHERE status IN ('pending', 'running');
|