Building Your First RAG Pipeline: Chunking, Embedding, and Retrieval
Last time, Rae chose pgvector for storing and searching her support bot’s embedded help docs. She already runs Postgres for her app’s user data, so adding a vector column cost zero new infrastructure. Storage settled, she’s ready to build the full pipeline—one that chunks her product manual, embeds each piece, pulls the relevant passages when a customer asks something, and hands them to an LLM to generate an answer.
1. Why Your LLM Needs Help: The Problem RAG Solves
Rae’s company has a 500-page product manual for its flagship device. When a customer asks, “How do I reset the connection module?” she could feed the whole manual to an LLM—or build a system that finds the two or three relevant paragraphs and shows only those to the model.
Large Language Models (LLMs) like GPT-4 are like a smart friend who has read the manual. The catch: they have a “context window.” Think of it as short-term memory. Stuff all 500 pages into one prompt, and she’ll hit a wall. The model might cut off the text, rack up a fortune in tokens, or get confused and hand the customer a hallucinated answer.
Retrieval-Augmented Generation (RAG) solves this. Instead of handing the LLM the whole book, we build a system that locates the specific paragraphs about “resetting the connection module” and shows only those to the model.
So what happens when we count tokens for a long document using tiktoken?
⚠️ Requires: pip install tiktoken
import tiktoken
# Let's simulate a long document
long_text = "This is a sentence about espresso machines. " * 5000
# We use the encoding for GPT-4
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode(long_text)
print(f"Total tokens: {len(tokens)}")
# Output: Total tokens: 40000
# GPT-4o has a large window, but older or smaller models
# might cap out at 4k, 8k, or 32k.
# Even if it fits, processing 40k tokens for one question is slow and expensive.
Feeding the whole document to the model is like reading an entire encyclopedia to answer one trivia question. It’s a waste of time. RAG lets us be surgical instead.
2. The Three-Step Pipeline: Chunk, Embed, Retrieve
A RAG pipeline runs like an assembly line. Three main steps:
- Chunking: We chop the big document into bite-sized pieces (chunks).
- Embedding: We turn those text pieces into lists of numbers (vectors). These numbers represent the meaning of the text.
- Retrieval: When you ask a question, we turn your question into numbers too. We then find the chunks whose numbers are most similar to your question’s numbers.
The pizza metaphor makes it concrete: Chunking is cutting the pizza, Embedding is describing the toppings with numbers, and Retrieval is finding the slice that matches your craving.
3. Chunking: How to Split a Document Without Losing Meaning
This is the hardest part to get right. Split a document every 100 characters and you might slice right through a sentence.
- Naive Chunking: Fixed-length splits. Fast, but messy.
- Overlap: The end of Chunk A repeats at the start of Chunk B, so a key fact that gets split still appears fully in at least one chunk.
- Semantic Chunking: Splitting at natural breaks — double newlines, periods, that sort of thing.
Here’s how they compare in code:
text = "The espresso machine must be descaled every 3 months. Use only citric acid solutions. Do not use vinegar."
# Naive split at 30 chars
chunks = [text[i:i+30] for i in range(0, len(text), 30)]
for i, c in enumerate(chunks):
print(f"Chunk {i}: {c}")
# Notice how 'descaled' gets cut into 'd' and 'escaled' right at the
# chunk-0/chunk-1 boundary. A search for 'descaled' would fail to find
# the complete word in either chunk.
So chunk size is a trade-off. Go too small and you lose the context — the “why.” Too large and you fill the LLM’s memory with junk.
Which chunking strategy should Rae use for her product manual?
Naive / Fixed-Length Chunking — The simplest approach: split every N characters or tokens. Fast, predictable, no dependencies beyond Python’s built-in slicing. Use this for prototyping or when your documents are uniformly structured (e.g., a list of one-line FAQs). The trade-off: it slices right through words wherever the count happens to land — in the example above, “descaled” comes out as “d” and “escaled” at a 30-character chunk size. When a customer searches for “descaled,” the embedding can’t match the fragment, and the retrieval silently fails.
Overlap Chunking — Fixed-length splits, but the last M characters of Chunk A repeat at the start of Chunk B. This ensures that if a key fact straddles a boundary, it appears fully in at least one chunk. The trade-off: your index grows by roughly M/N percent (you’re storing the same text twice), and you can retrieve the same passage in two consecutive chunks. Worth the added complexity when your documents have medium-length sentences that frequently cross chunk boundaries — which is exactly Rae’s situation with her product manual’s paragraphs.
Semantic Chunking — Split at natural boundaries: sentence ends, double newlines, or heading markers. This preserves the integrity of each passage so every chunk is a complete thought. The trade-off: chunk sizes vary wildly (3 tokens vs. 300 tokens), which complicates embedding quality and context budgeting. Worth it when your documents have clear structural markers (Markdown headings, numbered steps, section breaks) — like Rae’s product manual, which has numbered sections and sub-headers.
A diagnostic, not a verdict: There’s no single best chunking strategy — it depends on your corpus. Before picking one, check: does your content have consistent structural markers (headings, numbered steps)? Semantic chunking is worth the setup. Is it mostly uniform, medium-length prose where sentences routinely straddle arbitrary boundaries, and you need something you can reason about without per-document logic? Overlap chunking at 256–512 tokens with 50–100 tokens of overlap is a reasonable thing to try first. Are you prototyping fast on short, uniform snippets where a split mid-phrase barely matters? Naive fixed-length is fine to start. For Rae’s numbered manual with sub-headers, the structural-markers condition points toward semantic chunking — but she should verify against her own retrieval failures rather than assume the diagnosis is right before she’s tested it.
When to avoid semantic chunking: If your documents have no clear structural markers (flat text, run-on paragraphs), semantic chunking degrades to sentence-splitting, which can produce hundreds of tiny, context-free fragments. In that case, overlap chunking with a larger window is safer.
4. Embeddings: Converting Words to Numbers the LLM Can Search
Computers don’t understand words — they understand numbers. An embedding is a vector, a list of floating-point numbers that drops a piece of text into a multi-dimensional map of meanings.
Two sentences with the same meaning — say, “How’s the weather?” and “Is it raining outside?” — land close together on that map. We measure that closeness with Cosine Similarity.
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
model = SentenceTransformer('all-MiniLM-L6-v2')
chunks = [
"Use citric acid to descale the machine.",
"The portafilter should be cleaned daily.",
"Mix the descaling solution with warm water."
]
# Convert text to vectors
embeddings = model.encode(chunks)
# Compare the first chunk to the others
sim_1_2 = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
sim_1_3 = cosine_similarity([embeddings[0]], [embeddings[2]])[0][0]
print(f"Similarity (Descale vs Clean): {sim_1_2:.4f}")
print(f"Similarity (Descale vs Solution): {sim_1_3:.4f}")
# The score for 'Descale vs Solution' (0.68) is meaningfully higher than
# 'Descale vs Clean' (0.37) — both are machine-maintenance sentences, so
# neither score is near zero, but the model still ranks the more directly
# related pair higher. The math 'felt' the meaning, even if the gap is
# smaller than you'd guess from the words alone.
5. Retrieval: Finding the Right Chunks
When a user asks a question, we don’t search for keywords like a 1990s search engine. We embed the question and look for the “Top-K” nearest neighbors.
If , we grab the three chunks with the highest similarity scores. The embedding model handles this well — even if the user uses different words (e.g., “reset” vs. “restart”), it knows they are related.
6. Putting It Together: Your First Working RAG Pipeline
Let’s build a mini-system with FAISS, a library that searches through thousands of vectors fast.
⚠️ Requires: pip install faiss-cpu
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. Setup
model = SentenceTransformer('all-MiniLM-L6-v2')
documents = [
"Step 1: Fill the water tank to the max line.",
"Step 2: Add the descaling powder to the tank.",
"Step 3: Run the brew cycle without coffee.",
"Step 4: Rinse the tank and run two more cycles."
]
# 2. Embed and Index
embeddings = model.encode(documents)
dimension = embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(embeddings.astype('float32'))
# 3. Query
query = "How do I use the powder?"
query_vec = model.encode([query])
# Find top 2 chunks
D, I = index.search(query_vec.astype('float32'), k=2)
print("Retrieved Chunks:")
for idx in I[0]:
print(f"- {documents[idx]}")
# The system correctly retrieved Step 2 and Step 1.
# Now you would send these to an LLM to generate a final answer.
A quick note on what “nearest” means here: IndexFlatL2 ranks by Euclidean distance, not cosine similarity — the two only produce the same ranking when the vectors are unit length. That’s not a gap you need to close by hand for this pipeline, though: all-MiniLM-L6-v2’s published pipeline ends with a normalization layer, so model.encode() already returns unit-length vectors (you can check with np.linalg.norm(embeddings, axis=1) — it comes back as 1.0 for every row). If you swap in an encoder that doesn’t normalize internally, you’d need to call faiss.normalize_L2() yourself before indexing and querying, or the L2 ranking and the cosine ranking can disagree.
7. Debugging Your Pipeline: Why Retrieval Fails
Sometimes RAG fails. You ask a question, the answer comes back “I don’t know,” and the info is sitting right there in the doc.
Common failure modes:
- The ‘Lost in the Middle’ problem: Retrieve 20 chunks and the LLM often ignores the ones in the middle of the list.
- Bad Chunking: Your chunk size was 30 characters, so a word like “descaled” got cut in half.
- Weak Embeddings: Your model doesn’t understand industry-specific jargon (medical terms, legal terms, etc.).
The Fix: Print your retrieved chunks during development. If those chunks are irrelevant, no amount of LLM “smartness” will save the final answer.
8. What’s Next: Reranking and Hybrid Search
Once your basic pipeline runs, you can add a few upgrades:
- Reranking: Retrieve 10 chunks, then use a slower, smarter model to pick the best 3.
- Metadata: Only search through documents from “Year 2024.”
- Hybrid Search: Combine keyword search (BM25) with vector search to get coverage that neither method alone provides.
Recap:
- LLMs have limited memory; RAG provides a “library” they can look at.
- Chunking breaks text down; embeddings turn it into math.
- Retrieval finds the most relevant math vectors.
- Always check your retrieved chunks before blaming the LLM!
Rae ships her first RAG pipeline, and her support bot starts answering real customer questions from the product manual. But she soon notices something odd: sometimes the bot misses a fact she can see right there in the retrieved chunks—it’s just buried in the middle of a long passage. The retrieval worked. The chunk was relevant. But the LLM didn’t use it. In the next article, we’ll explore why LLMs forget the middle of their context—and what Rae can do about it.
Check Your Understanding
Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.
Remember What are the three main steps of a RAG pipeline described in the article?
Understand In your own words, explain what Cosine Similarity measures, and why “How’s the weather?” and “Is it raining outside?” would end up with vectors close together even though they share almost no words.
Apply Using the article’s naive chunking example (splitting the descaling sentence into 30-character chunks), identify where the fixed 30-character split cuts the word “descaled” in half, and explain why Overlap chunking would fix that specific case.
Analyze The article’s FAISS example retrieves Step 2 and Step 1 for the query “How do I use the powder?” Walk through why an embedding-based search can match “powder” to “descaling powder” in Step 2 even though the query never uses the word “descaling.”
Evaluate The article’s “Lost in the Middle” failure mode says LLMs often ignore chunks in the middle of a long retrieved list. Critique the common fix of “just retrieve more chunks to be safe” (e.g., top-20 instead of top-3): what does this evaluation harness for RAG actually need to catch, if simply retrieving more doesn’t guarantee the LLM uses it?
Create Design a chunking strategy for a new document type the article doesn’t cover: a legal contract with numbered clauses and cross-references (e.g., “as defined in Section 4.2”). Would you use fixed-length, overlap, or semantic chunking, and what specific failure would you expect if you chunked this document the wrong way?
Related articles
- Vector Databases Compared: When You Actually Need One)
- Why LLMs Forget the Middle: Understanding Context Windows)
References & Further reading
- Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, V., Küttler, D., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S., & Kiela, D. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. arXiv:2005.11401
- LangChain — Text Splitters Documentation
- LlamaIndex — Chunking & Node Parsing
Apply What You Learned is for Supporter and Insider subscribers.
Subscribe to unlock the exercises on this post.
See plansRelated articles
- LLMs & GenAI Under review
Building a Simple LLM Evaluation Harness in Python
Stop guessing whether your LLM is good. Learn to build a Python evaluation harness with test cases, scorers, and model comparison that turns vibes into data.
- LLMs & GenAI Under review
Fine-Tuning vs. RAG: How to Actually Decide
Stop LLM hallucination: learn when to fine-tune vs. use RAG, with a decision framework, code examples, and a practical readiness checklist for your project.
- LLMs & GenAI Under review
Evaluating LLM Output Beyond "It Looks Right"
Learn systematic methods for evaluating LLM output across correctness, relevance, and safety using automated metrics, human review, and hybrid approaches.
- LLMs & GenAI Under review
Why LLMs Confidently Make Things Up: Understanding and Catching Hallucination
Learn why LLMs hallucinate through next-token prediction, and use log-probs and RAG to detect and prevent confident fabrication in your AI applications.
Looking for something else?
Search every article by title, summary or topic.