A RAG-based PDF assistant I'm building to learn how document retrieval systems work end to end.
The goal is to let a user upload a PDF, ask questions about it, retrieve the most relevant parts of the document, and eventually generate answers grounded in that context.
Right now, the ingestion and retrieval parts are working.
The app can:
- Upload a PDF from the React frontend
- Send it to an Express backend
- Parse the PDF with LlamaIndex
- Split the document into smaller chunks
- Generate embeddings with Hugging Face
- Store those embeddings in ChromaDB
- Search the stored chunks using semantic similarity
- Return the most relevant chunks for a question
I'm currently working on connecting retrieval to the /query route and then using the retrieved context to generate an answer.
PDF
↓
PDFReader
↓
Documents
↓
SentenceSplitter
↓
Text chunks
↓
Hugging Face embeddings
↓
ChromaDB
↓
Similarity search
↓
Relevant chunks
The next part will be:
Relevant chunks + user question
↓
LLM
↓
Grounded answer
↓
Source citations
Frontend
- React
- TypeScript
- Vite
Backend
- Node.js
- Express
- TypeScript
- Multer
RAG
- LlamaIndex
- ChromaDB
- Hugging Face embeddings
Embedding model:
BAAI/bge-small-en-v1.5
The user selects a PDF in the frontend.
The file is sent to the backend using multipart/form-data and handled with Multer.
React
↓
POST /upload
↓
Express
↓
Multer
Once the backend receives the file, LlamaIndex reads the PDF and converts it into documents.
The documents are then split into smaller chunks using SentenceSplitter.
Current settings:
chunkSize: 528
chunkOverlap: 120The chunks are converted into embeddings and stored in ChromaDB.
For retrieval, the app reconnects to the existing ChromaDB collection.
The user's question is embedded using the same embedding model, and LlamaIndex performs similarity search against the stored document chunks.
Right now I'm retrieving the top 2 results:
similarityTopK: 2pdf-knowledge-assistant/
│
├── client/
│ └── src/
│ ├── App.tsx
│ ├── App.css
│ └── main.tsx
│
├── server/
│ └── src/
│ ├── index.ts
│ ├── ingestion.ts
│ └── retrieval.ts
│
└── README.md
index.ts contains the Express routes.
ingestion.ts handles PDF parsing, chunking, embeddings, and storing vectors in ChromaDB.
retrieval.ts handles similarity search against the stored document chunks.
GET /healthPOST /uploadThe uploaded file is sent using the field name:
pdf
POST /queryRequest body:
{
"question": "What does the document say about..."
}The query route is set up, but I'm still connecting it to the retrieval pipeline.
Install dependencies:
npm --prefix server install
npm --prefix client installStart ChromaDB on:
http://localhost:8000
Start the backend:
npm --prefix server run devBackend:
http://localhost:4000
Start the frontend:
npm --prefix client run devFrontend:
http://localhost:5173
- Connect retrieval to
/query - Pass retrieved context to an LLM
- Generate answers using the document context
- Add source citations
- Handle questions that cannot be answered from the document
- Test different chunk sizes and overlap values
- Test different top-K values
- Measure retrieval quality and latency
- Deploy the project
I wanted to understand RAG beyond just calling a library method and getting an answer back.
This project has helped me understand what actually happens between uploading a document and retrieving useful context from it, especially around chunking, embeddings, vector databases, and similarity search.
The next part I'm focusing on is answer generation and evaluating whether the retrieval pipeline is actually returning useful context.