AI-Powered Document Q&A on Azure
Build a RAG pipeline that answers questions from your own PDF documents.
Introduction
30 Second Summary
When you need an answer buried in a 50-page PDF, you don't want to scroll through every page. You want to ask a question and get a direct answer.
In this project, you will build a serverless Q&A API on Azure that lets you ask natural-language questions about your PDF documents and receive AI-generated answers drawn from their actual content.
What You'll Build
Picture this: you upload a PDF, send a question like "What is the refund policy?", and your API responds with a clear, synthesized answer pulled directly from the document text.
By the end of this project, you'll have:
- A live API endpoint that accepts any question and returns an AI-generated answer grounded in your own documents.
- A complete RAG pipeline connecting Azure Blob Storage, Azure AI Search, and Google Gemini into a multi-service architecture.
- A serverless Azure Function you can hit from any browser, curl, or Postman to get answers on demand.
- Secret Mission: Add source citations that show exactly which document and passage each answer came from.
Are there any prerequisites?
You'll need an Azure student subscription (the free $100 credit covers everything in this project), Python and VS Code installed on your machine, and Node.js installed for the Azure Functions tooling.
No prior Azure experience is required. The project walks you through every service from scratch.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Right now, searching a collection of PDF documents means opening each one and skimming for the right paragraph. The RAG (Retrieval-Augmented Generation) pattern solves this by combining document search with AI generation, so you can ask a plain-English question and get a direct answer drawn from your own files.
Set Up Your Development Environment
Your Q&A system will run as a serverless Azure Function that connects to multiple services. To develop and test this locally, you need Azure Functions Core Tools to run the function on your machine, and a Gemini API key to power the AI generation layer.
In this step, you will install the tools, grab your API key, and scaffold the project folder that holds your entire RAG pipeline.
In this step, get ready to:
- Install Azure Functions Core Tools and verify the installation.
- Obtain a Gemini API key from Google AI Studio.
- Create a Python v2 Azure Functions project and install its dependencies.
Install Azure Functions Core Tools
Azure Functions Core Tools lets you run and debug your serverless functions locally before deploying to Azure. You need version 4.x to match the current Azure Functions runtime.
- Check whether Azure Functions Core Tools is already installed by running this command in your terminal:
func --version
✔️ I see a version starting with 4.x
You already have the right version installed. Skip ahead to the next substep.
ⓧ I see an older version (below 4.x)
You need to upgrade to version 4. Follow the install instructions for your operating system below, which will replace the older version.
macOS
- Install version 4 using npm by running this command:
npm i -g azure-functions-core-tools@4
Windows
- Install version 4 using npm by running this command:
npm i -g azure-functions-core-tools@4 --unsafe-perm true
- Confirm the upgrade by running:
func --version
You should now see a version number starting with 4.
ⓧ Command not found
Azure Functions Core Tools is not installed yet. Follow the install instructions for your operating system below.
macOS
- Install Azure Functions Core Tools v4 using npm by running this command:
npm i -g azure-functions-core-tools@4
Windows
- Install Azure Functions Core Tools v4 using npm by running this command:
npm i -g azure-functions-core-tools@4 --unsafe-perm true
- Confirm the installation by running:
func --version
You should see a version number starting with 4. This confirms Core Tools is ready.
Still seeing command not found?
Close your terminal and open a new one. The PATH update sometimes requires a fresh terminal session.
help me troubleshoot why func is not found after installing Azure Functions Core Tools
Get a Gemini API key from Google AI Studio
The AI generation layer of your RAG pipeline uses Google Gemini to turn raw document passages into natural-language answers. You need an API key to authenticate with the Gemini API.
- Open Google AI Studio in your browser.
- Sign in with your Google account.
- Click Get API Key in the left sidebar.
- Click Create API Key.
- Copy the key and save it somewhere safe (a text file or password manager). You will paste it into your project configuration shortly.
Why Gemini?
Gemini 2.5 Flash is free-tier eligible, which means this project costs nothing to run. The architecture is vendor-portable. If you later want to use Azure OpenAI instead, you only swap one SDK call.
The google-genai SDK automatically reads your API key from the GEMINI_API_KEY environment variable, so you never hard-code secrets in your source files.
Keep this key private
Never commit your API key to version control. You will store it in local.settings.json, which is already listed in .gitignore by default in Azure Functions projects.
Create your Azure Functions project and install dependencies
The project scaffold gives you all the boilerplate files an Azure Function needs to run locally. You will initialize it using the Python v2 programming model, which uses decorators to define triggers and bindings in a single file.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Azure Functions project by running this command:
func init document-qa --worker-runtime python --model V2
What does this command do?
func init scaffolds a new Azure Functions project in a folder called document-qa. The --worker-runtime python flag tells Core Tools to generate Python files. The --model V2 flag selects the decorator-based programming model where triggers and bindings live in your code, not in separate JSON files.
- Move into the new project folder by running:
cd document-qa
- Confirm the scaffold created your project files by running:
ls
You should see function_app.py, host.json, local.settings.json, and requirements.txt in the output.
- Create a Python virtual environment by running:
python -m venv .venv
- Activate the virtual environment:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
Your terminal prompt now shows (.venv) at the beginning, confirming the virtual environment is active.
Why a virtual environment?
Azure Functions Core Tools requires Python projects to run inside a virtual environment. It also keeps your project's packages isolated from your system Python, preventing version conflicts between projects.
- Open requirements.txt in VS Code and replace its contents with the following dependencies:
azure-functions
azure-search-documents
azure-core
google-genai
What are these packages?
- azure-functions is the core SDK for building Azure Functions in Python.
- azure-search-documents lets you query Azure AI Search indexes from Python.
- azure-core provides shared authentication and HTTP transport used by Azure SDKs.
- google-genai is Google's official SDK for calling Gemini models.
- Save requirements.txt and install all dependencies by running:
pip install -r requirements.txt
You should see pip downloading and installing each package. The final line says Successfully installed followed by the package names.
Seeing a permission error or install failure?
Make sure your terminal prompt shows (.venv) at the start. If it does not, the virtual environment is not active. Re-run the activation command for your OS above.
help me fix a pip install failure in my Azure Functions virtual environment
- Open local.settings.json in VS Code and replace its entire contents with this configuration:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python",
"SEARCH_ENDPOINT": "https://your-search-service.search.windows.net",
"SEARCH_API_KEY": "your-search-api-key-here",
"SEARCH_INDEX_NAME": "your-index-name",
"GEMINI_API_KEY": "your-gemini-api-key-here"
}
}
- Replace your-gemini-api-key-here with the actual API key you copied from Google AI Studio.
- Leave the SEARCH_ values as placeholders for now. You will fill them in after creating your Azure AI Search service in Step 2.
- Save local.settings.json.
What does local.settings.json do?
When you run your function locally with func start, Core Tools loads all values in this file as environment variables. Your code reads them with os.environ, keeping secrets out of source code.
Before you move on, predict what you will see when you run func --version one more time. Will it still show the same version?
- Confirm everything is in place by running:
func --version
You should see a version number starting with 4. confirming Core Tools is ready to run your project.
✔️ Awesome, I've got everything!
Great. Double check you have saved both requirements.txt and local.settings.json.
ⓧ I'd like to double check the full code
Scroll up and confirm each file is present in your document-qa folder. Here is what each file should contain:
function_app.py:
import azure.functions as func
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
requirements.txt:
azure-functions
azure-search-documents
azure-core
google-genai
local.settings.json:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python",
"SEARCH_ENDPOINT": "https://your-search-service.search.windows.net",
"SEARCH_API_KEY": "your-search-api-key-here",
"SEARCH_INDEX_NAME": "your-index-name",
"GEMINI_API_KEY": "your-gemini-api-key-here"
}
}
host.json:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
Your development environment is ready. Next up, you will upload PDF documents to Azure Blob Storage and configure Azure AI Search to automatically index their content.
Upload Documents and Index Them with Azure AI Search
Your Azure Function project is ready locally, but it has nothing to search yet. Before your Q&A system can answer questions, it needs documents stored somewhere accessible and a search engine that has extracted their text into a queryable index.
In this step, you will upload PDF documents to Azure Blob Storage and then configure Azure AI Search to automatically extract and index their text content. The search service's blob indexer handles all the PDF parsing for you.
In this step, get ready to:
- Create a Blob Storage container and upload sample PDF documents.
- Create an Azure AI Search service and configure a blob indexer.
- Verify indexed content using Search Explorer.
Create a Blob Storage container and upload PDF documents
Your PDFs need a home in the cloud where Azure AI Search can reach them. Blob Storage is Azure's object storage service designed for exactly this.
- Sign in to the Azure Portal at portal.azure.com.
- In the portal search bar at the top, type Storage accounts and select it from the results.
- Click Create.
- On the Basics tab, select your subscription and resource group (create a new one if needed).
- Enter a unique storage account name (lowercase letters and numbers only, 3-24 characters).
- Select the region closest to you.
- Leave Performance set to Standard and Redundancy set to Locally-redundant storage (LRS).
- Click Review + create, then click Create.
- When deployment completes, click Go to resource.
You should now see your storage account's overview page in the portal.
- In the left menu, scroll to the Data storage section and select Containers.
- Click the + Container button.
- Enter documents as the container name.
- Leave the anonymous access level as Private (no anonymous access).
- Click Create.
Your new documents container should now appear in the list. Now upload your sample PDFs.
- Click on the documents container to open it.
- Click the Upload button.
- Browse your local file system and select 2-3 PDF files. Use any text-based PDFs you have on hand (a company handbook, a product manual, or course notes all work well).
- Click Upload to upload them.
Your uploaded PDFs now appear listed inside the container.
Choose PDFs with searchable text
The blob indexer extracts embedded text from PDFs. Scanned-image-only PDFs (with no selectable text) will not yield useful results without an OCR skillset. Pick PDFs where you can highlight and copy text on your computer.
Don't see your container?
Make sure you are inside your storage account (not still on the Storage accounts list page). The Containers option appears under Data storage in the left menu of the storage account.
Help me find my container in Azure Blob Storage
Create an Azure AI Search service and configure a blob indexer
With documents in Blob Storage, you need a search engine that can crack open those PDFs and build a text index. Azure AI Search provides a built-in blob indexer that does this automatically.
- In the portal search bar at the top, type AI Search and select AI Search from the results.
- Click Create.
- Select your subscription and the same resource group you used for storage.
- Enter a unique service name (this becomes part of your endpoint URL).
- Select the same region as your storage account.
- Under Pricing tier, select Free.
- Click Review + create, then click Create.
- Wait for deployment to complete (this can take 1-2 minutes), then click Go to resource.
What is the Free tier?
The Azure AI Search Free tier gives you 50 MB of storage, up to 10,000 documents, and 3 indexes at no cost. This is more than enough for a learning project with a few PDF files.
Now use the Import data wizard to connect the search service to your Blob Storage container and create the indexer.
- On your search service's Overview page, click Import data.
- Select Azure Blob Storage as the data source.
- Select Keyword search.
- On the Connect to your data page, select your Azure subscription.
- Select your storage account, then select the documents container.
- Click Next.
What does dataToExtract mean?
The blob indexer uses a setting called dataToExtract to control what gets indexed. With the value contentAndMetadata, the indexer extracts all text content from your PDFs plus file metadata like the filename and last modified date.
The Import data wizard sets this by default, so you do not need to configure it manually.
- On the index configuration page, review the generated schema. The wizard automatically creates a content field for extracted text and various metadata fields.
- Click Next to skip advanced settings (semantic ranking and scheduling are not needed for this project).
- On the final review page, note the Objects name prefix. This is the name used for your index, indexer, and data source objects. Write it down.
- Click Create to create the index, indexer, and data source, and to start the indexer immediately.
The indexer now runs in the background, extracting text from your PDFs. This takes about 30-60 seconds for a few small files.
Seeing a connection error?
If the wizard cannot connect to your storage account, make sure both services are in the same region. Also check that your storage account has not disabled public network access.
Help me troubleshoot the Import data connection
Run the indexer and verify results in Search Explorer
The indexer should have run automatically when you created it. Let's confirm it succeeded and then test a search query against your indexed documents.
- In your search service, select Search management in the left pane, then select Indexers.
- Find your indexer in the list and check its status. You should see Success with a document count matching your number of uploaded PDFs.
What if the status shows In progress?
Wait about 30 seconds and refresh the page. For a few small PDFs, indexing usually completes within a minute.
- In the left pane, select Search management then Indexes.
- Select your index from the list.
- Select the Search explorer tab.
Before you run your first query, think about what you expect to see. If you search for a keyword from one of your PDFs, do you think the service will return the full text of the document or just matching passages?
- In the search bar, type a keyword you know exists in one of your uploaded PDFs (for example, a term from a heading or key paragraph).
- Click Search.
You should see JSON results containing a content field with extracted text from your PDF that matches your keyword. The search returns the full document's extracted text, not just the matching sentence.
Why does it return full documents?
By default, the blob indexer creates one search document per PDF. Each result contains the entire extracted text of that PDF in the content field. Your Azure Function will later use this to send relevant passages to Gemini for answer generation.
Before you move on, collect the connection details you will need in the next step. These go into your local.settings.json file.
- On your search service's Overview page, copy the Url (it looks like https://your-service.search.windows.net). This is your search endpoint.
- In the left pane, go to Settings then Keys. Copy either admin key (the primary or secondary key both work).
- Go back to Search management then Indexes and note the name of your index.
Save these values somewhere accessible
You will paste the search endpoint, API key, and index name into your local.settings.json in the next step. Keep them in a note or clipboard.
Your documents are now stored, indexed, and searchable. Next up, you will wire your Azure Function to query this index programmatically and return those raw search results as an API response.
Build a Search-Only API Endpoint
You have documents indexed and searchable in Azure AI Search. The next challenge is giving your application a way to query that index programmatically.
In this step, you will write an Azure Function that accepts a question, queries the search index, and returns the matching document passages. This exposes a core limitation you will fix in the next step: search returns raw text chunks, not a usable answer.
In this step, get ready to:
- Write an HTTP-triggered function that queries Azure AI Search.
- Configure your real search connection settings.
- Test the endpoint locally and observe the raw search output.
Write the HTTP-triggered function
Your function needs to accept a POST request containing a question, validate the input, and then use that question to search your index. You will build this in two chunks.
- In VS Code, open function_app.py and replace its entire contents with the following code:
import azure.functions as func
import logging
import json
import os
from azure.search.documents import SearchClient
from azure.core.credentials import AzureKeyCredential
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.route(route="ask", methods=["POST"])
def ask(req: func.HttpRequest) -> func.HttpResponse:
logging.info("Document Q&A function triggered.")
# Parse the question from the request body
try:
req_body = req.get_json()
question = req_body.get("question")
except ValueError:
return func.HttpResponse(
json.dumps({"error": "Please provide a JSON body with a 'question' field."}),
status_code=400,
mimetype="application/json"
)
# Reject empty questions
if not question:
return func.HttpResponse(
json.dumps({"error": "The 'question' field cannot be empty."}),
status_code=400,
mimetype="application/json"
)
What does this code do?
- The imports bring in the Azure Functions SDK, the SearchClient for querying your index, and standard libraries for JSON and environment variables.
- The @app.route decorator registers an HTTP endpoint at /api/ask that only accepts POST requests.
- The try/except block safely parses the JSON body, returning a 400 error if the request is malformed or the question is missing.
- Save function_app.py.
- Confirm the function registers by starting the local server:
func start
You should see output that includes your function URL, something like:
Functions:
ask: [POST] http://localhost:7071/api/ask
- Press Ctrl+C to stop the server. You will restart it after adding the search logic.
Don't see the ask function listed?
- Make sure your virtual environment is activated. The function host needs access to the installed packages.
- Check that the @app.route decorator is directly above the def ask line with no blank lines between them.
- If you see a syntax error, double-check that the parentheses on each func.HttpResponse( call are properly closed.
Why isn't my Azure Function starting?
Now add the search logic that queries your index and returns matching passages.
- In function_app.py, find the closing parenthesis of the if not question: block (the last line you just pasted). Add the following code directly below it, inside the ask function:
search_endpoint = os.environ["SEARCH_ENDPOINT"]
search_key = os.environ["SEARCH_API_KEY"]
index_name = os.environ["SEARCH_INDEX_NAME"]
search_client = SearchClient(
endpoint=search_endpoint,
index_name=index_name,
credential=AzureKeyCredential(search_key)
)
results = search_client.search(
search_text=question,
top=3,
select=["content"]
)
context_passages = []
for result in results:
content = result.get("content", "")
if content:
context_passages.append(content[:2000])
if not context_passages:
return func.HttpResponse(
json.dumps({"answer": "No relevant documents found for your question.", "sources_consulted": 0}),
status_code=200,
mimetype="application/json"
)
return func.HttpResponse(
json.dumps({"passages": context_passages}),
status_code=200,
mimetype="application/json"
)
What does this code do?
- It reads the search endpoint, API key, and index name from environment variables you will configure next.
- The SearchClient connects to your Azure AI Search service and runs a full-text search using the user's question.
- It takes the top 3 results, extracts the content field (the PDF text your indexer extracted), and caps each passage at 2000 characters.
- The response returns the raw passages as a JSON array. No AI processing happens yet.
- Save function_app.py.
✔️ Awesome, I've got everything!
Great. Double-check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Your function_app.py should contain these sections in order. Scroll up and confirm each one is present:
- Six import lines (azure.functions, logging, json, os, SearchClient, AzureKeyCredential).
- The app instance with AuthLevel.ANONYMOUS.
- The @app.route(route="ask", methods=["POST"]) decorator and def ask function.
- Request body parsing with a try/except and the if not question guard.
- Three os.environ reads and a SearchClient that calls .search().
- A loop collecting context_passages, an empty-results guard, and a final return with json.dumps({"passages": context_passages}).
Configure your search connection settings
Your function reads connection details from environment variables defined in local.settings.json. You need to replace the placeholder values with your real Azure AI Search credentials.
- In the Azure Portal, navigate to your Azure AI Search service.
- On the Overview page, copy the Url field (it looks like https://your-service-name.search.windows.net).
- In the left sidebar, click Keys under Settings.
- Copy one of the admin keys listed there.
- Note your index name from the Indexes page (the one your indexer created in Step 2).
- Open local.settings.json in VS Code and find these three lines:
"SEARCH_ENDPOINT": "https://your-search-service.search.windows.net",
"SEARCH_API_KEY": "your-search-api-key-here",
"SEARCH_INDEX_NAME": "your-index-name"
- Replace each placeholder value with the real values you just copied from the Azure Portal.
- Save local.settings.json.
Keep local.settings.json out of source control
This file contains your API key. The .gitignore generated by func init already excludes it, so it will not be committed to Git.
Test locally and observe the raw output
Everything is wired up. Time to start the function and send it a real question.
- Start the local Functions host by running:
func start
Wait until you see the ask: [POST] http://localhost:7071/api/ask line confirming the function is ready.
Before you send a request, think about what you expect to get back. You are sending a natural-language question to a search engine. Will you get a neatly worded answer, or something else?
- Open a second terminal window (keep the function host running in the first).
- Send a test question to your endpoint by running:
curl -X POST http://localhost:7071/api/ask -H "Content-Type: application/json" -d '{"question": "What is the refund policy?"}'
What should I see?
You will get back a JSON response containing a passages array. Each entry is a raw block of text pulled straight from your PDF documents.
The passages are unformatted excerpts. They contain relevant information, but they are not a usable answer to your question. A user would have to read through paragraphs of text and piece together the answer themselves.
This is the core limitation of search alone. You have the retrieval part of the RAG pattern working, but without the generation layer, users get raw data instead of a clear answer. The next step fixes this by feeding these passages into Gemini.
Getting a connection error or empty results?
- If you see a KeyError mentioning SEARCH_ENDPOINT, make sure you saved local.settings.json and restarted the function host with func start.
- If you get an authentication error, verify your SEARCH_API_KEY is an admin key, not the query key.
- If the passages array is empty, try a different question using keywords you know appear in your uploaded PDFs.
Why is my search function returning errors or no results?
Your search endpoint works. You can retrieve relevant document text for any question. But the response is a wall of unprocessed excerpts. Next, you will add Google Gemini to transform those raw passages into a clear, conversational answer.
Add the Gemini RAG Layer
In the last step, you hit the core limitation of search alone. Your function returned raw text chunks from the index, forcing the user to piece together an answer from disconnected paragraphs.
This step transforms those raw passages into a polished, conversational answer. You will feed the retrieved context plus the user's question into Google Gemini using a grounding prompt. The model reads only your document context and synthesizes a direct response.
In this step, get ready to:
- Add the Gemini SDK call that constructs a grounding prompt and generates answers from search results.
- Format the API response as JSON with the answer and metadata.
- Test with multiple questions to confirm answers are grounded in your document content.
Add the Gemini SDK and construct a grounding prompt
The RAG pattern has two halves: retrieval (done in Step 3) and generation (this step). You already have the retrieval logic that pulls relevant passages from your index. Now you will join those passages into a single context block, wrap them in a prompt that instructs Gemini to answer only from the provided text, and return the generated answer.
- In function_app.py, find the import lines at the top of the file.
- Add this new import below the existing AzureKeyCredential import line:
from google import genai
- Scroll down to the end of the ask function. Find this return statement:
return func.HttpResponse(
json.dumps({"passages": context_passages}),
status_code=200,
mimetype="application/json"
)
- Delete that entire return block. In its place, paste the following code that joins the passages into a context block and builds a grounding prompt:
# Join the retrieved passages into a single context block
context = "\n\n---\n\n".join(context_passages)
# Build a grounding prompt that restricts Gemini to document content
prompt = f"""You are a helpful assistant that answers questions based only on the provided document context.
If the answer is not found in the context, say "I cannot find this information in the available documents."
Document context:
{context}
Question: {question}
Answer:"""
What does this code do?
- context = "\n\n---\n\n".join(context_passages) merges the top 3 search results into one text block separated by dividers, giving Gemini all the relevant material at once.
- The prompt variable wraps the context and question in explicit instructions. Gemini is told to answer only from the provided text, which prevents hallucination.
- Directly below the prompt you just pasted, add this code that calls Gemini and returns the formatted answer:
# Call Gemini with the grounding prompt
gemini_client = genai.Client()
response = gemini_client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt
)
answer = response.text
# Return the answer with metadata
return func.HttpResponse(
json.dumps({
"question": question,
"answer": answer,
"sources_consulted": len(context_passages),
"model": "gemini-2.5-flash"
}),
status_code=200,
mimetype="application/json"
)
What does this code do?
- genai.Client() automatically reads your GEMINI_API_KEY environment variable. No extra key configuration is needed in code.
- generate_content sends the grounding prompt to gemini-2.5-flash and returns Gemini's response.
- The JSON response includes sources_consulted (how many passages Gemini read) and model (which model generated the answer) so consumers know what powered the response.
- Save function_app.py.
Seeing a syntax error on save?
- Make sure the indentation of the new code matches the rest of the function (4 spaces per indent level inside the function body).
- Check that the triple-quoted f-string (f"""...""") has both opening and closing triple quotes.
- Confirm you deleted the entire old return func.HttpResponse(json.dumps({"passages": context_passages}), ...) block. Leftover lines cause indentation conflicts.
Ask for help: My function_app.py has a syntax error after adding the Gemini code. Can you help me fix it?
✔️ Awesome, I've got everything!
Great. Double check you have saved function_app.py before moving on.
ⓧ I'd like to double check the full code
Your function_app.py is longer than 30 lines, so scroll through and confirm each section is present in order:
- Imports (including from google import genai).
- Request parsing and validation (the try/except block and the empty-question check).
- Azure AI Search query (creating SearchClient and building context_passages).
- Gemini RAG layer (joining context, building the prompt, calling generate_content, returning the JSON response).
Test the RAG-powered endpoint
Time to see the transformation. You will send the same kind of question you sent in Step 3, but this time the response should be a clear, natural-language answer instead of raw text chunks.
- If func start is still running from Step 3, stop it with Ctrl+C.
- Start the function host again by running:
func start
Before you send a request, predict what you expect to see. Will the response still be raw passages, or something different?
- Send a question about your uploaded documents by running this command (replace the question with something relevant to your PDFs):
curl -X POST http://localhost:7071/api/ask -H "Content-Type: application/json" -d "{\"question\": \"What is the refund policy?\"}"
Instead of the raw passages array from Step 3, you now receive a JSON object with a clear answer field containing a natural-language response that directly addresses your question.
What should I see?
Your response looks something like this (the exact answer depends on your PDF content):
{
"question": "What is the refund policy?",
"answer": "Based on the documents, refunds are available within 30 days of purchase...",
"sources_consulted": 3,
"model": "gemini-2.5-flash"
}
The answer field is a synthesized response drawn from your actual documents. The sources_consulted field shows how many passages Gemini used as context.
- Send a second, different question to confirm the answers change based on document content:
curl -X POST http://localhost:7071/api/ask -H "Content-Type: application/json" -d "{\"question\": \"Who is the author of this document?\"}"
You should see a completely different answer that reflects the actual content of your documents. If your PDFs do not contain author information, Gemini responds with something like "I cannot find this information in the available documents."
- Try one more question on a topic you know is NOT in your documents. Gemini should decline to answer rather than making something up.
Getting an error about GEMINI_API_KEY?
- Confirm that GEMINI_API_KEY is set in your local.settings.json under the Values object with your real key from Google AI Studio.
- Make sure you restarted func start after editing local.settings.json. The function host only reads settings at startup.
- Verify your API key is active by visiting aistudio.google.com and checking the key status.
Ask for help: I am getting an API key error when calling Gemini from my Azure Function. How do I fix it?
Your RAG pipeline is complete. You have a working endpoint that retrieves relevant document passages and transforms them into clear, grounded answers. Next up, you will take on the Secret Mission and add source citations to make every answer traceable back to its original document.
Secret mission
Add Source Citations
Your Q&A system gives great answers, but users have no way to verify where the information came from. In production RAG systems, source citations build trust and make the system auditable. Can you modify the pipeline to show exactly which document and passage backs each claim?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Azure AI Search's Free tier ($0/month) and minimal Blob Storage (pennies on your student subscription), so there is no urgency to delete.
Resources you used:
- Azure AI Search service (Free tier).
- Azure Storage account with the documents blob container and uploaded PDFs.
- Azure Functions Core Tools (npm global package).
- Local Azure Functions project folder on your Desktop (including the Python virtual environment).
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment further with your RAG pipeline.
- Your Azure AI Search Free tier service stays running at $0/month. There is no time limit beyond the 12-month new-customer window for Azure free services.
- Your Blob Storage container holds a few small PDFs. Storage costs are negligible on a student subscription.
- Azure Functions Core Tools remains installed globally via npm. It takes minimal disk space.
- Your local project folder stays on your Desktop, ready to pick up where you left off.
Pause - I'll come back to this later
There is no pause option for Azure AI Search's Free tier. It stays running at $0, so there is nothing to shut down.
- Stop the local Functions host if it is still running by pressing Ctrl+C in your terminal.
- Your Azure resources remain in place at no cost. You can return and run func start again whenever you are ready.
Delete - I don't want to use this again
Remove all project resources and start fresh. Deleting the Azure resource group removes both AI Search and the Storage account in one action.
- In the Azure Portal, search for Resource groups in the top search bar.
- Select the resource group that contains your AI Search service and Storage account.
- Click Delete resource group at the top of the page.
- Type the resource group name to confirm, then click Delete.
- Uninstall Azure Functions Core Tools by running:
npm uninstall -g azure-functions-core-tools
- Delete your local project folder. Run the command for your operating system:
macOS
cd ~/Desktop
rm -rf your-project-folder
Windows
cd ~\Desktop
rmdir /s /q your-project-folder
What about my Gemini API key?
Your Gemini API key lives in your Google AI Studio account. Deleting your local project files does not revoke the key. If you want to revoke it, visit aistudio.google.com, open your API keys list, and delete the key from there.
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-style document Q&A system powered by RAG on Azure.
You've learned how to:
- Built a complete RAG pipeline connecting Azure Blob Storage, Azure AI Search, and Google Gemini into a working document Q&A system.
- Configured a blob indexer to automatically extract and index text from PDF documents without writing any custom parsing code.
- Created a serverless Azure Function API that retrieves relevant document passages and uses prompt engineering to generate grounded, natural-language answers.
- Secret Mission: Added source citations that make the system auditable and trustworthy for enterprise use.
Ready to quiz yourself?