What if preparing a lesson meant asking a question and getting the right passages from the right curricula, sources included, in a few seconds?
That is the bet behind wag.titcheur.fr, a semantic search engine covering the reference frameworks and curricula of French secondary education: collège, general and technological lycée, and vocational tracks.
The problem
Official curricula are public, but scattered across 5,680 PDF documents, 4.9 GB of text, thousands of pages, dozens of subjects. Finding "what the 8th-grade math curriculum says about the Pythagorean theorem" is a feat in itself. Classic search engines match words; they do not understand questions.
The idea in 30 seconds
Instead of matching keywords, the tool understands meaning.
Each document is split into chunks of about 700 characters (with overlap). A bilingual French/English AI model (bilingual-embedding-large, 559M parameters) turns each chunk into a vector of 1024 numbers, a kind of coordinate in a semantic atlas: two texts about the same thing land in the same place, even when worded differently.
Your question goes through the same transformation. The system then looks for its nearest neighbors among 234,893 indexed chunks: search becomes a simple distance computation. The result: a query in natural French, with or without typos, returns the relevant official passages, ranked by semantic proximity, with a direct link to the source PDF.
And with "Élaborer", a second model writes a structured answer strictly from the retrieved excerpts, with sources cited in parentheses.
Choosing the right brain through benchmarking
A semantic engine is only as good as its embedding model. Rather than following a trend or a blog post, I measured.
I built an isolated benchmark harness (500 documents, 22,393 chunks, 30 hand-rewritten questions, 8 questions typed by subject) and pitted three models against each other:
| Model | Recall@10 | Subject purity | Latency |
|---|---|---|---|
| bilingual-embedding-large (fr/en, 559M) | 0.867 | 0.900 | 56 ms |
| multilingual-e5-base (278M, the previous one) | 0.800 | 0.825 | 33 ms |
| gte-multilingual-base (305M) | 0.767 | 0.862 | 40 ms |

Verdict: the bilingual French-English model bilingual-embedding-large wins by 7.5 points of subject purity and 6.7 points of recall. The moral: on a French corpus, a generalist multilingual model loses to a model trained for French. The switch is done. But changing models means changing vector dimensions: the whole database becomes unreadable. The story of that migration deserves its own chapter.
The fork: 234,000 chunks computed in one evening on a "disposable" cluster
Changing models means changing vector geometry (768 → 1024 dimensions). The existing database is incompatible: no delta possible, everything must be re-encoded: 234,000 chunks.
First estimate on my laptop (Apple Silicon M2/16): ~2 s per document at first... then 9 to 10 s per document on large PDFs. Extrapolation: a full night with the machine locked up.
The pivot: an existing k3s cluster on Azure: 4 nodes, 32 vCPU, no GPU, a free tier that was already used for other experiments. Zero infrastructure to create. The recipe, in about 1 hour:
- Export the state, not start from scratch: 5 GB of PDFs + the SQLite database + the already computed partial index (~900 MB) transferred to the cluster. The job is resumable: it picks up exactly where it stopped; the ~2 hours of laptop compute were not lost.
- Shard the corpus into 4 overlapping ranges, one per node, indexing in parallel.
- Merge the shards with upserts (overlaps deduplicate themselves), transfer the final index, switch atomically with immediate rollback possible.
Results:
- 234,893 chunks re-encoded in about 4 hours of distributed compute, versus a full night on a single machine
- Total cost: under $10, minute-based billing, the 4 VMs deallocated as soon as the job finished
- Production served without interruption throughout the computation; public search stayed online on the old index until the switch
- Three ChromaDB traps neutralized along the way (read pagination, the 5,461-entry insertion limit, per-document atomicity), fixed and versioned
The lesson I take away: a well-designed batch job (portable state, resume, sharding, merge) is data, not a process. It can move from a laptop to a cluster in an hour, without losing a chunk, without paying for dedicated infrastructure that would sleep 99% of the time.
The stacks
- Python / FastAPI for the API, a clean web interface in pure JS
- ChromaDB (HNSW, cosine space) for the vector database
- Sentence-Transformers for the embeddings (
bilingual-embedding-large, 1024 dimensions), with a detail that matters: thepassage:/query:prefixes the model expects, without which quality collapses - DeepSeek via API for the summaries; search itself is 100% local and instant (< 1 s)
- Docker non-root (
cap_drop: ALL), nginx + Let's Encrypt, GitLab CI/CD with SAST and secret detection, and a nightly cycle at 3 a.m. that re-crawls the sources, re-indexes what is new and rotates the database without erasing the previous one - k3s on Azure (4 nodes, 32 vCPU, no GPU) for disposable batch jobs: a full re-index in one evening, VMs deallocated as soon as the job is done
What the project taught me (the part engineers care about)
- Post-filtering a vector database is a trap: filtering after the ANN search silently truncates results. It cost me a day, with comparative tests to prove it.
- A public engine costs money per query as soon as an LLM enters the loop: rate limiting by IP (6 req/min) + automatic banning (fail2ban), otherwise the bill takes the hit.
- Security for an exposed service is won through small moves: XSS escaping of metadata, a restricted CORS policy, SSH keys only, a minimal firewall, and an auth log that already showed brute-force attempts.
- Data comes before code: staging/live rotation of the vector database, automatic rollback if the new index does not respond, never a hot overwrite.
- Benchmark on your own data: public leaderboards (MTEB) do not tell the whole story. On 500 real documents, the gap between two credible models turned out to be 7 points. Those points make the difference between a useful tool and a toy.
All of it is observable live on the page: a small graph shows the incremental evolution of the index, night after night.
Why it matters
For a teacher: "conditional probability in première" → the contents, expected skills and demonstrations of the curriculum, exact excerpts and the original PDF. For a trainer, a school inspector, a curious parent: the same entry point into what the institution actually says.
And this is only the beginning: the engine is designed to swallow other corpora, primary education, higher education, regulatory texts...
Try it: https://wag.titcheur.fr, it is free, no account required, and it answers in French.
Question, feedback, corpus idea? wag@titcheur.fr
Project developed for 199A Consulting: ingestion, indexing, hardening and automation end to end.
