The problem it solves
Type otkaz na bolovanju into any Serbian legal search and you get nothing. Not because the law is silent, but because the statute never uses the word bolovanje — it says privremena sprečenost za rad usled bolesti. Not one word in common, so keyword search returns an empty page while the answer sits in article 183 of the Labour Act.
The gap is vocabulary, not knowledge. People know what happened to them; they cannot guess the term it was filed under. Measured against the corpus, the word predomislio ("changed my mind") appears in none of the 5,056 indexed articles — the law calls it odustanak od ugovora.
So the question is translated into statutory vocabulary first, and only then searched. That single step is what replaces embeddings.
No vector database
The first build did it the orthodox way: Postgres, pgvector, an embedding per article. It worked, and for a project with no budget it meant a database that idles into suspension, a monthly bill, and one more service that can fail.
The whole corpus is now a single 8 MB SQLite file committed to the repo and
read in-process through node:sqlite, with FTS5 doing retrieval in about
0.1 ms. Deploying needs exactly one environment variable.
Honest caveat, because it matters: in my own measurements vectors still ranked the correct article first slightly more often. SQLite is cheaper and sufficient here — not equivalent.
Citations are verified, not trusted
The model is given the retrieved articles and told to use nothing else, with a
sanctioned way to refuse. That instruction is not trusted. After generation,
code extracts every [Zakon o radu, član 183] citation and checks it against
the articles actually retrieved; anything unmatched is flagged in the UI as
nepotvrđen citat. The legal disclaimer is appended by the application, so the
model cannot reword or drop it.
Refusal is treated as a feature. Ask something outside the corpus and it says so rather than assembling a confident-sounding paragraph — which, for legal questions, is the difference between useless and harmful.
Built in a day
First commit to live site inside a day, written with Claude Code. What that time actually bought was not the UI but the parts that are easy to skip: the citation verifier, graceful degradation to the retrieved articles when the upstream model quota runs out, and a privacy page that states plainly that questions are sent to Google.
Repealed articles are a good example of how narrow the margin is. The corpus
keeps them, because a statute printed with silent gaps is harder to read — but
retrieval excludes anything opening with (Brisano), so a dead provision can
never end up in an answer. The filter matches on that prefix rather than the
word anywhere in the text: 23 articles are genuinely repealed, while 14 live
ones merely mention it, including one about the property of a struck-off
company. Catching that distinction is the difference between fixing a bug and
quietly deleting fourteen valid articles.