FolioDocument Q&A
What it does for you
Ask questions across your own PDFs with local hashed embeddings, BM25 and rank fusion, and get answers that cite the exact page.

Screenshots


In more detail
Folio lets you drop in your own PDFs and notes, ask questions across them, and get answers that cite the exact page, with the passage highlighted when you click. Most "chat with your PDF" tools send every page to a server and hide how retrieval works. I wanted a research workspace that retrieves locally, shows its work, and admits when the documents don't contain the answer.
Honest limits
Claude mode is built and its request and response shapes are tested, but I never called the live API during this build. File uploads through the picker were exercised by automation, not by hand. Scanned PDFs won't work because there is no OCR. The hashed embeddings are a clever stand-in, not a real language model.
How I built it
Claude Code built this on September 23 from the Claude Code side of the split file, in about 20 minutes of wall-clock agent time. I set the spec: local embeddings as the headline, hybrid ranking, page citations, a passage map, compare mode, and no API calls during the build. Claude wrote the pure core first, then 170-plus assertions across small test-*.mjs files, then the server and the UI modules.
It's plain HTML, CSS, and ES modules with no build step and no npm, pdf.js from a CDN, and IndexedDB for storage. The embeddings use no model and no API: words, word pairs, and letter trigrams get hashed into 1024 buckets, weighted by TF-IDF, and normalized. That gets fused with BM25 so both signals count.
The tuning was the fun part. We ran retrieval experiments in Node together, changed a number, re-ran, and watched what moved. The API connection kept dropping long responses all day, so every file was written in pieces of about 100 lines.
What I learned
- Rank fusion math decides which signal wins more than the embedding does. Plain reciprocal rank fusion rewards whatever both lists agree on, so a paraphrased passage only beat a keyword-only match once the dense list was weighted to lead (1.0 versus 0.8) and the passage shared at least one keyword. A test pins that down.
- Hashed letter trigrams are a cheap, API-free way to let "rotation" meet "Rotate." But the trigram weight had to go from 0.35 to 1.0 before it mattered.
- PCA over a few sparse hashed vectors gets dominated by overlapping chunk pairs. Moving from 90-word to 60-word chunks and adding a signed square root turned one clump into a readable map.
- Headless Chrome with
--virtual-time-budgetnever finished loading pdf.js and its worker from the CDN. A real-time run over the DevTools protocol worked on the first try. Test lazy-loaded CDN code in real time. - Extractive answers read better when sentences from the same page stay in reading order under one citation.
What carried forward
Folio sits between Lantern (64), which answers from keyword search, and Binder (66), which became the team wiki version. Lantern's case study lists an embedding layer for paraphrased questions as its next step. Folio is the build in this run that actually has one, and it does it without calling any API. The "answerable" gate that returns "not covered" instead of a confident wrong quote is the same honest-decline idea from 64.
If I took it further
I'd add an optional small on-device embedding model as a third ranked list and OCR for scanned PDFs.
#065 Built with Claude Code September 23, 2026 173 automated checks