Closes the search half of the unified Fristenrechner. Phase D (concept-card
UI on /tools/fristenrechner) follows in a subsequent shift.
Migration 047:
- Seed the missing `wiedereinsetzung` concept and re-point the four
Wiedereinsetzung trigger_events (200..203) at it. PR-7 referenced
the slug `re-establishment-of-rights` but never seeded the concept,
so the four cross-cutting triggers were dropping out of any concept-
JOINing query. Per m's slug rule (Q1: shared cross-cutting concepts
use DE slug because German term dominates HLC vocabulary).
- Create paliad.deadline_search materialised view: UNION ALL of
(deadline_rules joined to deadline_concepts) and (trigger_events
joined to deadline_concepts via slug). Trigram GIN indexes on
legal_source / concept_name_de / concept_name_en / rule_name_de /
rule_name_en / rule_code; gin (concept_aliases) for array
containment; UNIQUE INDEX on a synthetic row_key so refresh can
run CONCURRENTLY.
Refresh strategy: data only mutates via migration files at server
startup, so no AFTER triggers and no pg_cron — main.go calls
services.RefreshSearchView right after db.ApplyMigrations. CONCURRENTLY
keeps reads online and stays well under 100 ms at < 1k rows.
Service `internal/services/deadline_search_service.go`:
- Two-query pipeline per request: (1) rank concept_ids by
GREATEST(similarity()) across name / aliases / legal_source / rule_code
plus a 0.2 alias-hit boost; (2) load all matview rows for the top-N
concepts and assemble per-pill JSON.
- normalizeQuery strips legal-prefix noise (`§`, `Art.`, `Section`,
`Rule `) so users typing `§ 82` find DE.PatG.82.1 even though the
structured legal_source column doesn't carry the prefix.
- FormatLegalSourceDisplay renders structured codes back to the
pleading form HLC users expect:
UPC.RoP.23.1 → "UPC RoP R.23(1)"
DE.PatG.82.1 → "PatG §82(1)"
EU.EPÜ.108 → "EPÜ Art.108"
EU.EPC-R.79.1 → "EPC R.79(1)"
EU.RPBA.12.1.c → "RPBA Art.12(1)(c)"
- Drill URLs route per kind: rule pills → ?proc=…&focus=…, trigger
pills → ?mode=event&triggerId=…
Handler `GET /api/tools/fristenrechner/search?q=&party=&proc=&source=&limit=`:
- Returns the JSON shape from design §6.1 (cards-with-pills).
- 503 with friendly DE message when DATABASE_URL is unset, mirroring
the other Fristenrechner endpoints.
- Empty q returns an empty cards array (browse surface is Phase D).
Tests:
- Pure-Go: TestFormatLegalSourceDisplay (12 cases across all known
prefixes) + TestNormalizeQuery (8 cases).
- Integration (skipped without TEST_DATABASE_URL): golden table
pinning the design's binding queries — Klageerwiderung returns the
statement-of-defence card with UPC.RoP.23.1, DE.ZPO.276.1,
DE.PatG.82.1, EU.EPC-R.79.1, DE.PatG.59.3 pills; "RoP 23" returns
the same card; "§ 82" → normalized "82" → BPatG hit; Wiedereinsetzung
returns one card with exactly 4 trigger pills (ids 200..203);
party / source filters narrow as expected; limit cap honoured.
- SQL semantics validated against live data via supabase MCP using a
CTE-inlined matview definition with the slug fix simulated; results
match the golden table.
Per design doc `docs/plans/unified-fristenrechner.md` §4.6 (matview
shape) + §6 (search ranking + API).
165 lines
6.9 KiB
SQL
165 lines
6.9 KiB
SQL
-- t-paliad-131 Phase C: search backend.
|
||
--
|
||
-- Two artefacts in one migration:
|
||
--
|
||
-- 1. Fix a dangling concept slug from PR-7 (Phase B6). The four
|
||
-- Wiedereinsetzung trigger_events were inserted referencing concept
|
||
-- slug `re-establishment-of-rights`, but the concept was never
|
||
-- seeded — the slug was English-style while the concept-naming
|
||
-- convention (m's Q1, design §11) requires DE slugs for shared
|
||
-- cross-cutting concepts where the German term dominates HLC
|
||
-- vocabulary. Without the fix, the four Wiedereinsetzung rows
|
||
-- drop out of the unified search view's INNER JOIN and the
|
||
-- golden test `search "Wiedereinsetzung" → 1 concept × 4 pills`
|
||
-- cannot pass.
|
||
--
|
||
-- 2. Create paliad.deadline_search — a materialised view that
|
||
-- flattens (deadline_concepts × deadline_rules) and (deadline_concepts
|
||
-- × trigger_events via slug) into one searchable shape. Indexed
|
||
-- with pg_trgm GIN on the columns the search query probes. See
|
||
-- design doc §4.6 + §6.
|
||
--
|
||
-- Refresh strategy: data only mutates via migration files at server
|
||
-- startup, so a refresh after migrations apply (in main.go) is enough.
|
||
-- No AFTER triggers, no pg_cron — keeps the migration pure SQL.
|
||
|
||
-- ============================================================================
|
||
-- 1. Wiedereinsetzung concept (fixes the PR-7 dangling slug)
|
||
-- ============================================================================
|
||
|
||
INSERT INTO paliad.deadline_concepts (slug, name_de, name_en, description, aliases, party, category, sort_order)
|
||
VALUES (
|
||
'wiedereinsetzung',
|
||
'Wiedereinsetzung in den vorigen Stand',
|
||
'Re-establishment of Rights',
|
||
'Antrag auf Wiedereinsetzung in den vorigen Stand bei unverschuldeter Versäumung einer gesetzlichen Frist. Die Frist beträgt zwei Monate ab Wegfall des Hindernisses (PatG §123, ZPO §233, EPÜ Art.122).',
|
||
ARRAY['Wiedereinsetzung', 'Wiedereinsetzungsantrag', 'Re-establishment of Rights', 'Re-establishment', 'Antrag auf Wiedereinsetzung', 'Restitutio', 'restitutio in integrum'],
|
||
'both',
|
||
'submission',
|
||
35
|
||
);
|
||
|
||
UPDATE paliad.trigger_events
|
||
SET concept_id = 'wiedereinsetzung'
|
||
WHERE id IN (200, 201, 202, 203)
|
||
AND concept_id = 're-establishment-of-rights';
|
||
|
||
-- ============================================================================
|
||
-- 2. Materialised search view + indexes
|
||
-- ============================================================================
|
||
--
|
||
-- One row per (concept × context). Two contexts:
|
||
-- kind='rule' — concept-linked deadline_rule under an active
|
||
-- fristenrechner-category proceeding_type
|
||
-- kind='trigger' — concept-linked trigger_event (cross-cutting; no
|
||
-- proceeding context, no rule_id, no duration)
|
||
--
|
||
-- Search ranks rows per concept_id, then per concept fans out to all its
|
||
-- rule + trigger pills. row_key is a synthetic UNIQUE column required for
|
||
-- REFRESH MATERIALIZED VIEW CONCURRENTLY.
|
||
CREATE MATERIALIZED VIEW paliad.deadline_search AS
|
||
SELECT
|
||
'rule'::text AS kind,
|
||
'r:' || dr.id::text AS row_key,
|
||
dc.id AS concept_id,
|
||
dc.slug AS concept_slug,
|
||
dc.name_de AS concept_name_de,
|
||
dc.name_en AS concept_name_en,
|
||
dc.description AS concept_description,
|
||
dc.aliases AS concept_aliases,
|
||
dc.party AS concept_party,
|
||
dc.category AS concept_category,
|
||
dc.sort_order AS concept_sort_order,
|
||
dr.id AS rule_id,
|
||
NULL::bigint AS trigger_event_id,
|
||
pt.code AS proceeding_code,
|
||
pt.name AS proceeding_name_de,
|
||
pt.name_en AS proceeding_name_en,
|
||
pt.jurisdiction AS jurisdiction,
|
||
dr.code AS rule_local_code,
|
||
dr.name AS rule_name_de,
|
||
dr.name_en AS rule_name_en,
|
||
dr.legal_source AS legal_source,
|
||
dr.rule_code AS rule_code,
|
||
dr.duration_value,
|
||
dr.duration_unit,
|
||
dr.timing,
|
||
COALESCE(dr.primary_party, dc.party) AS effective_party
|
||
FROM paliad.deadline_rules dr
|
||
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
|
||
JOIN paliad.deadline_concepts dc ON dc.id = dr.concept_id
|
||
WHERE dr.is_active
|
||
AND pt.is_active
|
||
AND pt.category = 'fristenrechner'
|
||
|
||
UNION ALL
|
||
|
||
SELECT
|
||
'trigger'::text,
|
||
't:' || te.id::text,
|
||
dc.id,
|
||
dc.slug,
|
||
dc.name_de,
|
||
dc.name_en,
|
||
dc.description,
|
||
dc.aliases,
|
||
dc.party,
|
||
dc.category,
|
||
dc.sort_order,
|
||
NULL::uuid,
|
||
te.id,
|
||
NULL::text,
|
||
NULL::text,
|
||
NULL::text,
|
||
'cross-cutting'::text,
|
||
te.code,
|
||
te.name_de,
|
||
te.name,
|
||
NULL::text,
|
||
NULL::text,
|
||
NULL::int,
|
||
NULL::text,
|
||
NULL::text,
|
||
dc.party
|
||
FROM paliad.trigger_events te
|
||
JOIN paliad.deadline_concepts dc ON dc.slug = te.concept_id
|
||
WHERE te.is_active;
|
||
|
||
-- Required for REFRESH MATERIALIZED VIEW CONCURRENTLY.
|
||
CREATE UNIQUE INDEX deadline_search_row_key
|
||
ON paliad.deadline_search (row_key);
|
||
|
||
-- Btree for filter narrowing.
|
||
CREATE INDEX deadline_search_concept_id
|
||
ON paliad.deadline_search (concept_id);
|
||
CREATE INDEX deadline_search_proc_code
|
||
ON paliad.deadline_search (proceeding_code);
|
||
CREATE INDEX deadline_search_legal_source
|
||
ON paliad.deadline_search (legal_source);
|
||
CREATE INDEX deadline_search_effective_party
|
||
ON paliad.deadline_search (effective_party);
|
||
|
||
-- Trigram GIN for the LIKE / similarity()-driven WHERE clause.
|
||
CREATE INDEX deadline_search_legal_source_trgm
|
||
ON paliad.deadline_search USING gin (legal_source gin_trgm_ops);
|
||
CREATE INDEX deadline_search_concept_de_trgm
|
||
ON paliad.deadline_search USING gin (concept_name_de gin_trgm_ops);
|
||
CREATE INDEX deadline_search_concept_en_trgm
|
||
ON paliad.deadline_search USING gin (concept_name_en gin_trgm_ops);
|
||
CREATE INDEX deadline_search_rule_de_trgm
|
||
ON paliad.deadline_search USING gin (rule_name_de gin_trgm_ops);
|
||
CREATE INDEX deadline_search_rule_en_trgm
|
||
ON paliad.deadline_search USING gin (rule_name_en gin_trgm_ops);
|
||
CREATE INDEX deadline_search_rule_code_trgm
|
||
ON paliad.deadline_search USING gin (rule_code gin_trgm_ops);
|
||
|
||
-- Array containment for `aliases @> ARRAY[…]`.
|
||
CREATE INDEX deadline_search_aliases
|
||
ON paliad.deadline_search USING gin (concept_aliases);
|
||
|
||
COMMENT ON MATERIALIZED VIEW paliad.deadline_search IS
|
||
'Phase C unified search backend. Refreshed CONCURRENTLY by main.go '
|
||
'after migrations apply at server startup. Source data: '
|
||
'deadline_rules ∪ trigger_events, joined to deadline_concepts. '
|
||
'See docs/plans/unified-fristenrechner.md §4.6 + §6.';
|