§lectorium

Lectorium overview

Introduction

Estonian law is public and free to read. Riigi Teataja, the state gazette, is a good register - access is not the problem. Navigation is. To find the answer to your question you already need to know which act to open and how that provision happens to be worded. Someone who wants to know how much annual leave they get does not know the answer sits in § 55 of the Employment Contracts Act.

Lectorium is an attempt to close that gap. You ask in plain Estonian and get the relevant provision back with a citation. This page describes, as honestly as we can, how the system works, what we trained ourselves, how accurate it measurably is, and what does not work yet.

We write about the failures too, because with legal information the cost of being subtly wrong is high. A system whose limits are published is more trustworthy than one that promises everything.

What is Lectorium?

Lectorium is a retrieval-grounded legal assistant. It does not write the answer itself. It finds the provision that matches your question in an indexed body of legal text and quotes it verbatim, with a link to Riigi Teataja.

Twenty-two acts are indexed today - 3531 provisions in total:

  • 22acts indexed
  • 3531provisions
  • 768vector dimensions

The largest is the Law of Obligations Act (1143 provisions), the smallest the Youth Work Act (22). The rest: Law of Property Act (265), Family Law Act (225), Social Welfare Act (191), Law of Succession Act (188), Building Code (180), General Part of the Civil Code Act (174), Employment Contracts Act (159), Civil Service Act (144), Health Services Organisation Act (138), Animal Protection Act (103), Non-profit Associations Act (99), Consumer Protection Act (92), Health Insurance Act (82), Apartment Ownership and Apartment Associations Act (81), Occupational Health and Safety Act (62), Child Protection Act (52), Personal Data Protection Act (38), Work Ability Allowance Act (34), Health Care Provider Liability Insurance Act (34) and Collective Agreements Act (25).

How it works

The work splits in two: build time (constructing the index, in Python) and query time (answering a question, in pure Go). No Python runs while answering.

1. Ingesting the legal texts

Texts come from Riigi Teataja. Each act is referenced by a version id that is always resolved to the currently valid consolidated text, so citations stay correct across amendments.

2. Chunking

The text is split per provision. A long provision is split further at subsection boundaries, and every piece repeats the section heading - otherwise a provision holding ten different rules collapses into one averaged vector and answers none of them well. 3531 provisions become 4693 retrievable chunks this way. Each chunk carries its own metadata: act, section number, chapter, entry-into-force date, and a pre-built link to Riigi Teataja. That is why every answer can carry an exact citation - the citation is a fact recorded at build time, not a guess made afterwards. Pieces of the same provision are never repeated in the source list: the best-scoring one is shown.

2b. Annex tables

Some acts delegate their most-asked question to an annex. Building Code § 35 says a building notice is required "for the buildings named in annex 1" — whether a shed needs a permit is decided in a table, not in any section. Riigi Teataja hides those annexes inside the act's XML as base64 PDFs; Lectorium extracts them, renders them to text and indexes them like any other provision, keeping the column boundaries as separators so a row stays readable outside a monospace font.

3. Embedding

Each provision is turned into a numeric vector by multilingual-e5-base (768 dimensions), an open-weights multilingual model that handles Estonian. The vector encodes meaning, which is why a question and a provision can match even when they share no words at all.

4. Retrieval

The question is embedded by the same model and compared against every provision by cosine similarity. The top five come back. If the best result falls below a threshold of 0.81, Lectorium does not answer at all - it says it found nothing clearly relevant.

5. The answer

From the best-matching provision, the subsections most relevant to the question are selected and presented unchanged, in the form "§ N (Act): text".

Embedding model
multilingual-e5-base
Vector dimensions
768
Index size
3531 provisions (4693 chunks)
Results returned
5
Relevance threshold
0.81 cosine
Answer type
extractive (verbatim quote)
Query-time runtime
Go + ONNX Runtime

The model we trained

One goal of this project was to train an Estonian language model from scratch rather than fine-tune an existing one. We did that - and the result taught us the most important thing we know about the product.

First attempt: on a CPU

We started on an ordinary processor (16 cores, no GPU): a 6.79-million parameter model, our own 8000-token BPE vocabulary, 2200 steps, about two hours. Validation loss fell from 9.02 to 5.36 and perplexity from roughly 8300 to 210 - a clean, monotonic descent. The model wrote recognisable Estonian and knew real entities (Riigikogu, University of Tartu), but was plainly undertrained: it repeated itself and rambled.

That was not a usable model. It was proof the pipeline worked.

Second attempt: on a GPU

The real training run used a single rented RTX 3090.

Parameters
33.4 million
Architecture
8 layers, 8 heads, 512 dims
Context length
512 tokens
Vocabulary
16,000 BPE
Training corpus
111M tokens (Estonian Wikipedia)
Epochs
3
Steps
15,000
Training time
~41 minutes
Total cost
0.24 USD
Validation loss
3.16
Perplexity
~24

Perplexity improved from 210 to 24 - roughly ninefold. The model now writes fluent, grammatical Estonian and uses real names.

And then we found the problem

Fluent Estonian is not truthful Estonian. The model wrote, for example:

"Tallinn on väljamõeldud piirkond Põhja-Makedooniast."
("Tallinn is a fictional region of North Macedonia.")

Grammatically flawless. Entirely invented. The model had learned how Estonian sounds, but not what is true.

Conclusion

That experiment is exactly why Lectorium does not use this model to write answers. If a model can confidently claim Tallinn is in North Macedonia, it must not be the thing that tells you what your redundancy payment is. Our own trained model stands as proof of what a language model must not do with legal information.

Why the answer is a quote

That conclusion produced Lectorium's central design decision: the answer is a quote, not prose. The system does not paraphrase or summarise the law - it shows the provision as it stands in Riigi Teataja.

This gives up some comfort. A smoothly written summary would be pleasanter to read. But a summary cannot be checked, and a quote can.

The same logic is enforced in code as three rules:

  • No answer without a source. An answer and an empty source list are mutually exclusive - this is checked at the response-construction layer, not left to a model's discretion.
  • Below the threshold, no answer. If the best retrieved provision scores under 0.81, the response is "no clearly relevant provision found".
  • The disclaimer travels with it. Every answer carries a note that this is not legal advice.

The query-time service is written in Go, with the embedding model running through ONNX Runtime in the same process. Answering needs no Python and no external API - the question never leaves the server.

Sebastian: when retrieval is undecided

Some questions have two different correct answers depending on who is asking. "What are an employer's obligations towards an employee?" means one thing under the Employment Contracts Act and another under the Occupational Health and Safety Act. Both provisions are relevant, both clear the threshold, and their scores differ by thousandths. Until now Lectorium simply picked one - quietly, without asking.

"Millised on tööandja kohustused töötaja ees?"
TLS § 15 (0.880)  ·  TTOS § 14 (0.870) - a gap of 0.010

Sebastian now asks. He appears below the answer - the law has already been quoted and cited - and offers one choice: "Are you asking about employment law or workplace safety?" Tap it and retrieval runs again knowing what you meant. Ignore him and the first answer stands, exactly as before.

When he appears

The rule is deliberately narrow. Every condition must hold at once:

  • Two acts clear the threshold. If nothing was returned, or a single act answered it, there is nothing to disambiguate.
  • They are within 0.015 of each other. A clear winner needs no question. Further apart than thousandths and he stays quiet.
  • An authored case exists for that pair of acts. There are seven today. A near-tie with no case means silence — and goes into the backlog.
  • The question carries a topic cue. The act pair alone is too coarse: "Mis on ostueesõigus?" is not a question about defective goods, even though it hits the same pair.
  • You have not already said which side you mean. If the question contains the word "ametnik", he does not ask whether you are a public servant.

Measured against the evaluation set, he appears on ten questions out of 301 — three in a hundred. One question per answer, and at most two in a conversation — the second only ever about a different distinction. Which clarification has already been made is carried by the app itself; no session about you is kept on the server.

What he never does

Sebastian answers no legal question. He does not summarise, interpret or advise. Nor does he write his own sentences: every word he says — the question and the options — is authored by hand in advance and shipped alongside the index. No language model generates his text. The answer still comes from the act, word for word, with a link; Sebastian only steers which provision is looked for.

Where this stops

He knows only the distinctions written down for him — seven pairs across twenty-two acts. And he does not help where help is most needed: when retrieval is confidently wrong in favour of a single act, there is no tie to notice and he stays silent. He resolves being undecided, not being wrong.

How accurate is it?

Accuracy is measured over 296 questions spanning twenty-two acts. The expected answer for each question is set from the act's own section headings - not from what retrieval happens to return. That distinction matters: it is what stops the benchmark from flattering itself. Five further trap questions are included, where the correct outcome is silence: the answer lives in an instrument that is not in the index.

  • 79%correct provision ranked first
  • 94%correct provision in the top 3
  • 96%correct provision in the top 5

Since Lectorium shows several retrieved provisions at once, the middle number matters most in practice: 94% of questions surface the correct provision within the top three. That number has not fallen across eight consecutive act additions while the index doubled — a self-contained act does not break what already worked. The most recent gain came from a title bonus: a provision whose heading contains one of the question's content words earns three thousandths of extra weight. It fixes the "generic section outranks the specific one" pattern — MTÜS § 18 "Üldkoosolek" no longer displaces § 19 "Üldkoosoleku pädevus". Concrete questions - deadlines, rates, compensation, remedies - generally land correctly.

"Kui pikk on põhipuhkus?" TLS § 55 - correct

Problems

These are the known weaknesses. Some are fixable; one is live right now.

Definition questions in large acts

The weakest spot is still the simplest question shape: "What is X?". In a large act, the general definition provision loses to a more specific sibling that happens to use the term more densely. In the Law of Obligations Act, § 8 "Definition of contract" still wins queries that should go elsewhere.

Part of this turned out to be a chunking bug rather than a model limitation: long provisions were never being split, so a single vector averaged a dozen unrelated rules. Fixing it lifted first-place accuracy from 69% to 76% and top-three from 87% to 90%. The lesson: when a retrieval symptom tracks the size of the act, suspect the pipeline before the model.

A reranker would fix more of these, but not all - in some cases the correct provision never enters the top ten at all, which makes it a recall failure rather than a ranking one. Reranking cannot help when the right answer was never in the pool.

The wrong act for the right topic

This is a genuine bug, and it is currently in production:

"kaup on defektne, mida saan nõuda?"
("the goods are defective, what can I claim?")
TKS § 9 "General requirements" (0.867 - above threshold)
expected: VÕS § 222 (0.864 - second place)

Consumer remedies live in the Law of Obligations Act; the Consumer Protection Act mainly frames definitions and supervision. Retrieval prefers the wrong act here - and does so with a score above the threshold, which means the answer comes back confidently wrong.

This is not one query but a pattern: a passing mention inside a narrow act outscores the section that actually defines the term. "Mis on käsundusleping?" now returns a Social Welfare Act provision about foster-parent contracts (0.809), because the word happens to appear there; the Law of Obligations Act § 619 "Definition of a mandate contract" sits two thousandths behind. Every act we add gives the pattern more surface.

Why it isn't fixed

A reranker resolves this query - in testing, VÕS § 222 rises to first place. But there is no reranker in production: the Go service uses the embedding model only, and the reranker has not yet been exported to ONNX. The gap is now three thousandths (0.867 vs 0.864) and the right provision is visible in the source list, but the wrong act still holds first place. Until the reranker ships, this query is a known hole.

As an interim measure Sebastian now asks here: a three-thousandth gap is a tie by his rule, and an authored case exists for this pair of acts. "Mida saan müüjalt nõuda" leads into the Law of Obligations Act, "millised nõuded kauplejale kehtivad" into the Consumer Protection Act. It does not fix the ranking — it lets the user settle it.

Everyday words versus statutory language

People ask the way they speak; acts are drafted in abstractions. When no provision carries the word the asker used, the right one never surfaced at all. There is now a vocabulary bridge: before searching, the act's own terms are appended to the question — "müra" (noise) brings in "kahjulikud mõjutused", "kuur" (shed) brings in "abihoone". Only the search is expanded; the question shown back to you and the citation are untouched. It is limited to the words written into it, each added when a real question was seen to miss, and it deliberately stays out of the way when the asker already uses the statutory term.

What it looked like before, and why a reranker could never have fixed it:

"Kas pean taluma naabri müra ja tolmu?"
("do I have to tolerate my neighbour's noise and dust?")
the rule is AÕS § 143 "Harmful effects", but it landed fourth (0.774), below the threshold - so Lectorium stayed silent. With the bridge it is the first answer.

The word "servituut" behaved the same way: the Law of Property Act defines a reaalservituut and an isiklik servituut, never the bare term, so the question used to return a section of the Civil Service Act about disciplinary offences. A reranker could not have helped with any of these — the right provision was never in the pool to rank, which is precisely why the fix had to happen on the question rather than on the ordering.

Data protection: half the answer lives outside Riigi Teataja

The Personal Data Protection Act only supplements the EU GDPR. What people actually ask about - lawful bases for processing, data subject rights, the breach notification deadline, whether a DPO is required - sits in the Regulation itself, which Riigi Teataja does not host and Lectorium therefore does not quote.

So every answer resting on that act carries a separate note saying the quoted provision may not be the whole rule. Chapter 4 of the act (§ 12-50) is deliberately excluded from the index: it binds only law enforcement authorities in criminal proceedings, yet its headings read like general rules - "Designation of a data protection officer", for instance. Without the exclusion an ordinary company would have received a confident answer that does not apply to it.

What Lectorium does not do

  • It does not synthesise across provisions. When an answer requires two acts read together, Lectorium shows the provisions but will not combine them into a conclusion.
  • It does not know case law. The index holds statutory text, not judgments or interpretations.
  • It does not cover all of Estonian law. Twenty-two acts is a small slice. If your question concerns an act that is not indexed, Lectorium finds nothing - safe behaviour, but not an answer.
  • It is not legal advice. Estonia regulates who may provide legal counsel. Lectorium shows statutory text; the conclusion about your situation is yours or your lawyer's to draw.

What's next?

Roughly in this order:

  • Reranker into the Go service. Exporting the cross-encoder to ONNX. This closes the TKS/VÕS hole described above and improves some definition questions.
  • More acts. Every act added so far has come without degrading the accuracy of the earlier ones - adding the Law of Succession Act moved the previous results by exactly zero.
  • Instruction tuning. For our own model to ever phrase answers, it must learn to answer from given provisions rather than continue text. We have not done that yet - and until we do, the answer stays a quote.
  • Growing the vocabulary bridge. The mechanism exists; each new entry comes from a real question seen to miss. Longer term it should be derived from the acts' own vocabulary rather than written by hand.
  • The GDPR itself into the index. The Estonian-language text has to come from EUR-Lex. Until it does, general data protection questions are effectively unanswerable.
  • Better definition retrieval. Chunking is fixed; the handling of definition provisions still needs work.

If something goes wrong - the wrong provision, a missing act, a strange answer - that is useful information. The numbers on this page change as the system does.