I have been writing on this blog for a while now. Posts about AWS, Terraform, GitOps, investing, health, and life in general. Over time the archive has grown to 64 posts β and I started running into a problem I didn't expect.
I couldn't find my own content.
If I wanted to reference something I wrote about S3 six months ago, I had to scroll. If someone asked me what I'd written about a topic, I had to think. A search bar would have helped, but Blogger's built-in search is basic at best.
So I built something better. A question-answering widget powered by RAG β Retrieval Augmented Generation β running entirely on AWS, embedded on my portfolio at jayanthkatta.com.
This post is about why I built it, what I had to learn, and how I put it together.
Why I Built It
The honest reason: I wanted to actually use what I was learning.
I had been reading about RAG for a while β how it lets you build AI that answers questions from your own content without fine-tuning a model or doing any training. My blog felt like the perfect test case. The content is mine. The domain is narrow. The stakes are low if something breaks.
The secondary reason: I wanted something on my portfolio that wasn't just a list of technologies. Something a visitor could actually interact with and immediately understand what I do.
What I Had to Learn
RAG β Retrieval Augmented Generation
This was the core concept. The idea is straightforward: instead of asking an AI to answer from general knowledge, you find the most relevant pieces of your own content first, then hand those pieces to the AI and ask it to answer only from what you've given it. No fine-tuning. No training data. Just your content, an embedding model, and a language model.
Two models are involved:
- An embedding model β converts text into a list of numbers that captures the meaning of that text. Two pieces of text about the same topic will produce similar numbers.
- A language model β reads the relevant chunks and generates a plain-English answer.
I used Amazon Titan Embed v2 for embeddings and Claude Haiku 4.5 on Bedrock for answer generation β the two models behind the search widget itself. (A third model joined the pipeline later for a different job β see the update below.)
Vector Similarity
Once text is converted to numbers, finding the most relevant chunks comes down to cosine similarity β think of it like comparing directions. If your question and a blog chunk are pointing in the same direction in a mathematical space, they're about the same thing. The closer they are, the more relevant the chunk. I implemented this in plain Python in the Lambda function β no numpy needed at this scale β comparing the question's numbers against every chunk's numbers and returning the top 4 matches.
Serverless Pipeline Design
I had worked with Lambda and API Gateway before, but this was the first time I built a two-Lambda pipeline where one feeds the other indirectly through S3. The indexer writes a vector index to S3. The query Lambda loads it into memory on cold start and serves from RAM on every subsequent request. No database. No vector store. Just a JSON file and cosine math.
Windows + Git Bash Packaging Pain
Lambda runs on Linux. If you build your zip on Windows using Git Bash, pip will install Windows-specific wheels that crash immediately on Lambda with os.add_dll_directory errors.
The fix is one flag that forces pip to download the Linux-compatible binary regardless of your host OS:
pip install -r requirements.txt -t ./dist \
--platform manylinux2014_x86_64 \
--only-binary=:all: \
--python-version 3.12
Small thing. Cost me more time than I'd like to admit.
How I Built It
The architecture has two flows β one that runs daily automatically, and one that runs on demand when someone asks a question.
Daily Indexing β Automatic
An EventBridge rule fires every day and triggers the indexer Lambda. It fetches the current post list from my site's RSS feed, visits each post's actual page for the full text, breaks it into chunks of around 400 words with a 50-word overlap, sends each chunk to Titan Embed v2 to get a vector, and writes the full index β chunks plus vectors β to an S3 bucket as a single JSON file.
The whole job normally finishes in under a minute (it only gets slower the one time it has to back-fill something for every post at once β see the summary update below). In practice I don't wait for the daily schedule β I invoke the indexer manually right after every publish, so a new post is searchable within minutes, not up to a day later. The daily EventBridge run is the safety net in case I forget, not the primary path.
Query Flow β On Demand
When someone types a question on jayanthkatta.com, a POST request goes to API Gateway, which triggers the query Lambda. The Lambda loads the index from S3 into memory on cold start, embeds the question using Titan Embed v2, runs cosine similarity to find the 4 most relevant chunks, and passes them to Nova Lite on Bedrock with a prompt that says: answer only from this context. The response comes back with an answer and links to the source posts.
The whole round trip takes under 2 seconds.
Infrastructure
Everything is Terraform. The S3 bucket, both Lambdas, API Gateway, IAM roles, EventBridge rule, and CloudWatch logs β all defined as code, reproducible from scratch with a single terraform apply. No console clicks involved in the setup.
Update (July 2026): A Third Model, for a Different Job
My blog used to have an "at a glance" summary box at the top of every post β three lines under fixed labels: Problem, What you'll build, The catch. It was built by keyword-frequency extraction: pick the most keyword-dense sentences out of the post and stamp those labels on them regardless of whether they actually fit. It worked passably on my weekly AWS build posts. It broke everywhere else β mislabeled rows, and on one post it even chopped a sentence mid-<code>-tag and printed something like "routes it to ," in public. Of my 64 posts, only 11 are actually shaped like "Problem β Build β Catch." The other 53 β troubleshooting writeups, a day-by-day Terraform series, personal reflections β don't fit that mold, and no keyword heuristic was going to fix that.
So I killed the box, and then rebuilt it properly using the RAG infrastructure I already had running.
I added Claude Haiku 4.5 on Bedrock as a third model, used for exactly one job: once per post, the indexer Lambda sends the full post text to Haiku and asks for three generic, universal lines β Overview, Key detail, Takeaway β not the old fixed Problem/Build/Catch labels. Generic labels turned out to be the actual fix: they read naturally whether the post is a build walkthrough, a root-cause writeup, or a tutorial, because the model decides what's worth saying about that specific post instead of a heuristic forcing every post into the same shape.
Two things I had to get right that weren't obvious going in:
- Claude Haiku 4.5 on Bedrock requires an inference profile, not a direct model ID. Calling
anthropic.claude-haiku-4-5-20251001-v1:0directly returned aValidationExceptiontelling me to use an inference profile instead. The fix was straightforward once I knew to look for it β callus.anthropic.claude-haiku-4-5-20251001-v1:0instead, and grant IAM permission on both the inference-profile ARN and the underlying foundation-model ARNs in every region the profile can route through. - Generation and serving needed to be decoupled from the site build. My static site builds locally, before I push β but the indexer (which has the full post text and generates the summary) only runs against the live site, after a push. Baking the summary into the static HTML at build time would mean a brand-new post's summary is never available for its own first build. Instead the summary is fetched client-side: the indexer writes it to S3, a new
GET /summaryroute on the same API Gateway serves it by slug, and a small script on each post page fetches it on load β same shape as the search widget itself, just a different endpoint. A post with no cached summary yet (indexer hasn't run since it published) just renders nothing. No error, no broken box.
One judgment call, not a technical one: I excluded my Health and Life posts from the box entirely. Haiku's summaries for them were accurate β but a "scan this fast and decide if it's relevant" utility frame felt wrong on a personal reflection. That one's a post-category filter in the build script, not a model limitation.
What It Looks Like
A floating terminal-style button sits in the bottom-right corner of jayanthkatta.com. Click it and a dark terminal overlay opens. Type a question, press Run, and the answer appears with source links back to the original posts.
Try it yourself β go to jayanthkatta.com and click Ask my blog in the bottom right.
What's Next
This was a personal project but the pattern scales. The same architecture β embed your content, store vectors, query at runtime β works for documentation, internal wikis, and support knowledge bases. The only thing that changes is the content source.
If you're looking to build something similar, the full stack is: Lambda + S3 + API Gateway + EventBridge + Bedrock. All serverless. All pay-per-use. Monthly cost at my traffic: effectively zero.
AWS Lambda Amazon S3 API Gateway EventBridge Amazon Bedrock Titan Embed v2 Claude Haiku 4.5 Terraform RAG Serverless
Comments