Home Resume
Homeβ€Ί Blogβ€Ί I Built a RAG-Powered Search for My Blog β€” Here's …
AWS Terraform

I Built a RAG-Powered Search for My Blog β€” Here's Why and How

☁️ Personal Project πŸ€– AWS Bedrock ⚑ Serverless πŸ—οΈ Terraform πŸ’° ~$0/month
RSS Feed β†’ Lambda β†’ S3 β†’ Titan Embed v2 β†’ Claude Haiku 4.5 β†’ jayanthkatta.com

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.

64blog posts indexed
~$0monthly cost
<2squery response
30sdaily index job

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

BUG pip installs Windows binaries by default

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.

Why not a vector database? With 64 posts and a few hundred chunks, cosine similarity on a JSON file in Lambda memory is fast enough and costs nothing. A vector DB like Pinecone or pgvector adds cost and complexity that isn't justified at this scale.

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.

The insight: retrieval and summarization are different jobs. My search widget's whole point is retrieval β€” find the 4 chunks most relevant to a question. A per-post summary doesn't need retrieval at all, since the indexer already has each post's full text in hand while building the index. What it needed was a model that could actually read a post and describe it well β€” something a frequency-count heuristic was never going to do.

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:0 directly returned a ValidationException telling me to use an inference profile instead. The fix was straightforward once I knew to look for it β€” call us.anthropic.claude-haiku-4-5-20251001-v1:0 instead, 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 /summary route 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.

2Lambda functions
3Bedrock models
100%Terraform IaC
0servers to manage

AWS Lambda Amazon S3 API Gateway EventBridge Amazon Bedrock Titan Embed v2 Claude Haiku 4.5 Terraform RAG Serverless

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent