A Case study
StudyLoop
A study assistant scoped to one course. You upload the lecture PDFs, slides and notes, then ask questions. Answers come only from that material and cite the page they came from, and it refuses when the material doesn’t cover the question.
Free sign-up. Requests are rate-limited.

The problem
A university course arrives as scattered files: lecture PDFs, slide decks, Word handouts, photos of a whiteboard. General chatbots answer from the open internet rather than from the course, and they answer confidently when they should decline.
What it does

- Course spaces with owners, instructors and members. Invites go out by link or email.
- Uploads of PDF, PowerPoint, Word, or a photo of handwritten notes. Extraction runs in the background, and duplicates are rejected by content hash rather than by filename.
- Grounded answers. An answer streams in with numbered citations that open the source at the right page. If the material doesn’t cover the question, StudyLoop says so.
- Study tools built on the same retrieval: quizzes and flashcards from chosen lectures, a spaced-repetition queue seeded by wrong answers, and per-document summaries.
- For instructors, a heatmap of which lectures the class asks about, and a course forum. Refused questions can be escalated there, and an accepted answer is embedded back into the course.
- Cost controls. Every paid model call is recorded and priced. Near-identical questions are served from a cache, and each user has rate limits and a token allowance.
A quiz generated from the same lecture, after submitting it. Every option comes with an explanation.

Short answers are graded against an expected answer and a marking note.

How a question is answered
- Check the answer cache for a near-identical question.
- Search three ways in parallel: dense vectors (pgvector HNSW, cosine), PostgreSQL full-text search over a generated
tsvectorcolumn with a GIN index, and images of pages that carry figures, embedded in the same vector space as the text. - Fuse the three ranked lists with Reciprocal Rank Fusion into 30 candidates.
- Rerank the candidates with a cross-encoder (Cohere rerank-v3.5) and keep the top six.
- If the best relevance score is below the refusal threshold, decline and offer to post the question to the course forum.
- Otherwise expand each hit to the whole section it came from, and stream an answer over SSE with
[n]citations.
Clicking a citation opens the source at that page. Here, citation 1 from the answer above opens page 4 of the lecture, where the quantum trade-off is explained.
![A side panel labelled “Source [1]” over the Ask view, showing page 4 of 5 of the lecture PDF, with the section “6.1 Choosing the quantum” visible.](/_astro/studyloop-source.C9cBhSOE_Zz2jLx.webp)
The lecture doesn’t cover page faults, so StudyLoop declines instead of answering from general knowledge. The student can post the question to the course forum or ask for an uncited general answer.

How material gets in
Before anything is processed, the uploader shows the first and last pages of the chosen range. A textbook’s printed page numbers rarely match its PDF page numbers, and a wrong range looks healthy once it has been ingested.

- Pick an extractor by content type: Apache PDFBox for PDFs, Apache POI for slides and Word documents, Gemini vision for photos.
- Score every PDF page for extraction quality. Only the pages PDFBox got wrong (scans, broken font encodings, two-column layouts) are sent to Gemini 2.5 Flash.
- Convert everything to Markdown pages.
- Chunk on the document’s own headings. Documents with no headings fall back to semantic, then paragraph-based chunking.
- Embed each chunk with Cohere embed-v4.0 at 768 dimensions, with the document title and heading path prepended, and store it in PostgreSQL.
Decisions
One intermediate format. Every extractor produces Markdown pages. Adding slides, Word documents and photos needed no change to chunking, retrieval, citations, quizzes or evaluation.
Refuse on the reranker’s score. A cross-encoder’s relevance score means the same thing for every question, and cosine similarity doesn’t. The threshold sits between the lowest-scoring answerable question and the highest-scoring unanswerable one in the evaluation set.
Measure before switching on. An evaluation harness in the repository scores retrieval with Recall@6, MRR and nDCG over a fixed question set that includes deliberately unanswerable questions. Every retrieval stage is a config flag, so a comparison is a property change. Stages that didn’t improve the scores, such as synthetic query indexing, ship switched off. The README has the results for each stage.
Filter inside the vector search. Every search is scoped to a course, to documents that finished processing, and to what the asking member is allowed to see. An HNSW index applies filters after it picks neighbours, so a small course could get fewer candidates than it asked for. Iterative scans (hnsw.iterative_scan) keep it searching until enough rows pass the filter.
Sandbox the model’s code. Optional video explanations run Manim animation code written by a language model. That code has to pass an allow-list over its Python syntax tree. It then runs as a non-root process with CPU, memory, file-size and process limits and no network, in a separate container that holds no credentials. Its tests are attacks: a fork bomb, a ten-gigabyte write, imports, sockets and infinite loops.
Testing and delivery
JUnit 5 and MockMvc, with integration tests that run against a real PostgreSQL with pgvector rather than an in-memory stand-in. GitHub Actions builds and tests every push, and builds both Docker images. Flyway manages the schema.
Try it
The live demo is open for sign-up. Create an account, make a course and upload a PDF. Requests are rate-limited because every answer calls paid model APIs. Video explanations aren’t available in the demo, because the renderer needs a machine of its own.
Stack
- Backend: Java 21, Spring Boot 4.1 (Web MVC, Security, Data JPA), PostgreSQL with pgvector on Supabase, Flyway, Apache PDFBox and Apache POI.
- Models: Cohere embed-v4.0, rerank-v3.5 and Command R. Google Gemini 2.5 Flash for vision.
- Frontend: React 19, TypeScript, Vite and Tailwind CSS 4.