7.1 KiB
Feature Spec: Kunnskapsgraf og Relasjoner (Logseq-modell)
Filsti: docs/features/kunnskapsgraf_og_relasjoner.md
1. Konsept
Inspirert av verktøy som Logseq og Obsidian, bygger vi databasen som en toveis-lenket graf. Målet er å skape "serendipity" (lykketreff) i research-fasen ved å synliggjøre uventede forbindelser. Hvis Aktør A og Aktør B begge er nevnt i samme chat-tråd eller knyttet til samme Tema over tid, skal systemet kunne visualisere denne røde tråden for programlederne.
2. Arkitektur og Teknologivalg
Vi unngår tunge, dedikerte grafdatabaser (som Neo4j) for å holde infrastrukturen og ressursbruken (RAM) minimal.
- Valgt teknologi: Vanilla PostgreSQL.
- Mekanisme: En "Nodes and Edges" (Noder og Kanter) tabellstruktur kombinert med Recursive CTEs (Common Table Expressions) i SQL for å traversere grafen. Dette er mer enn raskt nok for redaksjonelle datamengder (100k+ noder).
3. Datastruktur
3.1 Supertabell: nodes
Alle entiteter i systemet arver sin UUID fra én sentral tabell. Dette gir ekte Foreign Key-integritet på graph_edges uten applikasjonslogikk-hacks.
CREATE TYPE node_type AS ENUM (
'tema', 'aktør', 'faktoide', 'episode', 'segment', 'melding'
);
CREATE TABLE nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_type node_type NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
3.2 Detailtabeller
Hver nodetype har sin egen tabell med FK til nodes. Eksempler:
CREATE TABLE actors (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT -- 'person', 'organisasjon', etc.
);
CREATE TABLE topics (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
name TEXT NOT NULL
);
CREATE TABLE episodes (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
title TEXT NOT NULL,
published_at TIMESTAMPTZ,
guid TEXT UNIQUE NOT NULL -- RSS <guid>, aldri endres
);
CREATE TABLE segments (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
episode_id UUID NOT NULL REFERENCES episodes(id) ON DELETE CASCADE,
start_time INTERVAL NOT NULL,
end_time INTERVAL NOT NULL,
transcript TEXT, -- Segmentets transkripsjon
CONSTRAINT valid_timerange CHECK (end_time > start_time)
);
CREATE INDEX idx_segments_episode ON segments(episode_id);
CREATE INDEX idx_segments_transcript_fts ON segments USING GIN (to_tsvector('norwegian', transcript));
3.3 Kantene: graph_edges
All kobling skjer i én sentral tabell med ekte FK-integritet:
CREATE TABLE graph_edges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
relation_type TEXT NOT NULL, -- 'MENTIONS', 'CONTRADICTS', 'WORKS_FOR', 'PART_OF', 'DISCUSSED_IN'
context_id UUID REFERENCES nodes(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT no_self_reference CHECK (source_id != target_id)
);
CREATE INDEX idx_edges_source ON graph_edges(source_id);
CREATE INDEX idx_edges_target ON graph_edges(target_id);
CREATE INDEX idx_edges_relation ON graph_edges(relation_type);
4. Segmenter og Transkripsjoner
4.1 Segment som grafnode
Episoder deles i segmenter — tidsavgrensede deler med egen transkripsjon. Hvert segment er en node i grafen og kan kobles til Temaer, Aktører og Faktoider. Dette muliggjør presise oppslag som: "I Episode 42, fra 14:23 til 21:07, diskuterte dere Skolepolitikk og sa følgende..."
Episode 42 (node: episode)
├── Segment 00:00-14:22 (node: segment) ──DISCUSSED_IN──► Tema: Mediepolitikk
├── Segment 14:23-21:07 (node: segment) ──DISCUSSED_IN──► Tema: Skolepolitikk
│ ──MENTIONS──────► Aktør: Støre
└── Segment 21:08-45:00 (node: segment) ──DISCUSSED_IN──► Tema: Kommuneøkonomi
Kapitler i RSS-feeden genereres fra segmentene, men er et eget konsern (se docs/features/podcastfabrikken.md).
4.2 Transkripsjoner: Git som master, PG som søkeindeks
Transkripsjoner lever i to steder med klart eierskap:
| Sted | Rolle | Format |
|---|---|---|
| Git (Forgejo) | Kilde til sannhet. Redigerbar, sporbar, diffbar | Markdown med tidsstempler |
| PostgreSQL | Søkeindeks. Full-text search, koblet til grafen | Segmentert i segments-tabellen |
Flyt:
Whisper → Git (rå transkripsjon med tidsstempler)
→ Redaksjonen korrigerer manuelt ved behov
→ Push til Forgejo
→ Forgejo webhook trigger 'transcript_reimport'-jobb i jobbkøen
→ Rust-worker parser filen, splitter i segmenter
→ DELETE + INSERT i én PG-transaksjon (idempotent reimport)
→ Grafkoblinger bevares (segment-UUID deterministisk fra episode-UUID + tidsstempel)
Deterministisk UUID for segmenter: UUID = uuid_v5(episode_uuid, start_time_ms). Dette sikrer at samme segment alltid får samme UUID, selv ved reimport. Grafkoblinger som peker på segmentet overlever dermed en full reimport.
5. Arbeidsflyt: Hvordan grafen vokser
Grafen bygger seg opp organisk gjennom daglig bruk av Sidelinja-suiten:
- Chat & Notater: En bruker skriver: "Apropos #Hans_Petter_Sjøli, hva var greia med #Arbeiderpartiet?"
- Parsing (Svelte/Rust): Systemet fanger opp de to
#-taggene (som allerede har UUIDs iAktør-tabellen). - Edge Creation: SvelteKit server-side oppretter automatisk to nye oppføringer i
graph_edges-tabellen:- [Melding UUID] ->
MENTIONS-> [Sjøli UUID] - [Melding UUID] ->
MENTIONS-> [Arbeiderpartiet UUID]
- [Melding UUID] ->
- Indirekte relasjon: Fordi begge aktørene nå deler samme
context_id(meldingen), vet Kunnskapsgrafen at det finnes en tematisk kobling mellom Sjøli og Ap. - Publisering: Når en episode publiseres, kobles segmentene automatisk til relevante Temaer og Aktører basert på AI-analyse av transkripsjonen.
6. Instruks for Claude Code
nodes-tabellen er obligatorisk. Opprett alltid en rad inodesfør du inserter i en detailtabell. Bruk en hjelpefunksjon som gjør begge i én transaksjon.- Graf-spørringer: Bruk
WITH RECURSIVEi PostgreSQL når du bygger endepunkter som skal hente ut "Linked Mentions" eller nettverket rundt en spesifikk Aktør opp til 2-3 ledd ut. - Fremtidssikring for UI: Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (som D3.js eller Vis.js) i Svelte-frontenden. Formatet bør være
{ "nodes": [...], "edges": [...] }. - Transkripsjon-reimport: Workeren må være idempotent. Bruk
uuid_v5(episode_uuid, start_time_ms)for deterministiske segment-UUIDs. Slett og gjenopprett segmenter i én transaksjon, men ikke slett edges som peker til segmentene — de overlever fordi UUID-en er stabil. - Full-text search: Bruk
to_tsvector('norwegian', transcript)for norsk språkstøtte i søk.