I built a RAG pipeline from scratch — no LangChain, just FastAPI + FAISS
Most RAG tutorials I found were either "pip install langchain and you're done" or 50-page academic papers. I wanted something in between — a pipeline I could actually explain in an interview, where I understood every line. So I built one from scratch. No LangChain, no LlamaIndex, no frameworks. Just FastAPI, FAISS, sentence-transformers, and an LLM API. Here's what I built, what worked, and what broke. The architecture PDF --> extract text (pypdf) --> chunk (500 char, 50 overlap) --> embed (MiniLM-L6-v2) | v question --> embed --> FAISS top-k search --> build prompt with chunks --> LLM --> answer + sources Five Python files, ~300 lines total: File Responsibility main.py FastAPI app, 3 endpoints, prompt engineering pdf_loader.py PDF text extraction via pypdf rag.py Chunking + embedding store.py FAISS vector store wrapper llm.py Swappable LLM client (Groq / OpenAI / Anthropic) How the upload works When you POST a PDF to /upload , three things happen: 1. Text extraction — pypdf reads each page and returns the raw text. Pages with no extractable text (scanned images) are skipped. 2. Chunking — each page is split into ~500-character chunks with 50 characters of overlap. The overlap prevents losing context at chunk boundaries. CHUNK_SIZE = 500 CHUNK_OVERLAP = 50 def chunk_pages ( pages ): chunks = [] chunk_id = 0 for text , page_num in pages : start = 0 while start < len ( text ): end = min ( start + CHUNK_SIZE , len ( text )) chunk_text = text [ start : end ]. strip () if chunk_text : chunks . append ( Chunk ( chunk_id = chunk_id , text = chunk_text , page = page_num )) chunk_id += 1 if end == len ( text ): break start = end - CHUNK_OVERLAP return chunks 3. Embedding — each chunk is embedded into a 384-dimensional vector using all-MiniLM-L6-v2 . This runs locally on CPU, no API call needed. Vectors are normalized so we can use inner product as cosine similarity. def embed_texts ( texts ): model = get_embed_model () # lazy-loaded singleton vectors = model . encode ( texts