Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
238 lines
8.2 KiB
PL/PgSQL
238 lines
8.2 KiB
PL/PgSQL
-- Phase 7: Suche.
|
|
--
|
|
-- Der bisherige Suchvektor enthielt nur, was jeder sehen darf (minClearance
|
|
-- = 0). Das war leck-sicher, aber unbrauchbar: ab Stufe 1 fand die Suche
|
|
-- nichts von dem, was den Fall ausmacht.
|
|
--
|
|
-- Stattdessen jetzt ein Vektor **je Freigabestufe**. Der zur Stufe N enthält
|
|
-- alles bis einschliesslich N. Eine Suche nimmt genau die Zeile zur Stufe des
|
|
-- Suchenden und kann deshalb nichts finden, was er nicht lesen darf.
|
|
|
|
CREATE TABLE "SearchIndex" (
|
|
"targetType" TEXT NOT NULL,
|
|
"targetId" TEXT NOT NULL,
|
|
"minClearance" INTEGER NOT NULL,
|
|
"vector" tsvector NOT NULL,
|
|
|
|
CONSTRAINT "SearchIndex_pkey" PRIMARY KEY ("targetType", "targetId", "minClearance")
|
|
);
|
|
|
|
CREATE INDEX "SearchIndex_vector_idx" ON "SearchIndex" USING GIN ("vector");
|
|
CREATE INDEX "SearchIndex_targetType_minClearance_idx"
|
|
ON "SearchIndex"("targetType", "minClearance");
|
|
|
|
CREATE TABLE "SavedView" (
|
|
"id" TEXT NOT NULL,
|
|
"userId" TEXT NOT NULL,
|
|
"name" TEXT NOT NULL,
|
|
"query" TEXT NOT NULL,
|
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "SavedView_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
CREATE INDEX "SavedView_userId_sortOrder_idx" ON "SavedView"("userId", "sortOrder");
|
|
|
|
ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_userId_fkey"
|
|
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- ===========================================================================
|
|
-- Schwärzungen stufenweise auflösen
|
|
--
|
|
-- `strip_redactions` entfernt jede markierte Stelle. Für den Vektor einer
|
|
-- Stufe muss aber stehen bleiben, was diese Stufe lesen darf - sonst findet
|
|
-- ein Leser der Stufe 4 einen Namen nicht, den er auf der Seite sieht.
|
|
-- ===========================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION strip_redactions_above(input text, max_level int)
|
|
RETURNS text AS $$
|
|
DECLARE
|
|
ergebnis text := coalesce(input, '');
|
|
stufe int;
|
|
BEGIN
|
|
-- Erst alles wegwerfen, was über der Stufe liegt. Die Stufe steht als eine
|
|
-- Ziffer im Marker, deshalb je Stufe ein Durchgang.
|
|
FOR stufe IN (max_level + 1)..5 LOOP
|
|
ergebnis := regexp_replace(
|
|
ergebnis,
|
|
'\[\[c' || stufe || '\]\].*?\[\[/c' || stufe || '\]\]',
|
|
' ',
|
|
'g'
|
|
);
|
|
END LOOP;
|
|
|
|
-- Was übrig ist, darf gelesen werden: nur die Marker selbst entfernen.
|
|
RETURN regexp_replace(ergebnis, '\[\[/?c[0-9]\]\]', ' ', 'g');
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
-- ===========================================================================
|
|
-- Fallakte: sechs Vektoren
|
|
--
|
|
-- Gewichtung wie in docs/08: A = Aktenzeichen und Titel, B = Abschnitte,
|
|
-- C = Verlaufseinträge.
|
|
-- ===========================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION refresh_case_search(target_id text) RETURNS void AS $$
|
|
DECLARE
|
|
stufe int;
|
|
kopf tsvector;
|
|
abschnitte tsvector;
|
|
eintraege tsvector;
|
|
BEGIN
|
|
DELETE FROM "SearchIndex" WHERE "targetType" = 'case' AND "targetId" = target_id;
|
|
|
|
SELECT setweight(to_tsvector('german', c."caseNumber" || ' ' || c."title"), 'A')
|
|
INTO kopf
|
|
FROM "Case" c WHERE c."id" = target_id;
|
|
|
|
IF kopf IS NULL THEN
|
|
RETURN; -- Der Fall ist weg, dann gehört auch kein Index dazu.
|
|
END IF;
|
|
|
|
FOR stufe IN 0..5 LOOP
|
|
SELECT coalesce(
|
|
to_tsvector('german', string_agg(strip_redactions_above(s."value" #>> '{}', stufe), ' ')),
|
|
''::tsvector
|
|
) INTO abschnitte
|
|
FROM "CaseSection" s
|
|
WHERE s."caseId" = target_id AND s."minClearance" <= stufe;
|
|
|
|
SELECT coalesce(
|
|
to_tsvector('german', string_agg(strip_redactions_above(e."body", stufe), ' ')),
|
|
''::tsvector
|
|
) INTO eintraege
|
|
FROM "CaseEntry" e
|
|
WHERE e."caseId" = target_id
|
|
AND e."minClearance" <= stufe
|
|
AND e."retractedAt" IS NULL;
|
|
|
|
INSERT INTO "SearchIndex" ("targetType", "targetId", "minClearance", "vector")
|
|
VALUES (
|
|
'case',
|
|
target_id,
|
|
stufe,
|
|
kopf || setweight(abschnitte, 'B') || setweight(eintraege, 'C')
|
|
);
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE OR REPLACE FUNCTION refresh_document_search(target_id text) RETURNS void AS $$
|
|
DECLARE
|
|
stufe int;
|
|
kopf tsvector;
|
|
abschnitte tsvector;
|
|
BEGIN
|
|
DELETE FROM "SearchIndex" WHERE "targetType" = 'document' AND "targetId" = target_id;
|
|
|
|
SELECT setweight(to_tsvector('german', d."documentNumber" || ' ' || d."title"), 'A')
|
|
INTO kopf
|
|
FROM "Document" d WHERE d."id" = target_id;
|
|
|
|
IF kopf IS NULL THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
FOR stufe IN 0..5 LOOP
|
|
SELECT coalesce(
|
|
to_tsvector('german', string_agg(strip_redactions_above(s."value" #>> '{}', stufe), ' ')),
|
|
''::tsvector
|
|
) INTO abschnitte
|
|
FROM "DocumentSection" s
|
|
WHERE s."documentId" = target_id AND s."minClearance" <= stufe;
|
|
|
|
INSERT INTO "SearchIndex" ("targetType", "targetId", "minClearance", "vector")
|
|
VALUES ('document', target_id, stufe, kopf || setweight(abschnitte, 'B'));
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Die Personalakte hat keine gestuften Inhalte; ihre Kopfdaten stehen für
|
|
-- jede Stufe gleich. Die Zeile wird trotzdem sechsmal geschrieben, damit die
|
|
-- Abfrage über alle drei Arten dieselbe bleibt.
|
|
CREATE OR REPLACE FUNCTION refresh_personnel_search(target_id text) RETURNS void AS $$
|
|
DECLARE
|
|
stufe int;
|
|
vec tsvector;
|
|
BEGIN
|
|
DELETE FROM "SearchIndex" WHERE "targetType" = 'personnel' AND "targetId" = target_id;
|
|
|
|
SELECT setweight(to_tsvector('german', p."displayName"), 'A')
|
|
|| setweight(to_tsvector('german',
|
|
coalesce(p."rank", '') || ' ' || coalesce(p."department", '') || ' ' ||
|
|
coalesce(array_to_string(p."statusTags", ' '), '')), 'B')
|
|
|| setweight(to_tsvector('german', coalesce(p."note", '')), 'C')
|
|
INTO vec
|
|
FROM "PersonnelFile" p WHERE p."id" = target_id;
|
|
|
|
IF vec IS NULL THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
FOR stufe IN 0..5 LOOP
|
|
INSERT INTO "SearchIndex" ("targetType", "targetId", "minClearance", "vector")
|
|
VALUES ('personnel', target_id, stufe, vec);
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- ===========================================================================
|
|
-- Auslöser
|
|
--
|
|
-- Wie beim alten Vektor: auch bei UPDATE, sonst findet die Suche nach einer
|
|
-- nachträglichen Änderung den alten Text.
|
|
-- ===========================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION search_index_trigger() RETURNS trigger AS $$
|
|
DECLARE
|
|
ziel text;
|
|
BEGIN
|
|
IF TG_TABLE_NAME = 'Case' THEN
|
|
PERFORM refresh_case_search(NEW."id");
|
|
ELSIF TG_TABLE_NAME IN ('CaseSection', 'CaseEntry') THEN
|
|
ziel := CASE WHEN TG_OP = 'DELETE' THEN OLD."caseId" ELSE NEW."caseId" END;
|
|
IF ziel IS NOT NULL THEN PERFORM refresh_case_search(ziel); END IF;
|
|
ELSIF TG_TABLE_NAME = 'Document' THEN
|
|
PERFORM refresh_document_search(NEW."id");
|
|
ELSIF TG_TABLE_NAME = 'DocumentSection' THEN
|
|
ziel := CASE WHEN TG_OP = 'DELETE' THEN OLD."documentId" ELSE NEW."documentId" END;
|
|
IF ziel IS NOT NULL THEN PERFORM refresh_document_search(ziel); END IF;
|
|
ELSIF TG_TABLE_NAME = 'PersonnelFile' THEN
|
|
PERFORM refresh_personnel_search(NEW."id");
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER search_on_case
|
|
AFTER INSERT OR UPDATE OF "title", "caseNumber" ON "Case"
|
|
FOR EACH ROW EXECUTE FUNCTION search_index_trigger();
|
|
|
|
CREATE TRIGGER search_on_case_section
|
|
AFTER INSERT OR UPDATE OR DELETE ON "CaseSection"
|
|
FOR EACH ROW EXECUTE FUNCTION search_index_trigger();
|
|
|
|
CREATE TRIGGER search_on_case_entry
|
|
AFTER INSERT OR UPDATE ON "CaseEntry"
|
|
FOR EACH ROW EXECUTE FUNCTION search_index_trigger();
|
|
|
|
CREATE TRIGGER search_on_document
|
|
AFTER INSERT OR UPDATE OF "title", "documentNumber" ON "Document"
|
|
FOR EACH ROW EXECUTE FUNCTION search_index_trigger();
|
|
|
|
CREATE TRIGGER search_on_document_section
|
|
AFTER INSERT OR UPDATE OR DELETE ON "DocumentSection"
|
|
FOR EACH ROW EXECUTE FUNCTION search_index_trigger();
|
|
|
|
CREATE TRIGGER search_on_personnel
|
|
AFTER INSERT OR UPDATE OF "displayName", "rank", "department", "statusTags", "note"
|
|
ON "PersonnelFile"
|
|
FOR EACH ROW EXECUTE FUNCTION search_index_trigger();
|
|
|
|
-- Bestand einmal aufbauen.
|
|
SELECT refresh_case_search(c."id") FROM "Case" c;
|
|
SELECT refresh_document_search(d."id") FROM "Document" d;
|
|
SELECT refresh_personnel_search(p."id") FROM "PersonnelFile" p;
|