Understand the loop.
Local ChromaDB collections held document chunks and quiz answers. MySQL tracked state. Document filters kept retrieval tied to the selected PDF.
Learning focus: document Q&A and semantic quiz grading.
Local prototype ↗A personal experiment in asking better questions of documents became a deeper study of retrieval, processing, and source evidence. This is how the system changed as the engineering questions grew.

The evolution
The first question was simple: can I ask a PDF a question and check my understanding? The later questions were about persistence, retrieval quality, long-running work, and the boundaries between users.
Local ChromaDB collections held document chunks and quiz answers. MySQL tracked state. Document filters kept retrieval tied to the selected PDF.
Learning focus: document Q&A and semantic quiz grading.
Local prototype ↗Chunks and embeddings moved into PostgreSQL with pgvector. Supabase Storage held the PDFs, while Docker and migrations gave the project a deployment-oriented shape.
Learning focus: cloud-backed storage and relational vector search.
Cloud iteration ↗Workspaces, asynchronous indexing, multiple retrieval strategies, source metadata, and a separate React interface became parts of the same product workflow.
Learning focus: retrieval engineering and a multi-user experience.
Current backend ↗Current system / V3
FastAPI handles authentication, uploads, scope checks, and queries. Longer indexing jobs run in a Celery worker. PostgreSQL stores both application records and pgvector embeddings; Redis coordinates queued work and status events. V3 stores original files in configurable local object storage.
HTTP / WebSocket
Engineering / Ingestion
PDF extraction, normalization, embedding requests, and database writes can outlast a normal API request. The upload path returns a queued document record and dispatches indexing to Celery. Users can follow the document state while the worker processes it.
Normalization repairs extraction artifacts before chunking. Each chunk retains its PDF page number, giving later evidence cards a route back to the original document. This improves traceability without promising perfect extraction for every PDF.
The worker uses bounded retries with exponential delay, up to three retries for document indexing. The document lifecycle is queued → processing → completed, or failed after the retry allowance is exhausted.
for page_number, text in pages:
records.extend(
(chunk, page_number)
for chunk in _page_chunks(text)
)The page number travels with each chunk into persistence. Older documents may need reprocessing to gain this metadata.
Engineering / Retrieval
Dense search can connect different phrasings of an idea. Lexical search can recover exact names, IDs, numbers, and phrases. V3 exposes both, plus hybrid and smart modes, so retrieval strategy is an explicit part of the experience.

Hybrid search combines rank positions rather than comparing raw vector-distance and lexical scores. Hybrid and lexical results then receive a lightweight rerank that blends retrieval score with query-term overlap. The current heuristic is intentionally simple; it is not a transformer cross-encoder.
overlap = len(terms & words) / max(len(terms), 1)
item["rerank_score"] = round(
item.get("retrieval_score", item.get("score", 0)) * 0.7
+ overlap * 0.3,
6,
)A ranking technique is an implementation choice, not a measured quality result. A labeled query set is still needed to evaluate its effect.
Engineering / Grounding
Retrieved passages reach the language model with filename, page number when available, chunk ID, and rank. The response also exposes evidence and citations for the frontend, so an answer can be inspected alongside the passages that informed it.
if not evidence:
return {
"answer": "I don't have enough information in the selected documents to answer that.",
"model": None,
}When retrieval returns no evidence, the query path abstains. When evidence exists, the prompt instructs the model to answer from it. That does not independently verify every generated claim; the default lightweight path does not run full claim-level verification.
Engineering / Scope & access
V3 organizes documents into knowledge bases inside workspaces. A query may target the workspace, a knowledge base, or selected documents. The backend checks membership, roles, and scope consistency before exposing records or retrieving evidence.

Authentication identifies the user; authorization determines which workspace operations they can perform. The authorization module ↗ enforces those membership and role checks.
Engineering / The product experience
The separate React and TypeScript application coordinates authentication, workspaces, uploads, and search through a shared Axios client and TanStack Query.
JWT access tokens identify API requests. Refresh sessions are hashed in the backend, rotated on refresh, and revocable. The frontend attempts a refresh after ordinary API calls return 401 and clears the session if refreshing fails.
Redis publishes document status events that FastAPI relays over authorized WebSockets. Query refreshes and status endpoints remain useful because Redis Pub/Sub is transient.

Query traces record strategy, latency, source counts, model usage, and errors. This makes the system easier to inspect and gives future experiments a starting point. Runtime telemetry does not establish retrieval accuracy.
Trade-offs & next questions
I prioritized an understandable retrieval pipeline: scoped search, page-aware ingestion, background processing, rank fusion, a lightweight reranker, and evidence-aware generation. The next step is to measure those choices with controlled experiments.
The most valuable part was the progression of questions: how to keep context scoped, preserve useful document structure, combine search signals, and make a generated answer traceable.