From b0a2197b5eb8b09e99e65d15265eb61f9b39509c Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Tue, 4 Aug 2026 21:53:04 +0530 Subject: [PATCH 1/9] Add pattern: bedrock-semantic-cache-s3vectors-sam Serverless semantic cache for Amazon Bedrock using AWS Lambda and Amazon S3 Vectors. Returns a cached answer when an incoming prompt is semantically similar to a prior one (cosine similarity threshold), with freshness TTL, one-call epoch force-invalidation via SSM Parameter Store, and a negation-parity guard. Cache persists in S3 Vectors (scales to zero); Lambda is stateless. SAM template, Python. --- .../README.md | 151 ++++++++++++++++++ .../example-pattern.json | 69 ++++++++ .../src/cache.py | 118 ++++++++++++++ .../src/requirements.txt | 1 + .../template.yaml | 101 ++++++++++++ 5 files changed, 440 insertions(+) create mode 100644 bedrock-semantic-cache-s3vectors-sam/README.md create mode 100644 bedrock-semantic-cache-s3vectors-sam/example-pattern.json create mode 100644 bedrock-semantic-cache-s3vectors-sam/src/cache.py create mode 100644 bedrock-semantic-cache-s3vectors-sam/src/requirements.txt create mode 100644 bedrock-semantic-cache-s3vectors-sam/template.yaml diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md new file mode 100644 index 000000000..93e5194c5 --- /dev/null +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -0,0 +1,151 @@ +# Serverless semantic cache for Amazon Bedrock (with Amazon S3 Vectors) + +Return cached answers for **semantically similar** prompts — different wording still hits — so you skip the LLM call on repeats and near-repeats. Cuts Amazon Bedrock cost and latency, scales to zero, and drops in front of any model. + +Learn more at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-semantic-cache-s3vectors-sam + +> Important: this application uses AWS services (AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, AWS Systems Manager) and there are costs associated with these services after the Free Tier usage. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +--- + +## TL;DR — read this first + +- **What it is:** a Lambda in front of Amazon Bedrock that caches answers by *meaning* (not exact text). Same question asked three different ways → one Bedrock call, two instant cache hits. +- **Best for:** FAQ / support bots, docs Q&A, high-traffic assistants — anywhere many users ask the same things in different words. +- **Not for:** answers that must be exact, fresh, or per-user (unless you add namespacing / invalidation / a verify step). +- **Cost:** a cache hit skips the expensive LLM call and pays only a tiny embedding + vector query. Break-even is a few percent hit rate, so any repetitive workload is a net win. +- **Distinct from Bedrock native features:** native Prompt Caching is *exact-prefix* only (one character breaks it); Intelligent Prompt Routing picks a cheaper model. This caches by *semantic similarity* and skips the model entirely. They complement each other. + +## Where it shines ✅ + +- **Repetitive, paraphrase-heavy traffic.** Research shows ~31% of LLM queries are semantically similar to a prior one — those become instant, free hits. +- **Latency-sensitive UX.** Measured ~7x faster on a hit (≈230 ms vs ≈1,700 ms). +- **Cost-sensitive, high-volume assistants.** Every hit is one fewer Bedrock invocation and does **not** count against your Bedrock TPM/RPM limits (throttle relief under load). +- **Any model / provider.** The cache is model-agnostic; the on-miss call is a drop-in for Bedrock or an external model. + +## Where it will NOT shine ❌ (be honest) + +- **Unique, one-off prompts.** No repetition → ~0 hits → you pay a tiny per-request overhead for nothing. Skip it here. +- **Answers that must be exact or fresh.** A similar-but-not-identical prompt can return a subtly different prior answer. Mitigate with a higher threshold + TTL, or bypass the cache for such routes. +- **Per-user / personalized answers.** Namespace the cache per user, or don't cache these. +- **Semantic antonyms.** The built-in negation guard catches "not / n't", but not opposites like "cheapest" vs "most expensive". For high-stakes (financial, legal, medical), add an optional LLM equivalence-verify on borderline hits. + +## How it saves money regardless (the math) + +Every request pays a tiny "cache tax": one embedding call (~$0.00002) + one vector query (fractions of a cent). You **save** on every HIT because you skip the LLM call (cents to dollars, especially with large prompts / RAG context / bigger models). + +``` +net savings = (hits x LLM cost skipped) - (all requests x tiny cache tax) +``` + +Because the tax is orders of magnitude smaller than an LLM call, **break-even is roughly a 1-5% hit rate.** Real repetitive workloads sit far above that, and savings **compound** as the cache warms. The only losing case is genuinely zero repetition. Plus: everything is pay-per-use and **scales to zero** (Lambda + S3 Vectors), so there is no idle cost. + +--- + +## How it works + +``` +prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) + | + |--search--> Amazon S3 Vectors (cosine top-K; answer is stored in vector metadata) + | |-- HIT (sim >= threshold, fresh, current epoch, negation-parity) --> return cached answer (~230 ms, $0 LLM) + | |-- MISS --> + |--generate--> Amazon Bedrock LLM (writes the answer) + |--store-----> Amazon S3 Vectors (embedding + answer + model + created_at + epoch) + |--return + (force-invalidate epoch is stored in AWS Systems Manager Parameter Store) +``` + +- **Amazon Bedrock** is used two ways: **embeddings** (turn text into a meaning vector so matching is semantic) and the **LLM** (answer on a miss). +- **Amazon S3 Vectors** is the cache store *and* the similarity search — pay-per-use, no always-on cost. This is the primitive that makes a serverless semantic cache economical. +- **AWS Lambda** is stateless glue. The cache lives entirely in S3 Vectors, so it survives cold starts, redeploys, and env recycling. +- **SSM Parameter Store** holds the epoch counter for force-invalidation. + +### Correctness features +- **Tunable similarity threshold** (default cosine 0.85) — per-deploy and per-request. +- **Freshness TTL** — entries older than `TTL_SECONDS` are treated as a miss. +- **Force-invalidate** — bump one epoch number → every prior entry instantly misses (no deletes/scans). For big changes that can't wait for TTL. +- **Negation-parity guard** — "is X" vs "is NOT X" embed ~identically but mean the opposite; the guard blocks that false hit. +- **top-K + iterate** — a stale duplicate near-neighbour never blocks a valid hit. + +## Requirements + +- An AWS account with permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2 (recent — must include the `s3vectors` and `lambda-microvms`-era models; `bedrock-runtime`). +- [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). +- Amazon Bedrock **model access enabled** for the embeddings model (`amazon.titan-embed-text-v2:0`) and the text model (`amazon.nova-lite-v1:0`) in your Region. +- A Region where Amazon S3 Vectors and Amazon Bedrock are available (e.g. `us-east-1`). + +## Deployment + +S3 Vectors is not yet a CloudFormation resource, so create the vector store first (two commands), then deploy the rest with SAM. + +```bash +# 1. Create the S3 Vectors bucket and a cosine index (1024 dims = Titan v2) +export VECTOR_BUCKET="semantic-cache-$(aws sts get-caller-identity --query Account --output text)" +aws s3vectors create-vector-bucket --vector-bucket-name "$VECTOR_BUCKET" +aws s3vectors create-index \ + --vector-bucket-name "$VECTOR_BUCKET" \ + --index-name prompt-cache --data-type float32 --dimension 1024 --distance-metric cosine \ + --metadata-configuration 'nonFilterableMetadataKeys=prompt,response,model,created_at,epoch' + +# 2. Build and deploy the Lambda + IAM + SSM epoch parameter +sam build +sam deploy --guided +# - VectorBucket: value of $VECTOR_BUCKET above +# - VectorIndex : prompt-cache +# - ApiKey : (optional) a secret for the x-api-key header, or leave blank for IAM-only +``` + +Note the `FunctionUrl` and `FunctionName` outputs. + +## Testing + +The function URL uses AWS_IAM auth (SigV4). The simplest test is a direct invoke: + +```bash +KEY="" +payload() { python3 -c "import json,sys;print(json.dumps({'headers':{'x-api-key':'$KEY'},'body':json.dumps({'prompt':sys.argv[1]})}))" "$1" > ev.json; } + +# MISS (calls Bedrock) +payload "What is the capital of France?"; aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json +sleep 6 +# HIT — exact repeat (cached=true, similarity ~1.0, ~230ms) +payload "What is the capital of France?"; aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json +# HIT — semantic (different words) +payload "Which city is the capital of France?"; aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json +``` + +Force-invalidate (e.g., after a data/policy change): + +```bash +python3 -c "import json;print(json.dumps({'headers':{'x-api-key':'$KEY'},'body':json.dumps({'action':'invalidate'})}))" > ev.json +aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json +# -> {"invalidated": true, "epoch": N}. Every prior answer now misses (propagates within ~30s). +``` + +Expected: exact/semantic repeats HIT (`cached=true` with a similarity score); unrelated prompts MISS; after `invalidate`, the same prompt MISSes once, then HITs again once re-cached. + +## Tuning + +| Setting | Env var / request field | Effect | +|---|---|---| +| Similarity threshold | `SIM_THRESHOLD` (deploy) or `threshold` (per request) | Higher = stricter matching, fewer but safer hits | +| Freshness | `TTL_SECONDS` | Max age of a served answer | +| Force-invalidate | `POST {"action":"invalidate"}` | Invalidate the whole cache instantly | +| Models | `EMBED_MODEL`, `LLM_MODEL` | Swap embeddings / answer model | + +## Cleanup + +```bash +sam delete +aws s3vectors delete-index --vector-bucket-name "$VECTOR_BUCKET" --index-name prompt-cache +aws s3vectors delete-vector-bucket --vector-bucket-name "$VECTOR_BUCKET" +aws ssm delete-parameter --name /semantic-cache/epoch +``` + +--- + +Author: Manish S + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 diff --git a/bedrock-semantic-cache-s3vectors-sam/example-pattern.json b/bedrock-semantic-cache-s3vectors-sam/example-pattern.json new file mode 100644 index 000000000..7e95c6dab --- /dev/null +++ b/bedrock-semantic-cache-s3vectors-sam/example-pattern.json @@ -0,0 +1,69 @@ +{ + "title": "Serverless semantic cache for Amazon Bedrock with Amazon S3 Vectors", + "description": "Cut Amazon Bedrock cost and latency by returning cached answers for semantically-similar prompts, using AWS Lambda and Amazon S3 Vectors.", + "language": "Python", + "level": "300", + "framework": "AWS SAM", + "patternArch": { + "icon1": { "x": 20, "y": 50, "service": "lambda", "label": "AWS Lambda (semantic cache)" }, + "icon2": { "x": 55, "y": 30, "service": "bedrock", "label": "Amazon Bedrock (embeddings + LLM)" }, + "icon3": { "x": 55, "y": 70, "service": "s3", "label": "Amazon S3 Vectors (cache store)" }, + "line1": { "from": "icon1", "to": "icon2" }, + "line2": { "from": "icon1", "to": "icon3" } + }, + "introBox": { + "headline": "How it works", + "text": [ + "Large language model (LLM) calls are slow and expensive, yet a large share of production prompts are paraphrases of ones already answered. This pattern places an AWS Lambda function in front of Amazon Bedrock that returns a cached answer whenever an incoming prompt is semantically similar to a previous one - so you skip the LLM call entirely on repeats and near-repeats.", + "When a request arrives, the Lambda function embeds the prompt with an Amazon Bedrock embeddings model (Amazon Titan Text Embeddings v2, 1024 dimensions). It then queries an Amazon S3 Vectors index for the nearest stored prompt using cosine similarity. Amazon S3 Vectors returns the closest match together with its distance and metadata in a single call, and the cached answer is stored directly in that metadata - so no separate database is required.", + "On a cache HIT (cosine similarity at or above a configurable threshold, the entry still within its freshness TTL, the current cache epoch, and passing a negation-parity guard) the function returns the stored answer in milliseconds at zero LLM cost. On a MISS, the function calls the Bedrock text model to generate the answer, stores the prompt embedding plus the answer, model, timestamp and epoch back into Amazon S3 Vectors, and returns the fresh result.", + "Correctness is designed, not assumed. A tunable similarity threshold trades precision for hit rate. A freshness TTL bounds staleness. A one-call force-invalidation bumps a global epoch stored in AWS Systems Manager Parameter Store, so every previously cached answer instantly becomes a miss without deleting or scanning anything - ideal for a policy or data change that cannot wait for the TTL. A negation-parity guard prevents the classic semantic-cache trap where a prompt and its negation ('is X' versus 'is NOT X') embed almost identically but mean the opposite. The query uses top-K retrieval and iterates candidates, so a stale duplicate near-neighbour never blocks a valid hit.", + "Amazon S3 Vectors is what makes this economical and fully serverless: pay-per-use vector storage and search that scales to zero, instead of an always-on vector database. The AWS Lambda function is stateless - the cache persists entirely in Amazon S3 Vectors, so it survives cold starts, redeploys, and execution-environment recycling. Access is over an IAM-signed Lambda function URL, with an optional application-level API key. IAM permissions follow least privilege: bedrock:InvokeModel is scoped to foundation models; s3vectors actions (QueryVectors, GetVectors, ListVectors, PutVectors, GetIndex) are scoped to the specific vector bucket and index; and ssm:GetParameter/PutParameter is scoped to the single epoch parameter.", + "The embeddings model and the answer model are both configurable, and the on-miss call is a drop-in for any model provider - the caching layer itself is model-agnostic. Best fit: FAQ and support assistants, documentation Q&A, and high-traffic assistants where users ask the same things in different words. Not intended for answers that must be exact, fresh, or per-user unless combined with namespacing, invalidation, or an equivalence-verification step." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/bedrock-semantic-cache-s3vectors-sam", + "templateURL": "serverless-patterns/bedrock-semantic-cache-s3vectors-sam", + "projectFolder": "bedrock-semantic-cache-s3vectors-sam", + "templateFile": "template.yaml" + } + }, + "resources": { + "headline": "Additional resources", + "bullets": [ + { "text": "Amazon S3 Vectors - vector storage in Amazon S3", "link": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html" }, + { "text": "Amazon Bedrock - Titan Text Embeddings", "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html" }, + { "text": "Amazon Bedrock prompt caching (native, prefix-based) - complements this pattern", "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html" }, + { "text": "AWS Lambda function URLs", "link": "https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" } + ] + }, + "deploy": { + "text": [ + "See the README for the two S3 Vectors setup commands, then: sam build && sam deploy --guided" + ] + }, + "testing": { + "headline": "Testing", + "text": [ + "See the GitHub repo for detailed testing instructions (miss, exact hit, semantic hit, force-invalidate)." + ] + }, + "cleanup": { + "headline": "Cleanup", + "text": [ + "1. Delete the stack: sam delete.", + "2. Delete the S3 Vectors index and bucket: aws s3vectors delete-index ... then aws s3vectors delete-vector-bucket ...." + ] + }, + "authors": [ + { + "name": "Manish S", + "image": "", + "bio": "", + "linkedin": "", + "twitter": "" + } + ] +} diff --git a/bedrock-semantic-cache-s3vectors-sam/src/cache.py b/bedrock-semantic-cache-s3vectors-sam/src/cache.py new file mode 100644 index 000000000..aafc14c5d --- /dev/null +++ b/bedrock-semantic-cache-s3vectors-sam/src/cache.py @@ -0,0 +1,118 @@ +"""Serverless Semantic Cache for Amazon Bedrock. +Embed prompt -> query S3 Vectors for a semantically-similar prior prompt -> +HIT (similarity >= threshold, fresh by TTL, and current epoch) return cached response; +MISS -> call Bedrock, store {embedding + metadata}, return. +Force-invalidate: POST {"action":"invalidate"} bumps a global epoch (SSM) -> every +prior entry instantly becomes a miss. No deletes, no scanning, O(1).""" +import json, os, re, time, uuid +import boto3 + +_NEG = {"not","no","never","without","cannot","nor","neither","none","cant","dont","doesnt","isnt","arent","wont","shouldnt","wasnt","werent","hasnt","havent","didnt","aint"} +def _has_negation(text): + for t in re.findall(r"[a-z']+", (text or "").lower()): + if t.replace("'","") in _NEG or t.endswith("n't"): + return True + return False + +REGION = os.environ.get("AWS_REGION", "us-east-1") +BUCKET = os.environ["VECTOR_BUCKET"] +INDEX = os.environ["VECTOR_INDEX"] +EMBED_MODEL = os.environ.get("EMBED_MODEL", "amazon.titan-embed-text-v2:0") +DEFAULT_MODEL = os.environ.get("LLM_MODEL", "amazon.nova-lite-v1:0") +SIM_THRESHOLD = float(os.environ.get("SIM_THRESHOLD", "0.85")) +TTL_SECONDS = int(os.environ.get("TTL_SECONDS", "86400")) +API_KEY = os.environ.get("API_KEY", "") +EPOCH_PARAM = os.environ.get("EPOCH_PARAM", "/semantic-cache/epoch") + +br = boto3.client("bedrock-runtime", region_name=REGION) +s3v = boto3.client("s3vectors", region_name=REGION) +ssm = boto3.client("ssm", region_name=REGION) + +_epoch = {"val": None, "ts": 0.0} + + +def current_epoch(): + now = time.time() + if _epoch["val"] is None or now - _epoch["ts"] > 30: # refresh at most every 30s + try: + _epoch["val"] = ssm.get_parameter(Name=EPOCH_PARAM)["Parameter"]["Value"] + except Exception: + _epoch["val"] = "1" + _epoch["ts"] = now + return _epoch["val"] + + +def bump_epoch(): + try: + new = str(int(current_epoch()) + 1) + except Exception: + new = str(int(time.time())) + ssm.put_parameter(Name=EPOCH_PARAM, Value=new, Type="String", Overwrite=True) + _epoch["val"], _epoch["ts"] = new, time.time() + return new + + +def _resp(code, obj): + return {"statusCode": code, "headers": {"Content-Type": "application/json"}, "body": json.dumps(obj)} + + +def embed(text): + r = br.invoke_model(modelId=EMBED_MODEL, body=json.dumps({"inputText": text})) + return json.loads(r["body"].read())["embedding"] + + +def llm(prompt, model): + r = br.converse(modelId=model, messages=[{"role": "user", "content": [{"text": prompt}]}]) + return r["output"]["message"]["content"][0]["text"] + + +def handler(event, context): + t0 = time.time() + headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} + if API_KEY and headers.get("x-api-key") != API_KEY: + return _resp(401, {"error": "unauthorized"}) + body = {} + if event.get("body"): + try: + body = json.loads(event["body"]) + except Exception: + body = {} + + # --- force-invalidate: bump epoch, everything before is now a miss --- + if body.get("action") == "invalidate": + new = bump_epoch() + return _resp(200, {"invalidated": True, "epoch": new, + "note": "all entries cached before this epoch now miss"}) + + prompt = (body.get("prompt") or "").strip() + model = body.get("model", DEFAULT_MODEL) + threshold = float(body.get("threshold", SIM_THRESHOLD)) + if not prompt: + return _resp(400, {"error": "missing 'prompt'"}) + + ep = current_epoch() + vec = embed(prompt) + + q = s3v.query_vectors(vectorBucketName=BUCKET, indexName=INDEX, topK=5, + queryVector={"float32": vec}, returnDistance=True, returnMetadata=True) + for m in q.get("vectors", []): + sim = 1.0 - float(m.get("distance", 2.0)) + md = m.get("metadata", {}) or {} + fresh = (time.time() - int(md.get("created_at", "0") or 0)) < TTL_SECONDS + current = md.get("epoch") == ep + # negation-parity guard: 'X' vs 'NOT X' embed ~identically but mean the opposite + neg_ok = _has_negation(prompt) == _has_negation(md.get("prompt", "")) + if sim >= threshold and fresh and current and neg_ok and md.get("response"): + return _resp(200, {"cached": True, "similarity": round(sim, 4), "epoch": ep, + "matched_prompt": md.get("prompt"), "response": md["response"], + "model": md.get("model"), "latency_ms": int((time.time() - t0) * 1000)}) + + answer = llm(prompt, model) + s3v.put_vectors(vectorBucketName=BUCKET, indexName=INDEX, vectors=[{ + "key": uuid.uuid4().hex, + "data": {"float32": vec}, + "metadata": {"prompt": prompt, "response": answer, "model": model, + "created_at": str(int(time.time())), "epoch": ep}, + }]) + return _resp(200, {"cached": False, "similarity": None, "epoch": ep, "response": answer, + "model": model, "latency_ms": int((time.time() - t0) * 1000)}) diff --git a/bedrock-semantic-cache-s3vectors-sam/src/requirements.txt b/bedrock-semantic-cache-s3vectors-sam/src/requirements.txt new file mode 100644 index 000000000..7ef81e07c --- /dev/null +++ b/bedrock-semantic-cache-s3vectors-sam/src/requirements.txt @@ -0,0 +1 @@ +boto3>=1.43.35 diff --git a/bedrock-semantic-cache-s3vectors-sam/template.yaml b/bedrock-semantic-cache-s3vectors-sam/template.yaml new file mode 100644 index 000000000..8d592838b --- /dev/null +++ b/bedrock-semantic-cache-s3vectors-sam/template.yaml @@ -0,0 +1,101 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: > + Serverless semantic cache for Amazon Bedrock. A Lambda function embeds each prompt, + finds semantically-similar prior prompts in Amazon S3 Vectors, and returns the cached + answer on a hit (skipping the LLM) or calls Amazon Bedrock and caches the result on a miss. + (bedrock-semantic-cache-s3vectors-sam) + +Parameters: + VectorBucket: + Type: String + Description: Name of the S3 Vectors vector bucket that stores the cache (create before deploy - see README). + VectorIndex: + Type: String + Default: prompt-cache + Description: Name of the S3 Vectors index (dimension 1024, distance metric cosine). + SimThreshold: + Type: String + Default: "0.85" + Description: Cosine-similarity threshold for a cache hit (0-1). Higher = stricter matching. + TtlSeconds: + Type: String + Default: "86400" + Description: Freshness window in seconds. Entries older than this are treated as a miss. + ApiKey: + Type: String + Default: "" + NoEcho: true + Description: Optional app-level API key checked in the x-api-key header. Leave blank to rely only on IAM auth. + EmbedModel: + Type: String + Default: amazon.titan-embed-text-v2:0 + Description: Bedrock embeddings model used for semantic matching (must be consistent; matches the index dimension). + LlmModel: + Type: String + Default: amazon.nova-lite-v1:0 + Description: Bedrock model used to generate the answer on a cache miss. + +Resources: + # Global cache epoch for one-call force-invalidation (bump this to invalidate everything). + CacheEpoch: + Type: AWS::SSM::Parameter + Properties: + Name: /semantic-cache/epoch + Type: String + Value: "1" + + SemanticCacheFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: semantic-cache + Runtime: python3.13 + Handler: cache.handler + CodeUri: src/ + Timeout: 60 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + VECTOR_BUCKET: !Ref VectorBucket + VECTOR_INDEX: !Ref VectorIndex + EMBED_MODEL: !Ref EmbedModel + LLM_MODEL: !Ref LlmModel + SIM_THRESHOLD: !Ref SimThreshold + TTL_SECONDS: !Ref TtlSeconds + API_KEY: !Ref ApiKey + EPOCH_PARAM: /semantic-cache/epoch + FunctionUrlConfig: + AuthType: AWS_IAM + Policies: + - Statement: + - Sid: BedrockInvoke + Effect: Allow + Action: bedrock:InvokeModel + Resource: "arn:aws:bedrock:*::foundation-model/*" + - Sid: S3Vectors + Effect: Allow + Action: + - s3vectors:QueryVectors + - s3vectors:GetVectors + - s3vectors:ListVectors + - s3vectors:PutVectors + - s3vectors:GetIndex + Resource: + - !Sub "arn:aws:s3vectors:${AWS::Region}:${AWS::AccountId}:bucket/${VectorBucket}" + - !Sub "arn:aws:s3vectors:${AWS::Region}:${AWS::AccountId}:bucket/${VectorBucket}/index/*" + - Sid: EpochParam + Effect: Allow + Action: + - ssm:GetParameter + - ssm:PutParameter + Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/semantic-cache/epoch" + +Outputs: + FunctionUrl: + Description: IAM-signed HTTPS endpoint of the semantic cache. + Value: !GetAtt SemanticCacheFunctionUrl.FunctionUrl + FunctionName: + Description: Lambda function name (for aws lambda invoke testing). + Value: !Ref SemanticCacheFunction From 43c3ec3d62befa5186911880eaaf61e6a346134d Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Tue, 4 Aug 2026 22:09:15 +0530 Subject: [PATCH 2/9] Clean README formatting: remove emojis and non-ASCII symbols --- .../README.md | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md index 93e5194c5..4611d92ac 100644 --- a/bedrock-semantic-cache-s3vectors-sam/README.md +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -1,6 +1,6 @@ # Serverless semantic cache for Amazon Bedrock (with Amazon S3 Vectors) -Return cached answers for **semantically similar** prompts — different wording still hits — so you skip the LLM call on repeats and near-repeats. Cuts Amazon Bedrock cost and latency, scales to zero, and drops in front of any model. +Return cached answers for **semantically similar** prompts - different wording still hits - so you skip the LLM call on repeats and near-repeats. Cuts Amazon Bedrock cost and latency, scales to zero, and drops in front of any model. Learn more at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-semantic-cache-s3vectors-sam @@ -8,24 +8,24 @@ Learn more at Serverless Land Patterns: https://serverlessland.com/patterns/bedr --- -## TL;DR — read this first +## TL;DR - read this first -- **What it is:** a Lambda in front of Amazon Bedrock that caches answers by *meaning* (not exact text). Same question asked three different ways → one Bedrock call, two instant cache hits. -- **Best for:** FAQ / support bots, docs Q&A, high-traffic assistants — anywhere many users ask the same things in different words. +- **What it is:** a Lambda in front of Amazon Bedrock that caches answers by *meaning* (not exact text). Same question asked three different ways -> one Bedrock call, two instant cache hits. +- **Best for:** FAQ / support bots, docs Q&A, high-traffic assistants - anywhere many users ask the same things in different words. - **Not for:** answers that must be exact, fresh, or per-user (unless you add namespacing / invalidation / a verify step). - **Cost:** a cache hit skips the expensive LLM call and pays only a tiny embedding + vector query. Break-even is a few percent hit rate, so any repetitive workload is a net win. - **Distinct from Bedrock native features:** native Prompt Caching is *exact-prefix* only (one character breaks it); Intelligent Prompt Routing picks a cheaper model. This caches by *semantic similarity* and skips the model entirely. They complement each other. -## Where it shines ✅ +## Where it shines -- **Repetitive, paraphrase-heavy traffic.** Research shows ~31% of LLM queries are semantically similar to a prior one — those become instant, free hits. -- **Latency-sensitive UX.** Measured ~7x faster on a hit (≈230 ms vs ≈1,700 ms). +- **Repetitive, paraphrase-heavy traffic.** Research shows ~31% of LLM queries are semantically similar to a prior one - those become instant, free hits. +- **Latency-sensitive UX.** Measured ~7x faster on a hit (~230 ms vs ~1,700 ms). - **Cost-sensitive, high-volume assistants.** Every hit is one fewer Bedrock invocation and does **not** count against your Bedrock TPM/RPM limits (throttle relief under load). - **Any model / provider.** The cache is model-agnostic; the on-miss call is a drop-in for Bedrock or an external model. -## Where it will NOT shine ❌ (be honest) +## Where it will not shine (be honest) -- **Unique, one-off prompts.** No repetition → ~0 hits → you pay a tiny per-request overhead for nothing. Skip it here. +- **Unique, one-off prompts.** No repetition -> ~0 hits -> you pay a tiny per-request overhead for nothing. Skip it here. - **Answers that must be exact or fresh.** A similar-but-not-identical prompt can return a subtly different prior answer. Mitigate with a higher threshold + TTL, or bypass the cache for such routes. - **Per-user / personalized answers.** Namespace the cache per user, or don't cache these. - **Semantic antonyms.** The built-in negation guard catches "not / n't", but not opposites like "cheapest" vs "most expensive". For high-stakes (financial, legal, medical), add an optional LLM equivalence-verify on borderline hits. @@ -57,21 +57,21 @@ prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) ``` - **Amazon Bedrock** is used two ways: **embeddings** (turn text into a meaning vector so matching is semantic) and the **LLM** (answer on a miss). -- **Amazon S3 Vectors** is the cache store *and* the similarity search — pay-per-use, no always-on cost. This is the primitive that makes a serverless semantic cache economical. +- **Amazon S3 Vectors** is the cache store *and* the similarity search - pay-per-use, no always-on cost. This is the primitive that makes a serverless semantic cache economical. - **AWS Lambda** is stateless glue. The cache lives entirely in S3 Vectors, so it survives cold starts, redeploys, and env recycling. - **SSM Parameter Store** holds the epoch counter for force-invalidation. ### Correctness features -- **Tunable similarity threshold** (default cosine 0.85) — per-deploy and per-request. -- **Freshness TTL** — entries older than `TTL_SECONDS` are treated as a miss. -- **Force-invalidate** — bump one epoch number → every prior entry instantly misses (no deletes/scans). For big changes that can't wait for TTL. -- **Negation-parity guard** — "is X" vs "is NOT X" embed ~identically but mean the opposite; the guard blocks that false hit. -- **top-K + iterate** — a stale duplicate near-neighbour never blocks a valid hit. +- **Tunable similarity threshold** (default cosine 0.85) - per-deploy and per-request. +- **Freshness TTL** - entries older than `TTL_SECONDS` are treated as a miss. +- **Force-invalidate** - bump one epoch number -> every prior entry instantly misses (no deletes/scans). For big changes that can't wait for TTL. +- **Negation-parity guard** - "is X" vs "is NOT X" embed ~identically but mean the opposite; the guard blocks that false hit. +- **top-K + iterate** - a stale duplicate near-neighbour never blocks a valid hit. ## Requirements - An AWS account with permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. -- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2 (recent — must include the `s3vectors` and `lambda-microvms`-era models; `bedrock-runtime`). +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2 (recent - must include the `s3vectors` and `lambda-microvms`-era models; `bedrock-runtime`). - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). - Amazon Bedrock **model access enabled** for the embeddings model (`amazon.titan-embed-text-v2:0`) and the text model (`amazon.nova-lite-v1:0`) in your Region. - A Region where Amazon S3 Vectors and Amazon Bedrock are available (e.g. `us-east-1`). @@ -110,9 +110,9 @@ payload() { python3 -c "import json,sys;print(json.dumps({'headers':{'x-api-key' # MISS (calls Bedrock) payload "What is the capital of France?"; aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json sleep 6 -# HIT — exact repeat (cached=true, similarity ~1.0, ~230ms) +# HIT - exact repeat (cached=true, similarity ~1.0, ~230ms) payload "What is the capital of France?"; aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json -# HIT — semantic (different words) +# HIT - semantic (different words) payload "Which city is the capital of France?"; aws lambda invoke --function-name semantic-cache --cli-binary-format raw-in-base64-out --payload file://ev.json out.json; cat out.json ``` From d9c153e19d53c7ac98b2b03f3223f3ba9f40738b Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Tue, 4 Aug 2026 22:12:04 +0530 Subject: [PATCH 3/9] README: tidy section heading --- bedrock-semantic-cache-s3vectors-sam/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md index 4611d92ac..dcb5c4c8f 100644 --- a/bedrock-semantic-cache-s3vectors-sam/README.md +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -23,7 +23,7 @@ Learn more at Serverless Land Patterns: https://serverlessland.com/patterns/bedr - **Cost-sensitive, high-volume assistants.** Every hit is one fewer Bedrock invocation and does **not** count against your Bedrock TPM/RPM limits (throttle relief under load). - **Any model / provider.** The cache is model-agnostic; the on-miss call is a drop-in for Bedrock or an external model. -## Where it will not shine (be honest) +## Where it will not shine - **Unique, one-off prompts.** No repetition -> ~0 hits -> you pay a tiny per-request overhead for nothing. Skip it here. - **Answers that must be exact or fresh.** A similar-but-not-identical prompt can return a subtly different prior answer. Mitigate with a higher threshold + TTL, or bypass the cache for such routes. From d9ce0c2d1dad00af6b7faf27b26b845827534155 Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Tue, 4 Aug 2026 22:16:04 +0530 Subject: [PATCH 4/9] README: fix requirements (remove unrelated CLI reference) --- bedrock-semantic-cache-s3vectors-sam/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md index dcb5c4c8f..3db30eb07 100644 --- a/bedrock-semantic-cache-s3vectors-sam/README.md +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -71,7 +71,7 @@ prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) ## Requirements - An AWS account with permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. -- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2 (recent - must include the `s3vectors` and `lambda-microvms`-era models; `bedrock-runtime`). +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2, recent enough to include the `s3vectors` commands. - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). - Amazon Bedrock **model access enabled** for the embeddings model (`amazon.titan-embed-text-v2:0`) and the text model (`amazon.nova-lite-v1:0`) in your Region. - A Region where Amazon S3 Vectors and Amazon Bedrock are available (e.g. `us-east-1`). From e32a392f757bbe743443c99ed5a37406fd15cd41 Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Thu, 10 Sep 2026 23:32:21 +0530 Subject: [PATCH 5/9] Add author bio and LinkedIn to example-pattern.json --- bedrock-semantic-cache-s3vectors-sam/example-pattern.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/example-pattern.json b/bedrock-semantic-cache-s3vectors-sam/example-pattern.json index 7e95c6dab..8a78c6244 100644 --- a/bedrock-semantic-cache-s3vectors-sam/example-pattern.json +++ b/bedrock-semantic-cache-s3vectors-sam/example-pattern.json @@ -61,8 +61,8 @@ { "name": "Manish S", "image": "", - "bio": "", - "linkedin": "", + "bio": "AWS Support Engineer, trying to build things", + "linkedin": "https://www.linkedin.com/in/manish-s-84199221b", "twitter": "" } ] From 979ff28e6f073e1dfb03441498ed9397c6d6a14e Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Wed, 23 Sep 2026 12:00:06 +0530 Subject: [PATCH 6/9] Address review feedback on bedrock-semantic-cache-s3vectors-sam - Read the optional app-level API key from an AWS Systems Manager Parameter Store SecureString at runtime instead of a plaintext Lambda environment variable. Only the parameter name is configured on the function. - Drop unused s3vectors:ListVectors and s3vectors:GetIndex from the IAM policy. GetVectors is retained because query_vectors uses returnMetadata=True. - Create the S3 Vectors vector bucket and index with AWS::S3Vectors::VectorBucket and AWS::S3Vectors::Index instead of manual CLI steps, and correct the README statement that S3 Vectors has no CloudFormation support. - Add a git clone and cd step to the deployment instructions. - List Python 3 in Requirements, needed only for the test payload helpers. - Use AWS Systems Manager Parameter Store consistently instead of SSM. --- .../README.md | 50 +++++++---- .../src/cache.py | 25 +++++- .../template.yaml | 85 ++++++++++++++++--- 3 files changed, 127 insertions(+), 33 deletions(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md index 3db30eb07..3eb4d6568 100644 --- a/bedrock-semantic-cache-s3vectors-sam/README.md +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -59,7 +59,7 @@ prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) - **Amazon Bedrock** is used two ways: **embeddings** (turn text into a meaning vector so matching is semantic) and the **LLM** (answer on a miss). - **Amazon S3 Vectors** is the cache store *and* the similarity search - pay-per-use, no always-on cost. This is the primitive that makes a serverless semantic cache economical. - **AWS Lambda** is stateless glue. The cache lives entirely in S3 Vectors, so it survives cold starts, redeploys, and env recycling. -- **SSM Parameter Store** holds the epoch counter for force-invalidation. +- **AWS Systems Manager Parameter Store** holds the epoch counter for force-invalidation, and optionally the app-level API key as a SecureString. ### Correctness features - **Tunable similarity threshold** (default cosine 0.85) - per-deploy and per-request. @@ -71,40 +71,53 @@ prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) ## Requirements - An AWS account with permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. -- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2, recent enough to include the `s3vectors` commands. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2. - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). +- Python 3, only needed to run the test payload helper commands further below. - Amazon Bedrock **model access enabled** for the embeddings model (`amazon.titan-embed-text-v2:0`) and the text model (`amazon.nova-lite-v1:0`) in your Region. - A Region where Amazon S3 Vectors and Amazon Bedrock are available (e.g. `us-east-1`). ## Deployment -S3 Vectors is not yet a CloudFormation resource, so create the vector store first (two commands), then deploy the rest with SAM. +Amazon S3 Vectors has native CloudFormation support, so the template creates the vector +bucket and the cosine index for you. There are no manual setup commands. ```bash -# 1. Create the S3 Vectors bucket and a cosine index (1024 dims = Titan v2) -export VECTOR_BUCKET="semantic-cache-$(aws sts get-caller-identity --query Account --output text)" -aws s3vectors create-vector-bucket --vector-bucket-name "$VECTOR_BUCKET" -aws s3vectors create-index \ - --vector-bucket-name "$VECTOR_BUCKET" \ - --index-name prompt-cache --data-type float32 --dimension 1024 --distance-metric cosine \ - --metadata-configuration 'nonFilterableMetadataKeys=prompt,response,model,created_at,epoch' - -# 2. Build and deploy the Lambda + IAM + SSM epoch parameter +git clone https://github.com/aws-samples/serverless-patterns +cd serverless-patterns/bedrock-semantic-cache-s3vectors-sam + sam build sam deploy --guided -# - VectorBucket: value of $VECTOR_BUCKET above -# - VectorIndex : prompt-cache -# - ApiKey : (optional) a secret for the x-api-key header, or leave blank for IAM-only +# - VectorBucketName : semantic-cache (must be unique per Region) +# - VectorIndex : prompt-cache +# - ApiKeyParameterName : (optional) see below, or leave blank for IAM-only auth ``` Note the `FunctionUrl` and `FunctionName` outputs. +### Optional: app-level API key + +The function URL already requires IAM (SigV4) auth. If you also want an application-level +key checked against the `x-api-key` header, store it as a SecureString in AWS Systems +Manager Parameter Store and pass only the parameter **name** to the stack. The secret +value is fetched at runtime with decryption and is never placed in a Lambda environment +variable. + +```bash +aws ssm put-parameter \ + --name /semantic-cache/api-key \ + --value "your-secret-key" \ + --type SecureString + +# then deploy with ApiKeyParameterName = /semantic-cache/api-key +``` + ## Testing The function URL uses AWS_IAM auth (SigV4). The simplest test is a direct invoke: ```bash -KEY="" +KEY="" payload() { python3 -c "import json,sys;print(json.dumps({'headers':{'x-api-key':'$KEY'},'body':json.dumps({'prompt':sys.argv[1]})}))" "$1" > ev.json; } # MISS (calls Bedrock) @@ -139,9 +152,10 @@ Expected: exact/semantic repeats HIT (`cached=true` with a similarity score); un ```bash sam delete -aws s3vectors delete-index --vector-bucket-name "$VECTOR_BUCKET" --index-name prompt-cache -aws s3vectors delete-vector-bucket --vector-bucket-name "$VECTOR_BUCKET" aws ssm delete-parameter --name /semantic-cache/epoch +# If you created one: aws ssm delete-parameter --name /semantic-cache/api-key +# sam delete removes the vector bucket and index too. If the bucket delete fails +# because it still holds vectors, delete the index first and retry. ``` --- diff --git a/bedrock-semantic-cache-s3vectors-sam/src/cache.py b/bedrock-semantic-cache-s3vectors-sam/src/cache.py index aafc14c5d..fe2b8117c 100644 --- a/bedrock-semantic-cache-s3vectors-sam/src/cache.py +++ b/bedrock-semantic-cache-s3vectors-sam/src/cache.py @@ -21,7 +21,7 @@ def _has_negation(text): DEFAULT_MODEL = os.environ.get("LLM_MODEL", "amazon.nova-lite-v1:0") SIM_THRESHOLD = float(os.environ.get("SIM_THRESHOLD", "0.85")) TTL_SECONDS = int(os.environ.get("TTL_SECONDS", "86400")) -API_KEY = os.environ.get("API_KEY", "") +API_KEY_PARAM = os.environ.get("API_KEY_PARAM", "") EPOCH_PARAM = os.environ.get("EPOCH_PARAM", "/semantic-cache/epoch") br = boto3.client("bedrock-runtime", region_name=REGION) @@ -29,6 +29,26 @@ def _has_negation(text): ssm = boto3.client("ssm", region_name=REGION) _epoch = {"val": None, "ts": 0.0} +_api_key = {"val": None, "loaded": False} + + +def api_key(): + """Read the optional app-level key from Parameter Store, once per environment. + + Only the parameter name is configured on the function. The secret value is never + stored in a Lambda environment variable; it is fetched here with decryption. + """ + if not API_KEY_PARAM: + return "" + if not _api_key["loaded"]: + try: + _api_key["val"] = ssm.get_parameter( + Name=API_KEY_PARAM, WithDecryption=True + )["Parameter"]["Value"] + except Exception: + _api_key["val"] = "" + _api_key["loaded"] = True + return _api_key["val"] or "" def current_epoch(): @@ -69,7 +89,8 @@ def llm(prompt, model): def handler(event, context): t0 = time.time() headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} - if API_KEY and headers.get("x-api-key") != API_KEY: + expected_key = api_key() + if expected_key and headers.get("x-api-key") != expected_key: return _resp(401, {"error": "unauthorized"}) body = {} if event.get("body"): diff --git a/bedrock-semantic-cache-s3vectors-sam/template.yaml b/bedrock-semantic-cache-s3vectors-sam/template.yaml index 8d592838b..25a419d60 100644 --- a/bedrock-semantic-cache-s3vectors-sam/template.yaml +++ b/bedrock-semantic-cache-s3vectors-sam/template.yaml @@ -7,13 +7,16 @@ Description: > (bedrock-semantic-cache-s3vectors-sam) Parameters: - VectorBucket: + VectorBucketName: Type: String - Description: Name of the S3 Vectors vector bucket that stores the cache (create before deploy - see README). + Default: semantic-cache + Description: > + Name for the S3 Vectors vector bucket created by this template. Must be 3-63 + characters, lowercase letters, numbers and hyphens only, and unique per Region. VectorIndex: Type: String Default: prompt-cache - Description: Name of the S3 Vectors index (dimension 1024, distance metric cosine). + Description: Name of the S3 Vectors index created by this template. SimThreshold: Type: String Default: "0.85" @@ -22,21 +25,51 @@ Parameters: Type: String Default: "86400" Description: Freshness window in seconds. Entries older than this are treated as a miss. - ApiKey: + ApiKeyParameterName: Type: String Default: "" - NoEcho: true - Description: Optional app-level API key checked in the x-api-key header. Leave blank to rely only on IAM auth. + Description: > + Optional. Name of an existing AWS Systems Manager Parameter Store SecureString + parameter (for example /semantic-cache/api-key) holding an app-level key checked + against the x-api-key header. Leave blank to rely only on IAM auth. The value is + read at runtime and is never stored in a Lambda environment variable. EmbedModel: Type: String Default: amazon.titan-embed-text-v2:0 - Description: Bedrock embeddings model used for semantic matching (must be consistent; matches the index dimension). + Description: Bedrock embeddings model used for semantic matching (must match the index dimension). LlmModel: Type: String Default: amazon.nova-lite-v1:0 Description: Bedrock model used to generate the answer on a cache miss. +Conditions: + HasApiKeyParam: !Not [!Equals [!Ref ApiKeyParameterName, ""]] + Resources: + # The vector store is created by the template. Amazon S3 Vectors has native + # CloudFormation support, so no manual CLI setup is required. + VectorBucket: + Type: AWS::S3Vectors::VectorBucket + Properties: + VectorBucketName: !Ref VectorBucketName + + # Dimension 1024 and cosine match the Titan Text Embeddings V2 model above. + VectorIndexResource: + Type: AWS::S3Vectors::Index + Properties: + VectorBucketArn: !Ref VectorBucket + IndexName: !Ref VectorIndex + DataType: float32 + Dimension: 1024 + DistanceMetric: cosine + MetadataConfiguration: + NonFilterableMetadataKeys: + - prompt + - response + - model + - created_at + - epoch + # Global cache epoch for one-call force-invalidation (bump this to invalidate everything). CacheEpoch: Type: AWS::SSM::Parameter @@ -47,6 +80,7 @@ Resources: SemanticCacheFunction: Type: AWS::Serverless::Function + DependsOn: VectorIndexResource Properties: FunctionName: semantic-cache Runtime: python3.13 @@ -58,14 +92,16 @@ Resources: - arm64 Environment: Variables: - VECTOR_BUCKET: !Ref VectorBucket + VECTOR_BUCKET: !Ref VectorBucketName VECTOR_INDEX: !Ref VectorIndex EMBED_MODEL: !Ref EmbedModel LLM_MODEL: !Ref LlmModel SIM_THRESHOLD: !Ref SimThreshold TTL_SECONDS: !Ref TtlSeconds - API_KEY: !Ref ApiKey EPOCH_PARAM: /semantic-cache/epoch + # Only the parameter NAME is passed. The secret value itself is fetched at + # runtime with decryption, so it never appears in the function configuration. + API_KEY_PARAM: !Ref ApiKeyParameterName FunctionUrlConfig: AuthType: AWS_IAM Policies: @@ -74,23 +110,40 @@ Resources: Effect: Allow Action: bedrock:InvokeModel Resource: "arn:aws:bedrock:*::foundation-model/*" + # Only the three actions cache.py actually calls. GetVectors is required + # because query_vectors is called with returnMetadata=True. - Sid: S3Vectors Effect: Allow Action: - s3vectors:QueryVectors - s3vectors:GetVectors - - s3vectors:ListVectors - s3vectors:PutVectors - - s3vectors:GetIndex Resource: - - !Sub "arn:aws:s3vectors:${AWS::Region}:${AWS::AccountId}:bucket/${VectorBucket}" - - !Sub "arn:aws:s3vectors:${AWS::Region}:${AWS::AccountId}:bucket/${VectorBucket}/index/*" + - !Sub "arn:aws:s3vectors:${AWS::Region}:${AWS::AccountId}:bucket/${VectorBucketName}" + - !Sub "arn:aws:s3vectors:${AWS::Region}:${AWS::AccountId}:bucket/${VectorBucketName}/index/*" - Sid: EpochParam Effect: Allow Action: - ssm:GetParameter - ssm:PutParameter Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/semantic-cache/epoch" + - !If + - HasApiKeyParam + - Sid: ReadApiKeyParameter + Effect: Allow + Action: ssm:GetParameter + Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${ApiKeyParameterName}" + - !Ref AWS::NoValue + - !If + - HasApiKeyParam + - Sid: DecryptApiKeyParameter + Effect: Allow + Action: kms:Decrypt + Resource: "*" + Condition: + StringEquals: + kms:ViaService: !Sub "ssm.${AWS::Region}.amazonaws.com" + - !Ref AWS::NoValue Outputs: FunctionUrl: @@ -99,3 +152,9 @@ Outputs: FunctionName: Description: Lambda function name (for aws lambda invoke testing). Value: !Ref SemanticCacheFunction + VectorBucketArn: + Description: ARN of the S3 Vectors vector bucket created by this template. + Value: !Ref VectorBucket + VectorIndexArn: + Description: ARN of the S3 Vectors index created by this template. + Value: !Ref VectorIndexResource From 7a6666b506f813f73ff2c9f4defbeca895aa4240 Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Wed, 23 Sep 2026 12:04:18 +0530 Subject: [PATCH 7/9] Make cache epoch parameter and function name stack-scoped so the pattern can deploy more than once per account --- bedrock-semantic-cache-s3vectors-sam/template.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/template.yaml b/bedrock-semantic-cache-s3vectors-sam/template.yaml index 25a419d60..cd2fc6d91 100644 --- a/bedrock-semantic-cache-s3vectors-sam/template.yaml +++ b/bedrock-semantic-cache-s3vectors-sam/template.yaml @@ -74,7 +74,7 @@ Resources: CacheEpoch: Type: AWS::SSM::Parameter Properties: - Name: /semantic-cache/epoch + Name: !Sub "/${AWS::StackName}/epoch" Type: String Value: "1" @@ -82,7 +82,6 @@ Resources: Type: AWS::Serverless::Function DependsOn: VectorIndexResource Properties: - FunctionName: semantic-cache Runtime: python3.13 Handler: cache.handler CodeUri: src/ @@ -98,7 +97,7 @@ Resources: LLM_MODEL: !Ref LlmModel SIM_THRESHOLD: !Ref SimThreshold TTL_SECONDS: !Ref TtlSeconds - EPOCH_PARAM: /semantic-cache/epoch + EPOCH_PARAM: !Ref CacheEpoch # Only the parameter NAME is passed. The secret value itself is fetched at # runtime with decryption, so it never appears in the function configuration. API_KEY_PARAM: !Ref ApiKeyParameterName @@ -126,7 +125,7 @@ Resources: Action: - ssm:GetParameter - ssm:PutParameter - Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/semantic-cache/epoch" + Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/epoch" - !If - HasApiKeyParam - Sid: ReadApiKeyParameter From 29bbac3cdc8a8404b2781f0d0d2b4ee7203594c5 Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Wed, 23 Sep 2026 15:11:24 +0530 Subject: [PATCH 8/9] Align Requirements and cost note with the repository pattern model --- bedrock-semantic-cache-s3vectors-sam/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md index 3eb4d6568..9e3472fd5 100644 --- a/bedrock-semantic-cache-s3vectors-sam/README.md +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -4,7 +4,7 @@ Return cached answers for **semantically similar** prompts - different wording s Learn more at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-semantic-cache-s3vectors-sam -> Important: this application uses AWS services (AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, AWS Systems Manager) and there are costs associated with these services after the Free Tier usage. You are responsible for any AWS costs incurred. No warranty is implied in this example. +> Important: this application uses AWS services (AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, AWS Systems Manager) and there are costs associated with these services after the Free Tier usage, 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. --- @@ -70,12 +70,14 @@ prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) ## Requirements +- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one, and an IAM principal with permissions to make the necessary service calls and manage the resources in this pattern. - An AWS account with permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2. - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). - Python 3, only needed to run the test payload helper commands further below. - Amazon Bedrock **model access enabled** for the embeddings model (`amazon.titan-embed-text-v2:0`) and the text model (`amazon.nova-lite-v1:0`) in your Region. - A Region where Amazon S3 Vectors and Amazon Bedrock are available (e.g. `us-east-1`). +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed. ## Deployment From bd8f26c6a361e6b21e0d52ff56882f1461f0b403 Mon Sep 17 00:00:00 2001 From: manishh-13 Date: Wed, 23 Sep 2026 15:12:03 +0530 Subject: [PATCH 9/9] Remove duplicated AWS account wording in Requirements --- bedrock-semantic-cache-s3vectors-sam/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bedrock-semantic-cache-s3vectors-sam/README.md b/bedrock-semantic-cache-s3vectors-sam/README.md index 9e3472fd5..50ac4d68d 100644 --- a/bedrock-semantic-cache-s3vectors-sam/README.md +++ b/bedrock-semantic-cache-s3vectors-sam/README.md @@ -71,7 +71,7 @@ prompt --> [Lambda] --embed--> Amazon Bedrock (Titan v2 -> 1024-dim vector) ## Requirements - [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one, and an IAM principal with permissions to make the necessary service calls and manage the resources in this pattern. -- An AWS account with permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. +- Permissions for AWS Lambda, Amazon Bedrock, Amazon S3 Vectors, and AWS Systems Manager. - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2. - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). - Python 3, only needed to run the test payload helper commands further below.