Build a RAG Application with Python
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
pip install chromadb anthropic python-dotenvReal, indexing Bright Leaf Coffee's actual FAQ
# 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
# 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
answer_question("do you offer worldwide shipping")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 spaceThis 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.
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
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
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.