NovuSpark
All articles
AWSDecember 5, 2025 · NovuSpark Team

Amazon Bedrock Knowledge Bases: Building RAG Without Managing Infrastructure

This is the second post in our Amazon Bedrock series, building on getting started with Bedrock. It also assumes the RAG concepts from our OpenAI and LangChain series.

We've now built a RAG pipeline twice in this blog — first from raw embeddings and manual similarity search, then again with LangChain's document loaders, text splitters, and vector stores. Both versions require you to actually run and maintain the underlying infrastructure: a vector database, an embedding pipeline, a re-indexing process for updated documents. A Bedrock Knowledge Base manages that entire infrastructure layer for you, directly integrated with S3 as the document source.

Setting up a Knowledge Base

aws bedrock-agent create-knowledge-base \
  --name "novuspark-handbook-kb" \
  --role-arn "arn:aws:iam::008971632408:role/bedrock-kb-role" \
  --knowledge-base-configuration '{
    "type": "VECTOR",
    "vectorKnowledgeBaseConfiguration": {
      "embeddingModelArn": "arn:aws:bedrock:eu-west-2::foundation-model/amazon.titan-embed-text-v2:0"
    }
  }' \
  --storage-configuration '{
    "type": "OPENSEARCH_SERVERLESS",
    "opensearchServerlessConfiguration": {
      "collectionArn": "arn:aws:aoss:eu-west-2:008971632408:collection/abc123",
      "vectorIndexName": "handbook-index",
      "fieldMapping": {
        "vectorField": "embedding",
        "textField": "text",
        "metadataField": "metadata"
      }
    }
  }'
aws bedrock-agent create-data-source \
  --knowledge-base-id "KB123ABC" \
  --name "handbook-docs" \
  --data-source-configuration '{
    "type": "S3",
    "s3Configuration": {
      "bucketArn": "arn:aws:s3:::novuspark-internal-docs"
    }
  }'

This is the exact same underlying architecture as the RAG pipelines built earlier in this blog — an embedding model (Amazon Titan Embeddings here, specified by ARN, the same "provider behind a consistent interface" swap covered in our LangChain series), a vector store (OpenSearch Serverless, in this configuration), and a document source. The genuine difference: AWS provisions and operates the vector store itself — no self-managed Chroma instance, no separately-run vector database to patch, scale, or back up.

S3 bucketingestion jobchunk + embed, managedOpenSearch ServerlessAWS-operated, not self-managedretrieve_and_generate() — one call
Fig. 1 — the same RAG architecture as our earlier hand-built pipelines, with AWS operating the vector store and ingestion pipeline

Ingestion: chunking and embedding, managed automatically

aws bedrock-agent start-ingestion-job \
  --knowledge-base-id "KB123ABC" \
  --data-source-id "DS456DEF"

Triggering an ingestion job is the managed equivalent of the manual "load documents, split into chunks, embed each chunk" pipeline built by hand in our LangChain RAG post — Bedrock handles document loading directly from S3, chunking (with configurable chunk size and overlap, the same trade-off flagged in our earlier RAG posts), and embedding, without you writing or maintaining that pipeline code yourself. Adding a new document is as simple as uploading it to the source S3 bucket and re-running an ingestion job — no code deployment required for new content.

Choosing a chunking strategy

Bedrock exposes several built-in chunking strategies, worth choosing deliberately rather than accepting whatever default a first setup happened to use:

{
  "chunkingConfiguration": {
    "chunkingStrategy": "SEMANTIC",
    "semanticChunkingConfiguration": {
      "maxTokens": 300,
      "bufferSize": 1,
      "breakpointPercentileThreshold": 95
    }
  }
}

FIXED_SIZE chunking (a straightforward token-count split, with configurable overlap) is the direct equivalent of the RecursiveCharacterTextSplitter from our LangChain post. SEMANTIC chunking is a more sophisticated option: it splits based on genuine shifts in meaning between sentences, rather than a fixed token count — grouping sentences that discuss the same idea into one chunk, and starting a new chunk where the topic actually shifts. This tends to produce more coherent, self-contained chunks for documents with clearly distinct sections, at the cost of some additional processing during ingestion — worth evaluating against fixed-size chunking on your own actual documents rather than assuming one strategy is universally better.

Querying and generating in one call

client = boto3.client("bedrock-agent-runtime", region_name="eu-west-2")
 
response = client.retrieve_and_generate(
    input={"text": "How many vacation days do employees get?"},
    retrieveAndGenerateConfiguration={
        "type": "KNOWLEDGE_BASE",
        "knowledgeBaseConfiguration": {
            "knowledgeBaseId": "KB123ABC",
            "modelArn": "arn:aws:bedrock:eu-west-2::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
        },
    },
)
 
print(response["output"]["text"])
According to the employee handbook, full-time employees receive 25
days of paid vacation per year, accrued monthly.

retrieve_and_generate collapses the entire retrieve-then-generate pattern — built manually with raw embeddings, then again with LangChain's composed chains, earlier in this blog — into a single API call: retrieval against the Knowledge Base, followed by generation using the retrieved context, both handled server-side by Bedrock.

Inspecting citations, not just the final answer

A production application generally needs to show which source a claim actually came from, the same requirement covered for LangChain's RunnableParallel-based sources pattern in an earlier post:

for citation in response["citations"]:
    for reference in citation["retrievedReferences"]:
        print(reference["location"]["s3Location"]["uri"], reference["content"]["text"][:100])
s3://novuspark-internal-docs/employee-handbook.pdf Full-time employees receive 25 days of paid vacation per year, accrued monthly...

retrieve_and_generate returns structured citations alongside the generated text automatically — each one tracing directly back to the specific S3 object and passage that grounded a given part of the answer, without you needing to separately track that mapping yourself the way a self-managed pipeline would require.

Retrieval only, when you need more control over generation

response = client.retrieve(
    knowledgeBaseId="KB123ABC",
    retrievalQuery={"text": "How many vacation days do employees get?"},
)
 
for result in response["retrievalResults"]:
    print(result["content"]["text"], result["score"])

For applications that need to combine retrieved context with additional custom logic before generation — the multi-agent coordination patterns covered in our LangGraph series, for instance — the separate retrieve call returns just the relevant passages and their similarity scores, leaving the generation step, and everything you do with the retrieved context before that step, fully under your own application's control.

The genuine trade-off versus a self-managed pipeline

A Bedrock Knowledge Base trades some flexibility for meaningfully less operational burden. Fine-grained control over chunking strategy, a custom re-ranking step between retrieval and generation, or a vector store Bedrock doesn't directly support are all easier to implement in a self-managed LangChain pipeline. For a large share of real internal-knowledge RAG use cases — genuinely most of them — that additional flexibility isn't actually needed, and the operational savings of not running your own vector database infrastructure are the more consequential factor in the decision.

Metadata filtering: narrowing retrieval beyond similarity alone

Pure vector similarity search occasionally returns a passage that's semantically close to a query but wrong for a different reason — an outdated policy document version, content scoped to the wrong region or product line. Metadata filtering narrows the candidate set before similarity ranking even applies:

response = client.retrieve(
    knowledgeBaseId="KB123ABC",
    retrievalQuery={"text": "How many vacation days do employees get?"},
    retrievalConfiguration={
        "vectorSearchConfiguration": {
            "filter": {
                "equals": {"key": "region", "value": "UK"}
            }
        }
    },
)

Attaching metadata (region, document version, department) to each chunk during ingestion, then filtering on it at query time, is the same "narrow before ranking" principle behind a SQL WHERE clause combined with full-text search — a query for a UK employee's vacation policy should never surface a US-specific document as its top result purely because the wording happens to be semantically similar.

Multiple data sources feeding one Knowledge Base

A single Knowledge Base isn't limited to one S3 bucket — multiple data sources (an HR policy bucket, a product documentation bucket, a support-ticket export) can feed the same Knowledge Base, each ingested and re-ingested independently:

aws bedrock-agent create-data-source \
  --knowledge-base-id "KB123ABC" \
  --name "product-docs" \
  --data-source-configuration '{"type": "S3", "s3Configuration": {"bucketArn": "arn:aws:s3:::novuspark-product-docs"}}'

Combined with metadata filtering, this supports a genuinely common real pattern: one Knowledge Base serving several distinct content domains, with retrieval scoped to the relevant domain per query — rather than needing an entirely separate Knowledge Base (and separate infrastructure) per content source.

Monitoring ingestion job health

Because ingestion runs asynchronously, it's worth checking a job's actual status rather than assuming it succeeded the moment start-ingestion-job returns:

response = client.get_ingestion_job(
    knowledgeBaseId="KB123ABC",
    dataSourceId="DS456DEF",
    ingestionJobId=job_id,
)
print(response["ingestionJob"]["status"], response["ingestionJob"]["statistics"])

A failed or partially-completed ingestion job leaves the Knowledge Base's index stale relative to what's actually in S3 — checking status explicitly, ideally as part of the same scheduled process that triggers ingestion, catches that gap before someone notices retrieval quietly returning outdated content.

What to actually remember from this post

  • A Knowledge Base is the same RAG architecture built earlier in this blog — embedding model, vector store, document source — with AWS operating the vector store infrastructure for you.
  • Ingestion jobs handle chunking and embedding automatically from an S3 source — no self-managed pipeline code to maintain, and semantic chunking is worth evaluating against fixed-size for documents with clearly distinct sections.
  • retrieve_and_generate collapses retrieval and generation into one call, with structured citations tracing every claim back to its source; retrieve alone gives you retrieved context to combine with custom logic yourself.
  • The real trade-off is flexibility versus operational burden — less fine-grained control than a self-managed pipeline, in exchange for not running vector database infrastructure yourself.

Next in the series: Amazon Bedrock Agents: Automating Multi-Step Tasks, where Bedrock provides a managed equivalent of the LangChain and LangGraph agent patterns covered earlier in this blog.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.