~/TechPurAI
~/tutorials/llm-and-advanced-ai/build-a-rag-application-with-python
intermediate·part 6 of 22·3 min read

Build a RAG Application with Python

Updated Aug 16, 2026AI

This is the real, complete implementation of part 5's pipeline — replacing the AI Projects series' own keyword-matching retrieval with genuine, embedding-based semantic search using Chroma, part 4's chosen tool for this scale.

Real, practical setup

bash
pip install chromadb anthropic python-dotenv

Real, indexing Bright Leaf Coffee's actual FAQ

python
# index_documents.py
import chromadb

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection("bright_leaf_faqs")

FAQ_DOCUMENTS = [
    {"id": "shipping-1", "text": "Orders ship within 24 hours of roasting. Standard shipping takes 3-5 business days."},
    {"id": "refund-1", "text": "If your coffee arrives stale or damaged, contact us within 14 days for a full refund or free replacement."},
    {"id": "subscription-1", "text": "You can change your roast selection or pause your subscription anytime before the next billing cycle."},
]

collection.add(
    documents=[doc["text"] for doc in FAQ_DOCUMENTS],
    ids=[doc["id"] for doc in FAQ_DOCUMENTS],
)

PersistentClient stores the real index on disk, rather than only in memory — a genuine, practical requirement so this real index survives between separate runs of the actual application, exactly the kind of durability the earlier in-memory KNOWLEDGE_BASE list never needed to consider.

The real, complete RAG query function

python
# rag_app.py
import chromadb
from shared.ai_client import get_client

db_client = chromadb.PersistentClient(path="./chroma_db")
collection = db_client.get_collection("bright_leaf_faqs")

def answer_question(question: str) -> dict:
    results = collection.query(query_texts=[question], n_results=2)
    retrieved_docs = results["documents"][0]
    retrieved_ids = results["ids"][0]

    if not retrieved_docs:
        return {"answer": "I don't have information about that.", "sources": []}

    context = "\n\n".join(retrieved_docs)

    ai_client = get_client()
    response = ai_client.messages.create(
        model="claude-sonnet-5",
        max_tokens=512,
        system=(
            f"Answer using ONLY this real, retrieved information:\n\n"
            f"{context}\n\nIf this doesn't fully answer the question, "
            f"say so directly rather than guessing."
        ),
        messages=[{"role": "user", "content": question}],
    )
    return {"answer": response.content[0].text, "sources": retrieved_ids}

A real, direct comparison against the earlier keyword version

python
answer_question("do you offer worldwide shipping")
text
The AI Projects series' keyword version: fails to match — no
  literal "worldwide" or "international" appears in any real
  document, so find_relevant_docs() returns nothing

This part's embedding version: correctly retrieves shipping-1
  despite the different literal phrasing, since "worldwide shipping"
  and "orders ship" are genuinely close in embedding space

This is the real, concrete, working proof of everything parts 1 through 5 built toward — the exact honest limitation flagged in the earlier series is now genuinely resolved, not just described.

Why it matters

Notice how little of this code is actually new compared to the AI Projects series' version — answer_question()'s overall shape (retrieve, then generate grounded on what's retrieved, with an explicit no-match fallback) is identical. The real, meaningful change is entirely in HOW retrieval happens — collection.query() instead of keyword-overlap counting — which is exactly the kind of clean, isolated upgrade a well-architected retrieve-then-generate system should allow.

Re-indexing when real documents change

python
def update_document(doc_id: str, new_text: str):
    collection.update(ids=[doc_id], documents=[new_text])

def add_document(doc_id: str, text: str):
    collection.add(ids=[doc_id], documents=[text])

Chroma's real update() and add() methods handle re-embedding automatically — a genuine, practical convenience over manually managing vectors, though part 16 covers the real, harder problem of when and how to trigger these updates as Bright Leaf Coffee's actual FAQ content changes over time.

What this real version is still missing

text
No real chunking strategy for longer documents (part 7)
No comparison against fine-tuning as an alternative approach (part 8)
No real evaluation of whether retrieval quality is actually good
  (part 19)

This part's real, working code is a genuine, complete RAG application — not a toy — but production maturity, exactly like every earlier project in this site's tutorials, comes from the dedicated parts ahead addressing each of these real, remaining concerns directly.

Next: improving RAG retrieval quality — real chunking strategy, re-ranking, and hybrid search, for when this part's basic version isn't precise enough.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous5. What Is RAG (Retrieval-Augmented Generation)? The Complete Technical Explanationnext →7. Improving RAG Retrieval Quality