Search, chat and research over parliamentary speeches and documents
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

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.

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.