CAMPUX Cloud Bootcamp Lab · AI Platform ← All labs
Hands-On Lab · Advanced
~50 min · Free search tier · Cloud Shell
Python · AI Search · Azure OpenAI · torn down
AI Platform Engineering

Grounded RAG: answers with a citation.

Retrieval-augmented generation is how you make a model answer from your documents instead of its imagination — and "RAG using Azure AI Search, vector databases, and embeddings" is written straight into the job spec. You will build the whole pipeline over a Campux knowledge base, get cited answers, and prove the hardest part: that it refuses to answer what it cannot find.

Fig. 1 · RAG: retrieve, then ground
Resource group · rg-lab-ragindexretrieve → groundAI Searchvector indexEmbedding modelchunks → vectorsChat modelanswers, groundedgrounded, or it refuses
● Screen walkthrough Not yet recorded · ~10 min
Reel · 00:00 / 10:00

Ground a RAG answer on Azure AI Search — same question, cited vs. hallucinated, side by side.

Placeholder — the page below stands alone until the reel lands
Why

The point of RAG is the refusal

A language model on its own is a confident guesser: ask it your company's return policy and it will invent a plausible one. Retrieval-augmented generation fixes that by putting a retrieval step in front of the model — you convert your documents into vectors (lists of numbers that capture meaning), store them in a search index, and at question time you find the closest chunks and hand only those to the model as context. The model answers from the passage, cites it, and — this is the part that matters in a regulated business — says "I don't have that" when the passage does not contain the answer. Anyone can wire an LLM to a search box; the professional deliverable is a pipeline you can trust, and trust is demonstrated by a clean refusal, not just a good answer.

You will stand up a free-tier Azure AI Search service and an Azure OpenAI resource, embed a small Campux knowledge base into a vector index, and write two short scripts: one to ingest, one to ask. Then you will ask it something it cannot know and watch it decline.

A model that always answers is a liability. RAG's job is to teach it when to say "not in the documents."

Before you begin — one-time setup

You need a free Azure account, the Azure CLI (az) and Python 3, then be signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs (winget / brew / apt), and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash), preinstalled and already signed in.

What you need & what it costs

Azure Cloud Shell (Bash) with Python. Source is in github.com/kloudcaptain/campux-labs under lab-rag-azure-ai-search. The Free AI Search tier costs nothing and includes vector search; embedding a handful of short documents and asking a few questions with text-embedding-3-small and gpt-4o-mini comes to a fraction of a cent. Tear down at the end regardless.

Setup

A search service and two models

Create the resource group, a free-tier search service (it includes vector search at no charge), and an Azure OpenAI resource with two deployments — one to turn text into vectors, one to write the grounded answer.

# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

RG="campux-lab-rag-rg"
az group create -n "$RG" -l eastus

# free-tier AI Search - one per subscription, includes vector search
SEARCH="campuxsearch$RANDOM"
az search service create -n "$SEARCH" -g "$RG" --sku free -l eastus
SEARCH_ENDPOINT="https://$SEARCH.search.windows.net"
SEARCH_KEY=$(az search admin-key show --service-name "$SEARCH" -g "$RG" --query primaryKey -o tsv)

# Azure OpenAI + an embeddings model and a small chat model
AOAI="campuxaoai$RANDOM"
az cognitiveservices account create -n "$AOAI" -g "$RG" -l eastus2 \
  --kind OpenAI --sku S0 --custom-domain "$AOAI" --yes
az cognitiveservices account deployment create -g "$RG" -n "$AOAI" \
  --deployment-name text-embedding-3-small \
  --model-name text-embedding-3-small --model-version 1 --model-format OpenAI \
  --sku-name Standard --sku-capacity 1
az cognitiveservices account deployment create -g "$RG" -n "$AOAI" \
  --deployment-name gpt-4o-mini \
  --model-name gpt-4o-mini --model-version 2024-07-18 --model-format OpenAI \
  --sku-name Standard --sku-capacity 1
AOAI_ENDPOINT=$(az cognitiveservices account show -n "$AOAI" -g "$RG" --query properties.endpoint -o tsv)
AOAI_KEY=$(az cognitiveservices account keys list -n "$AOAI" -g "$RG" --query key1 -o tsv)

export SEARCH_ENDPOINT SEARCH_KEY AOAI_ENDPOINT AOAI_KEY
pip install --quiet azure-search-documents openai
Checkpoint Both services exist and four values are exported. If a model deployment is refused in eastus2, that region is out of capacity for it — try swedencentral or eastus (footnote 1). The free search service is the one-per-subscription freebie; if you already have one, reuse it.
Step 1

Ingest: chunk, embed, index

This script creates a vector index and uploads the Campux knowledge base. Each document is embedded into a 1536-number vector; the index stores the text and its vector side by side.

cat > ingest.py <<'EOF'
import os
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
    SearchIndex, SimpleField, SearchableField, SearchField,
    SearchFieldDataType, VectorSearch, HnswAlgorithmConfiguration, VectorSearchProfile)
from openai import AzureOpenAI

SE, SK = os.environ["SEARCH_ENDPOINT"], os.environ["SEARCH_KEY"]
oai = AzureOpenAI(api_key=os.environ["AOAI_KEY"], api_version="2024-10-21",
                  azure_endpoint=os.environ["AOAI_ENDPOINT"])
INDEX = "campux-kb"

DOCS = [
  ("returns", "Campux Retail accepts returns within 30 days with a receipt. Perishable goods such as oat milk are non-returnable."),
  ("hours",   "The Camden store opens at 07:00 and closes at 20:00 on weekdays. Shoreditch opens at 08:00 and closes at 22:00."),
  ("loyalty", "The Campux loyalty card earns one point per pound spent. 100 points can be redeemed for a free coffee."),
]

def embed(text):
    return oai.embeddings.create(model="text-embedding-3-small", input=text).data[0].embedding

# create (or update) the vector index
fields = [
  SimpleField(name="id", type=SearchFieldDataType.String, key=True),
  SearchableField(name="content", type=SearchFieldDataType.String),
  SearchField(name="vector", type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
              searchable=True, vector_search_dimensions=1536,
              vector_search_profile_name="hnsw-profile"),
]
vs = VectorSearch(
  algorithms=[HnswAlgorithmConfiguration(name="hnsw")],
  profiles=[VectorSearchProfile(name="hnsw-profile", algorithm_configuration_name="hnsw")])
SearchIndexClient(SE, AzureKeyCredential(SK)).create_or_update_index(
  SearchIndex(name=INDEX, fields=fields, vector_search=vs))

# embed and upload
docs = [{"id": i, "content": t, "vector": embed(t)} for i, t in DOCS]
SearchClient(SE, INDEX, AzureKeyCredential(SK)).upload_documents(docs)
print(f"indexed {len(docs)} documents into {INDEX}")
EOF

python ingest.py
Checkpoint It prints indexed 3 documents into campux-kb. Your documents now live in Azure AI Search as vectors — the same shape whether it is three coffee-shop policies or thirty thousand pages of a bank's procedures.
Step 2

Ask: retrieve, then ground

The ask script embeds the question, runs a vector search for the nearest chunks, and hands only those to the chat model with a strict instruction: answer from the context, cite the source id, and refuse otherwise.

cat > ask.py <<'EOF'
import os, sys
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
from openai import AzureOpenAI

SE, SK = os.environ["SEARCH_ENDPOINT"], os.environ["SEARCH_KEY"]
oai = AzureOpenAI(api_key=os.environ["AOAI_KEY"], api_version="2024-10-21",
                  azure_endpoint=os.environ["AOAI_ENDPOINT"])
q = sys.argv[1]

qvec = oai.embeddings.create(model="text-embedding-3-small", input=q).data[0].embedding
sc = SearchClient(SE, "campux-kb", AzureKeyCredential(SK))
hits = sc.search(search_text=None, select=["id", "content"],
                 vector_queries=[VectorizedQuery(vector=qvec, k_nearest_neighbors=3, fields="vector")])
context = "\n".join(f"[{h['id']}] {h['content']}" for h in hits)

prompt = (
  "Answer the question using ONLY the context below. "
  "Cite the source id in square brackets. "
  "If the answer is not in the context, reply exactly: "
  "'Not found in the Campux documents.'\n\n"
  f"Context:\n{context}\n\nQuestion: {q}")
ans = oai.chat.completions.create(model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}], temperature=0)
print(ans.choices[0].message.content)
EOF

Ask it something the documents do cover:

python ask.py "Can I return oat milk I bought yesterday?"
Checkpoint You get a grounded answer with a citation — something like No. Perishable goods such as oat milk are non-returnable. [returns]. The model did not know your return policy; it read it from the retrieved chunk and told you where it came from.
Step 3

The real test: make it refuse

Now ask something that is nowhere in the knowledge base. A naive chatbot invents an answer; a grounded pipeline declines.

python ask.py "What is the Wi-Fi password at the Camden store?"
Checkpoint It replies exactly Not found in the Campux documents. — no invented password, no confident guess. That refusal is the deliverable: it is what lets a regulated business put this in front of customers or staff. You have just demonstrated the difference between a demo and something an auditor would sign off.
Down

Tear it down

Delete the resource group; it removes the search service and the Azure OpenAI resource together.

az group delete -n campux-lab-rag-rg --yes
az group exists -n campux-lab-rag-rg      # -> false
Checkpoint az group exists returns false. The free search service is released (so your one-per-subscription freebie is available again) and the model deployments are gone. Total spend: a fraction of a cent for the embeddings and answers.
End

What you can now honestly claim

You built a retrieval-augmented generation pipeline on Azure AI Search: you embedded documents with Azure OpenAI, stored and searched them as vectors, grounded a chat model in the retrieved passages with citations, and — the part most people skip — proved that it refuses to answer outside its sources. That is "implement RAG patterns using Azure AI Search, vector stores, and embeddings" from the job description, built rather than name-dropped. Together with the MCP lab you can now speak to both sides of AI platform engineering: grounding a model in enterprise data, and governing how it is reached. Very few candidates have done either; you have done both.

Footnotes
  1. Model availability varies by region and shifts over time. If a deployment is refused, the resource's region is out of capacity for that model — recreate the Azure OpenAI resource in swedencentral or eastus, or check the current model-availability table in the Azure docs and adjust the --model-version to a version offered there. The pipeline itself is unchanged; only the deployment target moves.
  2. This lab embeds whole short documents for clarity. Real corpora are split into overlapping chunks of a few hundred tokens before embedding, because retrieval quality depends on chunk size — too large and the relevant sentence is diluted, too small and it loses context. Chunking strategy is a genuine part of the RAG engineering job, not a detail.