Our question bank is written by AI models, in batches, one exam slot at a time. A slot is a topic with a set number of points in an exam. If you ask a model for ten questions on the same rule, you get ten questions on the same rule. Some of them are the same question in different clothes.
For a practice exam that's a real problem. A candidate who sees two near-twins in one sitting gets the second one for free, and the form teaches less than it looks like. This post is about the three places we catch them (import, form assembly and a leak test for free questions), and the one place we deliberately don't look.
1. At import: trigram similarity per slot
When a draft has passed its checks, the importer writes it to the database. Before the insert, it asks Postgres whether the same slot already holds a question with a very similar stem:
select q . code , extensions . similarity ( v . stem_text , $ 1 ):: float8 as similarity from questions q join question_versions v on v . question_id = q . id where q .…