4.9 KiB
Database schema
The definitive version is _postgres/schema.sql, which is
verified against the production database. This page explains it.
Column names are still Swedish, inherited from the original Riksdag-only project. They are only names — nothing in the application derives meaning from them — but they are an obstacle to reading the code, so a rename to neutral English is planned. Until then, this is the translation.
Core tables
talks — speeches in the chamber
One row per speech. The primary key id comes from the source; note the separate
dok_id, which identifies the protocol document the speech appeared in.
| Column | Meaning |
|---|---|
id |
Primary key, from the source record's id field |
anforande_id |
The source's own UUID, preserved from an earlier key migration |
anforandetext |
The speech text |
avsnittsrubrik |
Heading of the agenda item |
anforande_nummer |
Position within the debate |
kammaraktivitet |
Activity type code; see activity_types in parliament.yaml |
talare |
Speaker name as printed. Not the presiding officer — "speaker" in the plain sense |
parti |
Party code |
intressent_id |
Person id, references people |
datum, dok_datum, year, period |
Date, raw source datetime, calendar year, session start year |
dok_id, rel_dok_id, dok_nummer, hangar_id |
Source document references |
titel |
Debate title |
debate |
Debate grouping key, "{date}:{index}" |
replik |
Whether this is a rebuttal (a short right-of-reply intervention) |
summary, tags, arguments |
LLM-derived |
summary_embedding |
Embedding of summary, for debate-level semantic search |
search_vector |
Full-text vector, maintained by trigger |
motions — member-submitted documents
The Swedish motion is a formal proposal submitted by members. Each contains one or more numbered yrkanden — operative demands — which is what people actually search and cite.
| Column | Meaning |
|---|---|
dok_id |
Primary key |
rm |
Session label, e.g. "2022/23" (Swedish riksmöte) |
beteckning |
Number within the session |
subtyp |
Whether submitted by an individual, a committee group, or a whole party |
organ |
Committee the document was referred to |
titel, undertitel, text |
Content |
forslag_text |
The proposal texts concatenated — high-signal, weighted separately in search |
parties, author_names |
Denormalised authorship, in signing order |
forslag, bilagor |
Raw proposal and attachment records from the source |
has_text |
False for documents that exist only as scanned PDFs |
motion_yrkanden — individual proposals within a document
| Column | Meaning |
|---|---|
lydelse |
The proposal's wording |
utskottet |
The committee's recommendation |
kammaren |
The chamber's decision, e.g. Bifall (approved) / Avslag (rejected) |
behandlas_i |
Which report handled it |
Values are stored exactly as published and never translated. decisions: in
parliament.yaml glosses them so prompts can explain them.
people — the register of members
intressent_id is the source's person id and the join key used everywhere.
valkrets is the electoral district; its meaning varies by country — a single-member
constituency in the UK, a multi-member district in Sweden — and nothing in the code
assumes either.
chunks, motion_chunks — passages with embeddings
Text split into passages, each with a vector(N) embedding under an HNSW cosine
index. N must match embeddings.dimension in parliament.yaml; the application
checks this at startup because a mismatch otherwise fails deep inside pgvector.
debates — aggregated debates
One row per debate, keyed "{date}:{index}", holding a synthesised summary and the
list of speeches it covers.
Application tables
Already country-neutral: users and auth_tokens (zero-knowledge auth),
chat_sessions and chat_snapshots (encrypted chat storage and shared snapshots),
research_boards, research_threads, jobs, job_events (deep research),
error_log and llm_events (observability), and the eval_* tables.
Two notes:
chat_snapshots.llm_messages holds the full tool-call history so a forked snapshot
resumes with the model's context. Stat cards replay the SQL stored there, which is
why renaming a column is not a clean break — old SQL has to keep working.
session_type is 'general' or 'mp'. The 'mp' value means a chat conducted in
the persona of a member; read it as "member", not as anything UK-specific.
Full-text search
Every search vector is built with the configuration named by language.fts_config,
read at runtime from current_setting('app.fts_config') inside the triggers. Set it
per database:
ALTER DATABASE plenum SET app.fts_config = 'swedish';
This only takes effect for new sessions, so restart the API after changing it.