diff --git a/s3-vectors-lambda-bedrock-cdk/.gitignore b/s3-vectors-lambda-bedrock-cdk/.gitignore new file mode 100644 index 000000000..2303e9b95 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/.gitignore @@ -0,0 +1,6 @@ +node_modules +cdk.out +*.js +!src/**/*.js +*.d.ts +package-lock.json diff --git a/s3-vectors-lambda-bedrock-cdk/README.md b/s3-vectors-lambda-bedrock-cdk/README.md new file mode 100644 index 000000000..117473982 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/README.md @@ -0,0 +1,120 @@ +# S3 Vectors with Lambda and Amazon Bedrock RAG + +This pattern deploys a serverless RAG (Retrieval-Augmented Generation) pipeline using Amazon S3 Vectors for vector storage, Lambda for orchestration, and Amazon Bedrock for embeddings and text generation. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/s3-vectors-lambda-bedrock-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Node.js 18+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed +* [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Amazon Titan Text Embeddings V2 and Anthropic Claude Sonnet + +## Architecture + +``` + ┌──────────────────────────────────────────────────────┐ + │ S3 Vector Bucket │ + │ ┌──────────────────────────────────┐ │ + │ │ Vector Index (1024-dim, cosine) │ │ + │ └──────────────────────────────────┘ │ + └──────────────┬───────────────┬───────────────────────┘ + │ │ + ┌──────────────┴──┐ ┌───────┴──────────────┐ + │ Ingest Lambda │ │ Query Lambda │ + │ (embed + store) │ │ (search + generate) │ + └────────┬─────────┘ └────────┬──────────────┘ + │ │ + ┌────────┴──────────────────────┴──────────┐ + │ Amazon Bedrock │ + │ Titan Embeddings V2 │ Claude Sonnet │ + └──────────────────────────────────────────┘ +``` + +## How it works + +**Ingest flow:** +1. Invoke the Ingest Lambda with an array of text documents. +2. Lambda calls Bedrock Titan Embeddings V2 to generate 1024-dimensional vectors. +3. Vectors are stored in an S3 vector index with metadata (source text, timestamp). + +**Query flow:** +1. Invoke the Query Lambda with a natural language question. +2. Lambda embeds the question using the same Titan model. +3. Lambda queries S3 Vectors for the top-K most similar documents. +4. Retrieved context is sent to Bedrock Claude to generate a grounded answer. + +## Deployment Instructions + +1. Clone the repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/s3-vectors-lambda-bedrock-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy the stack: + ```bash + cdk deploy + ``` + +4. Note the function names from the stack outputs. + +## Testing + +1. Ingest sample documents: + ```bash + aws lambda invoke \ + --function-name \ + --payload '{ + "documents": [ + {"key": "serverless", "text": "Serverless computing lets you run code without managing servers. AWS Lambda automatically scales your application."}, + {"key": "containers", "text": "Containers package applications with their dependencies. Amazon ECS and EKS manage container orchestration."}, + {"key": "s3-vectors", "text": "Amazon S3 Vectors provides purpose-built vector storage for AI applications with sub-second query latency."} + ] + }' \ + --cli-binary-format raw-in-base64-out \ + ingest-output.json + ``` + +2. Query with a question: + ```bash + aws lambda invoke \ + --function-name \ + --payload '{"question": "How do I store vectors for AI applications?"}' \ + --cli-binary-format raw-in-base64-out \ + query-output.json + + cat query-output.json | jq . + ``` + +## Cleanup + +1. Delete vectors and the vector index manually (S3 Vectors resources are not managed by CloudFormation): + ```bash + aws s3vectors delete-vector-index \ + --vector-bucket-name \ + --index-name knowledge-base + + aws s3vectors delete-vector-bucket \ + --vector-bucket-name + ``` + +2. Delete the CDK stack: + ```bash + cdk destroy + ``` + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/s3-vectors-lambda-bedrock-cdk/bin/app.ts b/s3-vectors-lambda-bedrock-cdk/bin/app.ts new file mode 100644 index 000000000..007fad563 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/bin/app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import "source-map-support/register"; +import * as cdk from "aws-cdk-lib"; +import { S3VectorsLambdaBedrockStack } from "../lib/s3-vectors-lambda-bedrock-stack"; + +const app = new cdk.App(); +new S3VectorsLambdaBedrockStack(app, "S3VectorsLambdaBedrockStack"); diff --git a/s3-vectors-lambda-bedrock-cdk/cdk.json b/s3-vectors-lambda-bedrock-cdk/cdk.json new file mode 100644 index 000000000..822400f59 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/cdk.json @@ -0,0 +1,11 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "node_modules", "src"] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true + } +} diff --git a/s3-vectors-lambda-bedrock-cdk/example-pattern.json b/s3-vectors-lambda-bedrock-cdk/example-pattern.json new file mode 100644 index 000000000..da46ad3a5 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/example-pattern.json @@ -0,0 +1,61 @@ +{ + "title": "S3 Vectors with Lambda and Amazon Bedrock RAG", + "description": "Build a serverless RAG pipeline using Amazon S3 Vectors for cost-optimized vector storage, Lambda for orchestration, and Amazon Bedrock for embeddings and generation.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys two Lambda functions backed by Amazon S3 Vectors and Amazon Bedrock to implement a serverless Retrieval-Augmented Generation (RAG) pipeline.", + "The Ingest function takes text documents, generates vector embeddings using Bedrock Titan Embeddings V2, and stores them in an S3 vector index with metadata. The Query function takes a natural language question, embeds it, performs a similarity search against S3 Vectors, and uses Bedrock Claude to generate an answer grounded in the retrieved context.", + "S3 Vectors provides purpose-built, cost-optimized vector storage with sub-second query latency \u2014 no vector database infrastructure to manage." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/s3-vectors-lambda-bedrock-cdk", + "templateURL": "serverless-patterns/s3-vectors-lambda-bedrock-cdk", + "projectFolder": "s3-vectors-lambda-bedrock-cdk", + "templateFile": "lib/s3-vectors-lambda-bedrock-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon S3 Vectors Documentation", + "link": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html" + }, + { + "text": "Getting started with S3 Vectors", + "link": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors-getting-started.html" + }, + { + "text": "Amazon Bedrock Titan Embeddings", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} \ No newline at end of file diff --git a/s3-vectors-lambda-bedrock-cdk/lib/s3-vectors-lambda-bedrock-stack.ts b/s3-vectors-lambda-bedrock-cdk/lib/s3-vectors-lambda-bedrock-stack.ts new file mode 100644 index 000000000..f5ef8b926 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/lib/s3-vectors-lambda-bedrock-stack.ts @@ -0,0 +1,140 @@ +import * as cdk from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as logs from "aws-cdk-lib/aws-logs"; +import * as cr from "aws-cdk-lib/custom-resources"; +import { Construct } from "constructs"; + +export class S3VectorsLambdaBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vectorBucketName = new cdk.CfnParameter(this, "VectorBucketName", { + type: "String", + default: "rag-knowledge-base-vectors", + description: "Name for the S3 vector bucket", + }); + + const indexName = "knowledge-base"; + + // S3 Vectors policy (shared by both functions and custom resource) + // Note: s3vectors does not support resource-level ARNs yet — wildcard required + const s3VectorsPolicy = new iam.PolicyStatement({ + actions: [ + "s3vectors:CreateVectorBucket", + "s3vectors:DeleteVectorBucket", + "s3vectors:CreateVectorIndex", + "s3vectors:DeleteVectorIndex", + "s3vectors:PutVectors", + "s3vectors:QueryVectors", + "s3vectors:GetVectors", + "s3vectors:DeleteVectors", + ], + resources: ["*"], + }); + + const bedrockPolicy = new iam.PolicyStatement({ + actions: ["bedrock:InvokeModel"], + resources: [ + `arn:aws:bedrock:${this.region}::foundation-model/amazon.titan-embed-text-v2:0`, + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/us.anthropic.claude-sonnet-4-20250514-v1:0`, + "arn:aws:bedrock:*::foundation-model/*", + ], + }); + + const sharedEnv = { + VECTOR_BUCKET_NAME: vectorBucketName.valueAsString, + INDEX_NAME: indexName, + EMBED_MODEL_ID: "amazon.titan-embed-text-v2:0", + GENERATION_MODEL_ID: "us.anthropic.claude-sonnet-4-20250514-v1:0", + }; + + // Ingest function — embeds text and stores in S3 Vectors + const ingestFn = new lambda.Function(this, "IngestFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "ingest.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(5), + memorySize: 256, + environment: sharedEnv, + logRetention: logs.RetentionDays.ONE_WEEK, + }); + ingestFn.addToRolePolicy(s3VectorsPolicy); + ingestFn.addToRolePolicy(bedrockPolicy); + + // Query function — searches S3 Vectors and generates answer with Bedrock + const queryFn = new lambda.Function(this, "QueryFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "query.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(2), + memorySize: 256, + environment: sharedEnv, + logRetention: logs.RetentionDays.ONE_WEEK, + }); + queryFn.addToRolePolicy(s3VectorsPolicy); + queryFn.addToRolePolicy(bedrockPolicy); + + // Custom resource to create vector bucket and index on deploy + const setupRole = new iam.Role(this, "SetupRole", { + assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWSLambdaBasicExecutionRole" + ), + ], + }); + setupRole.addToPolicy(s3VectorsPolicy); + + const createBucket = new cr.AwsCustomResource(this, "CreateVectorBucket", { + onCreate: { + service: "S3Vectors", + action: "createVectorBucket", + parameters: { vectorBucketName: vectorBucketName.valueAsString }, + physicalResourceId: cr.PhysicalResourceId.of("vector-bucket"), + }, + onDelete: { + service: "S3Vectors", + action: "deleteVectorBucket", + parameters: { vectorBucketName: vectorBucketName.valueAsString }, + }, + role: setupRole, + policy: cr.AwsCustomResourcePolicy.fromStatements([s3VectorsPolicy]), + }); + + const createIndex = new cr.AwsCustomResource(this, "CreateVectorIndex", { + onCreate: { + service: "S3Vectors", + action: "createVectorIndex", + parameters: { + vectorBucketName: vectorBucketName.valueAsString, + indexName, + dimension: 1024, + distanceMetric: "cosine", + }, + physicalResourceId: cr.PhysicalResourceId.of("vector-index"), + }, + onDelete: { + service: "S3Vectors", + action: "deleteVectorIndex", + parameters: { + vectorBucketName: vectorBucketName.valueAsString, + indexName, + }, + }, + role: setupRole, + policy: cr.AwsCustomResourcePolicy.fromStatements([s3VectorsPolicy]), + }); + createIndex.node.addDependency(createBucket); + + new cdk.CfnOutput(this, "IngestFunctionName", { + value: ingestFn.functionName, + }); + new cdk.CfnOutput(this, "QueryFunctionName", { + value: queryFn.functionName, + }); + new cdk.CfnOutput(this, "VectorBucketNameOutput", { + value: vectorBucketName.valueAsString, + }); + } +} diff --git a/s3-vectors-lambda-bedrock-cdk/package.json b/s3-vectors-lambda-bedrock-cdk/package.json new file mode 100644 index 000000000..590a9bdf5 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/package.json @@ -0,0 +1,20 @@ +{ + "name": "s3-vectors-lambda-bedrock-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.ts" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "dependencies": { + "aws-cdk-lib": "2.180.0", + "constructs": "10.4.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "ts-node": "^10.9.0", + "typescript": "~5.7.0" + } +} diff --git a/s3-vectors-lambda-bedrock-cdk/src/ingest.js b/s3-vectors-lambda-bedrock-cdk/src/ingest.js new file mode 100644 index 000000000..ceaf49a93 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/src/ingest.js @@ -0,0 +1,55 @@ +const { S3VectorsClient, PutVectorsCommand } = require("@aws-sdk/client-s3vectors"); +const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime"); + +const s3v = new S3VectorsClient(); +const bedrock = new BedrockRuntimeClient(); + +const VECTOR_BUCKET = process.env.VECTOR_BUCKET_NAME; +const INDEX_NAME = process.env.INDEX_NAME; +const EMBED_MODEL = process.env.EMBED_MODEL_ID; + +async function embed(text) { + const res = await bedrock.send( + new InvokeModelCommand({ + modelId: EMBED_MODEL, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ inputText: text }), + }) + ); + return JSON.parse(new TextDecoder().decode(res.body)).embedding; +} + +exports.handler = async (event) => { + const documents = event.documents || []; + if (!documents.length) return { statusCode: 400, body: "No documents provided" }; + + // Generate embeddings for all documents + const vectors = []; + for (const doc of documents) { + const embedding = await embed(doc.text); + vectors.push({ + key: doc.key, + data: { float32: embedding }, + metadata: { + source_text: doc.text, + ingested_at: new Date().toISOString(), + ...(doc.metadata || {}), + }, + }); + } + + // Batch put into S3 Vectors + await s3v.send( + new PutVectorsCommand({ + vectorBucketName: VECTOR_BUCKET, + indexName: INDEX_NAME, + vectors, + }) + ); + + return { + statusCode: 200, + body: JSON.stringify({ ingested: vectors.length }), + }; +}; diff --git a/s3-vectors-lambda-bedrock-cdk/src/query.js b/s3-vectors-lambda-bedrock-cdk/src/query.js new file mode 100644 index 000000000..40e2501e1 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/src/query.js @@ -0,0 +1,80 @@ +const { S3VectorsClient, QueryVectorsCommand } = require("@aws-sdk/client-s3vectors"); +const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime"); + +const s3v = new S3VectorsClient(); +const bedrock = new BedrockRuntimeClient(); + +const VECTOR_BUCKET = process.env.VECTOR_BUCKET_NAME; +const INDEX_NAME = process.env.INDEX_NAME; +const EMBED_MODEL = process.env.EMBED_MODEL_ID; +const GEN_MODEL = process.env.GENERATION_MODEL_ID; + +async function embed(text) { + const res = await bedrock.send( + new InvokeModelCommand({ + modelId: EMBED_MODEL, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ inputText: text }), + }) + ); + return JSON.parse(new TextDecoder().decode(res.body)).embedding; +} + +exports.handler = async (event) => { + const question = event.question; + if (!question) return { statusCode: 400, body: "No question provided" }; + + // Embed the question + const queryVector = await embed(question); + + // Search S3 Vectors for similar documents + const searchResult = await s3v.send( + new QueryVectorsCommand({ + vectorBucketName: VECTOR_BUCKET, + indexName: INDEX_NAME, + queryVector: { float32: queryVector }, + topK: 3, + returnMetadata: true, + returnDistance: true, + }) + ); + + const context = (searchResult.vectors || []) + .map((v) => v.metadata?.source_text || "") + .filter(Boolean) + .join("\n\n"); + + // Generate answer using retrieved context + const genRes = await bedrock.send( + new InvokeModelCommand({ + modelId: GEN_MODEL, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ + anthropic_version: "bedrock-2023-05-31", + max_tokens: 1024, + messages: [ + { + role: "user", + content: `Answer the question based on the context below. If the context doesn't contain the answer, say so.\n\nContext:\n${context}\n\nQuestion: ${question}`, + }, + ], + }), + }) + ); + + const answer = JSON.parse(new TextDecoder().decode(genRes.body)).content[0].text; + + return { + statusCode: 200, + body: JSON.stringify({ + question, + answer, + sources: (searchResult.vectors || []).map((v) => ({ + key: v.key, + distance: v.distance, + })), + }), + }; +}; diff --git a/s3-vectors-lambda-bedrock-cdk/tsconfig.json b/s3-vectors-lambda-bedrock-cdk/tsconfig.json new file mode 100644 index 000000000..15e54de36 --- /dev/null +++ b/s3-vectors-lambda-bedrock-cdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "outDir": "build", + "rootDir": ".", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "exclude": ["node_modules", "build", "src"] +}