Serverless RAG with Bedrock Knowledge Bases and S3 Vectors
, 4 min read
Retrieval-augmented generation (RAG) lets a model answer from your documents rather than only from what it learned in training. You find the passages most relevant to a question, then give them to the model along with the question.
Building that yourself means parsing files, splitting them into chunks, creating embeddings, storing vectors and keeping everything in sync. Amazon Bedrock Knowledge Bases handles those steps. This guide covers a mostly pay-per-use setup using S3 Vectors. If you haven't called Bedrock from Lambda before, start with Calling Amazon Bedrock from AWS Lambda.
The moving parts
- S3 bucket: holds your source documents.
- Knowledge base: parses and chunks the documents, then calls an embedding model.
- Vector store: holds the embeddings and answers similarity searches.
- Lambda function: takes a question and calls the knowledge base.
Choosing a vector store
This is the decision with the biggest effect on cost and search quality. The main serverless options are:
- S3 Vectors: the lowest-effort choice. Pricing is based on uploads, storage and queries, which suits workloads with low or uneven traffic. Queries are subsecond, but it only supports semantic search.
- OpenSearch Serverless: a better fit for high query volumes or when you need keyword matching alongside semantic search. That matters if users search for exact terms, such as product codes. Check how it bills for capacity first, because it can cost more than an idle prototype justifies.
- Aurora PostgreSQL Serverless: worth considering if your application already runs on PostgreSQL and you want your vectors alongside relational data.
A sensible approach is to start with S3 Vectors and move only if testing shows you need something else. AWS has a guide to choosing a vector store with more detail.
Setting it up
The console's quick-create flow is the fastest way to start:
- Create a knowledge base and choose S3 as the data source.
- Choose a parser and a chunking strategy.
- Choose an embedding model, and S3 Vectors as the store.
- Sync the data source, then try some questions in the console's test panel.
Some of these choices are hard to change later:
- Parsing and chunking are fixed. They can't be modified after creation, so try them on a sample of your real documents first.
- Dimensions must match. The index dimension has to match the embedding model, or ingestion will fail.
- Metadata is limited. Filterable metadata is capped at 2 KB per vector. If you create the index yourself, add
AMAZON_BEDROCK_TEXTtononFilterableMetadataKeysso that larger text chunks fit. This setting also can't be changed later.
Asking questions
RetrieveAndGenerate does the search and the model call in one request, and returns citations.
import json
import os
import boto3
client = boto3.client("bedrock-agent-runtime")
KNOWLEDGE_BASE_ID = os.environ["KNOWLEDGE_BASE_ID"]
MODEL_ARN = os.environ["MODEL_ARN"]
def handler(event, context):
body = json.loads(event.get("body") or "{}")
question = body.get("question")
if not question:
return {"statusCode": 400, "body": json.dumps({"error": "question is required"})}
request = {
"input": {"text": question},
"retrieveAndGenerateConfiguration": {
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": {
"knowledgeBaseId": KNOWLEDGE_BASE_ID,
"modelArn": MODEL_ARN,
"retrievalConfiguration": {
"vectorSearchConfiguration": {"numberOfResults": 5}
},
},
},
}
# Bedrock creates the session ID. Reuse it for follow-up questions.
if body.get("sessionId"):
request["sessionId"] = body["sessionId"]
response = client.retrieve_and_generate(**request)
sources = {
reference["location"]["s3Location"]["uri"]
for citation in response.get("citations", [])
for reference in citation.get("retrievedReferences", [])
if "s3Location" in reference.get("location", {})
}
return {
"statusCode": 200,
"body": json.dumps(
{
"answer": response["output"]["text"],
"sources": sorted(sources),
"sessionId": response["sessionId"],
}
),
}
MODEL_ARN can be a foundation model or an inference profile. For a profile, the format is arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-sonnet-5. The function's role needs permission to retrieve from the knowledge base, generate a response and invoke the model.
If you want more control over the prompt, call Retrieve to get the passages alone, then send them to the model yourself with Converse. Start with RetrieveAndGenerate, and switch if you need custom formatting or extra steps in between.
Limiting what each user can see
A single knowledge base often holds documents that not everyone should read. To filter results, add a metadata file next to each document. For handbook.pdf, that's handbook.pdf.metadata.json:
{
"metadataAttributes": {
"department": "legal"
}
}
Then apply a filter in vectorSearchConfiguration:
"vectorSearchConfiguration": {
"numberOfResults": 5,
"filter": {"equals": {"key": "department", "value": user_department}},
}
Take user_department from a verified source, such as the claims in the user's token, and never from the request body. Otherwise, anyone can change it and read another department's documents.
Keeping documents in sync
Adding a file to S3 doesn't update the index. You need to run a sync, which is an ingestion job. A small function can start one on a schedule with EventBridge:
import os
import boto3
client = boto3.client("bedrock-agent")
def handler(event, context):
job = client.start_ingestion_job(
knowledgeBaseId=os.environ["KNOWLEDGE_BASE_ID"],
dataSourceId=os.environ["DATA_SOURCE_ID"],
)
return {"ingestionJobId": job["ingestionJob"]["ingestionJobId"]}
A schedule is usually better than triggering on every S3 upload, especially when files arrive in batches. Check the job's status too, because individual files can fail while the rest succeed.
Before you ship
- Test with real questions. Collect 20 or so questions with known answers from your own documents, and rerun them whenever you change settings. Check that the right passages came back, not only that the answer sounds good.
- Show citations in your UI. Users can check an answer against its source, and you can spot bad retrievals sooner.
- Treat documents as untrusted input. Text inside a document can try to instruct the model. Be especially careful if the documents come from users or the public web, and if the model's output can trigger any actions.
- Plan for "no answer". Tell the model to say when the documents don't cover a question. You can set this with
promptTemplateingenerationConfiguration, keeping the$search_results$placeholder so the retrieved passages are still included.
AWS service details and limits were checked against the AWS documentation in November 2026. Bedrock changes quickly, so confirm model IDs, pricing and availability for your Region.