Hernando Abella
TutorialRAGPythonVector Databases

Building a RAG System with Python: Step by Step

Learn how to build an AI assistant that answers questions using your own documents — combining retrieval with generation for accurate, grounded responses.

15 min read Hernando Abella Intermediate
StackPythonOpenAI EmbeddingsLangChainHugging Face

Large Language Models only know what they were trained on. They cannot access your company's documents, PDFs, or internal knowledge bases — unless you build a RAG system.

RAG combines information retrieval with AI generation, allowing a model to search relevant documents and use that information when generating responses. In this guide, you'll learn how RAG works and build your own application with Python.


What Is RAG?

RAG stands for Retrieval-Augmented Generation. Instead of asking an AI model to answer directly, a RAG system first searches a knowledge base for relevant information.

What is RAG - Retrieval-Augmented Generation diagram

Example:

Question:"What is our company's refund policy?"

RAG system flow - from question to answer
Without RAG
Question → LLM → Answer
✕ Hallucinations
✕ Outdated information
✕ No access to private data
With RAG
Question → Retriever → Documents → LLM → Answer
✓ More accurate responses
✓ Access to private knowledge
✓ Reduced hallucinations

RAG bridges the gap between static AI knowledge and real-time, document-aware intelligence — turning any document collection into a searchable, answerable resource.


Core Components of a RAG System

Core components of a RAG system

Step 1: Prepare Documents

Every RAG system begins with data. The quality of your knowledge base directly determines the quality of the answers your system will generate.

Examples of document sources include:

PDFs Documentation Knowledge bases Product manuals Support articles Company policies

Before indexing, clean your data: remove headers, footers, page numbers, and irrelevant formatting. Plain text works best. If you have HTML or Markdown, strip the markup. The cleaner your input, the more accurate your retrieval will be.

ℹ️Note
Aim for clean, well-structured text. Remove noise like navigation bars, repeated watermarks, and boilerplate. Every extra word reduces retrieval precision.

Step 2: Split Documents into Chunks

Large documents must be divided into smaller pieces for efficient retrieval. A chunk is a self-contained segment of text that can be independently retrieved and provided as context to the LLM.

Fixed-size chunking

Split every N characters. Simple but can cut sentences in half.

Semantic chunking

Split at paragraph or section boundaries. Preserves meaning.

Recursive chunking

Try larger splits first, then recursively break down until size fits.

Overlapping windows

Chunks overlap to preserve context at boundaries. Prevents meaning loss.

python · chunking.py
1def chunk_text(text, chunk_size=200, overlap=20):
2    chunks = []
3    start = 0
4    while start < len(text):
5        end = min(start + chunk_size, len(text))
6        chunks.append(text[start:end])
7        start += chunk_size - overlap
8    return chunks

Why Chunking Matters:

100-page document → 500 chunks → Search only relevant chunks. This makes retrieval much faster and more precise. Good chunking is the foundation of an effective RAG system.


Step 3: Generate Embeddings

An embedding is a numerical vector representation of text. Words or sentences with similar meanings produce vectors that are close together in high-dimensional space. OpenAI's text-embedding-3-small converts any text into a 1536-dimensional vector that captures its semantic meaning.

python · embeddings.py
1from openai import OpenAI
2
3client = OpenAI()
4
5response = client.embeddings.create(
6    model="text-embedding-3-small",
7    input="Python is a programming language."
8)
9
10embedding = response.data[0].embedding
11print(f"Vector dimension: {len(embedding)}")
1,536

Dimensions
text-embedding-3-small

3,072

Dimensions
text-embedding-3-large

Each chunk of your document gets its own embedding. These vectors are what the system searches through — finding the chunks whose embeddings are mathematically closest to the user's query embedding.


Step 4: Store Embeddings in a Vector Database

Popular options: Chroma, FAISS, Pinecone, Weaviate, Qdrant

Chromaprototyping

Lightweight, local-first, no setup

FAISSperformance

Meta's library, CPU/GPU, billions of vectors

Pineconeproduction

Managed cloud, auto-scaling, real-time

Weaviateflexible

GraphQL API, hybrid search, open-source

Qdrantspeed

Rust-based, filtering, high performance

Milvusenterprise

Cloud-native, distributed, trillion-scale

A vector database stores embeddings and enables fast similarity search. When a query comes in, it compares the query embedding against all stored embeddings and returns the most similar ones — typically using cosine similarity.

terminal
1pip install chromadb
python · vectorstore.py
1import chromadb
2
3client = chromadb.Client()
4collection = client.create_collection(name="knowledge_base")
5
6# Add documents
7collection.add(
8    documents=[
9        "Python is a programming language.",
10        "Machine learning uses data."
11    ],
12    ids=["1", "2"]
13)
14
15# Search
16results = collection.query(
17    query_texts=["How is Python used?"],
18    n_results=2
19)
20print(results["documents"])
💡Pro Tip
For production use, Pinecone and Weaviate offer managed vector databases with better scaling. For quick prototyping, Chroma is lightweight and requires zero configuration.

Step 5: Retrieve Relevant Documents

When a user asks a question, the system performs a semantic search:

  1. Convert the user's question into an embedding
  2. Search the vector database for the most similar chunks using cosine similarity
  3. Retrieve the top K results (typically 3-10 chunks)
python · retrieve.py
1def retrieve(query, collection, n=3):
2    # Embed the query
3    q_emb = client.embeddings.create(
4        model="text-embedding-3-small",
5        input=query
6    ).data[0].embedding
7    
8    # Search vector DB
9    results = collection.query(
10        query_embeddings=[q_emb],
11        n_results=n
12    )
13    return results["documents"][0]

The retrieved chunks become the context that grounds the LLM's response. Without this step, the model would rely solely on its training data — which may be outdated, incomplete, or unaware of your specific information.

ℹ️Note
Re-rank retrieved chunks by relevance score and deduplicate overlapping content before sending to the LLM. This improves answer quality and reduces token usage.

Step 6: Send Context to the LLM

The final step combines everything: the retrieved chunks are injected into a prompt as context, and the LLM generates an answer grounded in that specific information. This is what separates RAG from a plain chatbot — the model answers based on your documents, not just its training data.

python · generate.py
1from openai import OpenAI
2
3client = OpenAI()
4
5prompt = f"""
6Context:
7{context}
8
9Question:
10{question}
11
12Answer using only the provided context.
13"""
14
15response = client.responses.create(
16    model="gpt-4o",
17    input=prompt
18)
19
20print(response.output_text)

The prompt structure is critical. Notice the explicit instruction: "Answer using only the provided context." This prevents the model from hallucinating or pulling from its training data. If the context doesn't contain the answer, the model should say so rather than fabricating one.


Full RAG Pipeline

Full RAG Pipeline diagram

Example Project Structure

Project Structure
rag-project/
├── data/
│   ├── docs/
│   │   ├── guide.pdf
│   │   └── policies.txt
├── embeddings/
│   └── build_embeddings.py
├── vectorstore/
│   └── chroma_db/
├── rag/
│   ├── retrieve.py
│   ├── generate.py
│   └── pipeline.py
├── app.py
└── requirements.txt

Improving Retrieval Quality

Better Chunking
Paragraph-based or semantic chunks instead of fixed sizes.
Metadata Filtering
Store source, department, date — filter before search.
Hybrid Search
Combine vector search + keyword search for accuracy.

Common Challenges

Poor Chunk Sizes

Too large = low precision. Too small = missing context.

Hallucinations

Model may still invent facts — enforce context-only answers.

Duplicate Results

Multiple chunks with similar info — use reranking.


Real-World RAG Use Cases

Customer Support
Search product documentation before answering.
Enterprise KB
Access internal company documents.
Legal Research
Retrieve contracts and regulations.
Medical Systems
Search approved clinical documentation.
Educational Platforms
Answer questions from course materials.
AI Search Engines
Combine retrieval with natural language responses.

Key Takeaways

  • RAG combines document retrieval with AI generation.
  • Documents are split into chunks and converted into embeddings.
  • Embeddings are stored in a vector database for fast similarity search.
  • Retrieved documents are sent to the LLM as context.
  • The model generates answers grounded in real information.

A well-designed RAG system is often one of the most practical and impactful AI applications you can build. It allows organizations to transform their documents into intelligent assistants that deliver accurate, context-aware answers on demand.


Ready to go deeper?

Generative AI with Python

Master RAG pipelines, AI agents, tool calling, vector databases, and multimodal systems — with hands-on code throughout.

RAG & Vector DBsAI AgentsTool CallingMultimodal AI
Get it on Amazon →
Generative AI with Python book cover
Share X LinkedIn
Hernando Abella

Hernando Abella

Software engineer and author. I write about Python, AI, and software architecture. Author of 55+ programming books and creator of interactive coding challenges.