The Problem
Document collections lose their value when answers can't be trusted: chunk-level RAG loses page context, ingestion pipelines fail silently, and nobody can tell which source an answer came from.
The Solution
An event-driven RAG backend built for reliability. PDFs are ingested through Inngest durable workflows — each stage (load, chunk, embed, upsert) is an independently retriable, observable step with throttling and per-source rate limits. Chunks carry page numbers end to end, so every answer ships with inline [1], [2] citations resolving to source file and page. A Streamlit console handles uploads and questions; queries can be scoped to a single document via payload filtering. Deterministic UUIDv5 ids make re-ingestion idempotent.
How the workflow runs
01 · Ingest
Upload fires a docsage/ingest_pdf event; Inngest runs load-and-chunk and embed-and-upsert as retriable steps.
02 · Guard
Throttling (2 runs/min) and per-source rate limits (1/4h) prevent accidental embedding storms.
03 · Store
Page-tagged chunks land in Qdrant with source payloads; deterministic ids make re-ingestion idempotent.
04 · Retrieve
Queries embed and search top-k chunks, optionally hard-filtered to one source document.
05 · Answer
GPT-4o-mini answers strictly from numbered context; the response carries citations with source, page, and snippet.
AI layer
- OpenAI embeddings
- text-embedding-3-large (3072-dim), model and dimensions configurable via env.
- Grounded answering
- System prompt restricts answers to numbered context with an explicit not-found fallback.
- Citation pipeline
- Source/page payloads flow from chunker to search to answer — provenance is never lost.
Automation layer
- Durable workflows
- Inngest steps retry independently and are observable step-by-step in the dashboard.
- Idempotent ingestion
- UUIDv5 ids derived from source + chunk index — re-uploads overwrite, never duplicate.
- One-command infra
- Docker Compose ships Qdrant, the Inngest dev server, and the API together.
Technical challenges
Naive chunking flattens PDFs and destroys page boundaries, making citations impossible.
Page-aware chunking: each page is chunked independently and the page number travels in the vector payload end to end.
Long ingestion jobs fail midway and re-running duplicates vectors or loses work.
Durable step functions with deterministic ids — failed steps retry in isolation and completed work is never redone.
Vector stores' evolving client APIs (deprecated search methods) break production code.
Standardised on the modern query_points API with a thin storage class that also exposes in-memory mode for tests.
Outcome
- Every answer is checkable — inline citations resolve to source file and page in one click.
- Ingestion is operationally boring: retries, throttling, and idempotency are built into the workflow, not scripts.
- The test suite runs on in-memory Qdrant with zero API keys, so CI stays fast and free.
Lessons learned
- “Provenance is a data-modelling problem, not a prompting problem — citations work because page numbers live in payloads.”
- “Durable execution turns 'hope the batch finishes' into an auditable, retryable pipeline.”
- “Storage layers deserve the same abstraction discipline as LLM calls: in-memory mode makes vector logic testable.”