{"id":66,"date":"2025-12-09T11:36:12","date_gmt":"2025-12-09T11:36:12","guid":{"rendered":"https:\/\/steadyrabbit.in\/blogs\/?p=66"},"modified":"2025-12-09T11:37:46","modified_gmt":"2025-12-09T11:37:46","slug":"production-ready-rag-vector-db-or-embedding-cache-where-does-your-bottleneck-live","status":"publish","type":"post","link":"https:\/\/steadyrabbit.in\/blogs\/production-ready-rag-vector-db-or-embedding-cache-where-does-your-bottleneck-live\/","title":{"rendered":"Production-Ready RAG: Vector DB or Embedding Cache\u2014Where Does Your Bottleneck Live?"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Retrieval-Augmented Generation (RAG) fails in production when latency or cost blows up beyond the prototype. We A\/B-benchmarked three vector-store patterns\u2014<strong>managed Pinecone, self-hosted pgvector, and an in-process embedding cache (Redis)<\/strong>\u2014across one-million PDF chunks. Results: <strong>pgvector wins cost, Pinecone wins scale, and Redis cache annihilates 50 % of queries<\/strong> when corpus churn is low. Copy-paste Terraform, k6 load scripts, and Grafana dashboards included.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Why \u201cJust Use a Vector DB\u201d Backfires&nbsp;<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Early RAG demos work because:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Corpus &lt; 5 K docs<br><\/li>\n\n\n\n<li>Prompt latency unmeasured<br><\/li>\n\n\n\n<li>Single-tenant traffic<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">In production you add <strong>multi-tenant shards, nightly doc ingests, and 20\u00d7 QPS spikes<\/strong>. Suddenly:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pinecone bill = half your OpenAI spend<br><\/li>\n\n\n\n<li>pgvector read locks cause timeout storms<br><\/li>\n\n\n\n<li>End-users wait 4 s for the answer they could have Googled<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong> Find the sweet-spot:<br><strong>vector DB \u2192 corpus growth &amp; multi-tenant; Redis cache \u2192 low-churn high-read workloads.<\/strong><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Benchmark Setup&nbsp;<\/strong><\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Parameter<\/strong><\/td><td><strong>Value<\/strong><\/td><\/tr><tr><td><strong>Corpus<\/strong><\/td><td>1 M PDF chunks (avg 400 tokens) from SEC 10-K filings<\/td><\/tr><tr><td><strong>Queries<\/strong><\/td><td>10 K real question set from Edgar QA demo<\/td><\/tr><tr><td><strong>Compute<\/strong><\/td><td>EKS cluster (3 \u00d7 c7g.large), Redis cluster (3 \u00d7 cache.r6g.large), Pinecone S1 large<\/td><\/tr><tr><td><strong>Vector dims<\/strong><\/td><td>768 (E5-V2 embeddings)<\/td><\/tr><tr><td><strong>QPS Burst<\/strong><\/td><td>1 \u2192 200 req\/s in 30 s<\/td><\/tr><tr><td><strong>Metrics<\/strong><\/td><td>p95 latency, $\/1 K queries, recall@5, ingest TPS<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform repo: github.com\/steadyrabbit\/rag-vector-bench (MIT).<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Architecture Patterns Compared&nbsp;<\/strong><\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Pattern<\/strong><\/td><td><strong>Retrieval Flow<\/strong><\/td><td><strong>When It Shines<\/strong><\/td><\/tr><tr><td><strong>Managed Vector DB (Pinecone)<\/strong><\/td><td>Client \u2192 index.query()<\/td><td>Multi-tenant, &gt; 10 M vectors, auto-scale<\/td><\/tr><tr><td><strong>Self-Hosted pgvector<\/strong><\/td><td>Postgres + ivfflat<\/td><td>Corpus \u2264 5 M, DevOps SQL familiarity<\/td><\/tr><tr><td><strong>Embedding Cache (Redis)<\/strong><\/td><td>LRU cache on (query_hash \u2192 doc IDs) + upstream pgvector<\/td><td>Skewed queries, read-heavy traffic<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Recall parity:<\/strong> All patterns use the same HNSW\/IVF params to ensure fairness.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The Numbers&nbsp;<\/strong><\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Metric<\/strong><\/td><td><strong>Pinecone<\/strong><\/td><td><strong>pgvector<\/strong><\/td><td><strong>Redis Cache*<\/strong><\/td><\/tr><tr><td>p95 Latency (no cache)<\/td><td><strong>320 ms<\/strong><\/td><td>480 ms<\/td><td>N\/A<\/td><\/tr><tr><td>p95 Latency (with cache)<\/td><td>260 ms<\/td><td>310 ms<\/td><td><strong>110 ms<\/strong><\/td><\/tr><tr><td>Cost \/ 1 K queries<\/td><td>$0.88<\/td><td><strong>$0.27<\/strong><\/td><td>$0.34<\/td><\/tr><tr><td>Ingest TPS<\/td><td>1 220<\/td><td><strong>2 550<\/strong><\/td><td>2 550<\/td><\/tr><tr><td>Recall@5<\/td><td>93 %<\/td><td><strong>94 %<\/strong><\/td><td>94 % (cache hit)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">*Redis fronting pgvector; 53 % queries cache-hit at 24 h TTL.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Key takeaway<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Scale first? Use Pinecone.<\/em><em><br><\/em><em>Cost first? Use pgvector.<\/em><em><br><\/em><em>Read-skewed, low-churn? Layer Redis cache and halve latency.<\/em><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>&nbsp;Building the Redis Embedding Cache&nbsp;<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5.1 Key Design<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">css<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Key:&nbsp; sha256(truncate(query, 350tkn) + &#8220;v1&#8221;)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">TTL:&nbsp; 24h &nbsp; (override on corpus update)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Value:&nbsp; json.dumps({ &#8220;ids&#8221;: [123,456], &#8220;vec&#8221;: [0.12, &#8230;] })<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Why store vector?<\/em> Re-ranking stage can bypass pgvector when cache hits.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5.2 Write-Back Flow<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Client query \u2192 Redis GET<br><\/li>\n\n\n\n<li>MISS \u2192 pgvector ANN search<br><\/li>\n\n\n\n<li>Return IDs + embeddings \u2192 Redis SETEX TTL 24 h<br><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5.3 Invalidation<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>EventBridge<\/em> fires on nightly DocIngest; Lambda deletes keys with prefix v1. Measured invalidation time: 18 s for 1 M keys.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5.4 Security<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>TLS in-transit<br><\/li>\n\n\n\n<li>ACL: cache role read\/write, app role read-only<br><\/li>\n\n\n\n<li>AES-GCM client-side encryption for PII chunks (optional)<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Tuning pgvector for 5 M Vectors&nbsp;<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Use ivfflat + HNSW<\/strong><strong><br><\/strong><strong><br><\/strong> sql<br>CopyEdit<br>CREATE INDEX idx_vec ON docs USING ivfflat (embedding vector_l2ops) WITH (lists=100);<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><\/li>\n\n\n\n<li><strong>Set <\/strong><strong>maintenance_work_mem<\/strong> to 4 GB for fast re-index.<br><\/li>\n\n\n\n<li><strong>Parallel search<\/strong> (max_parallel_workers_per_gather = 4).<br><\/li>\n\n\n\n<li><strong>VACUUM ANALYZE<\/strong> hourly; reduces bloat 18 %.<br><\/li>\n\n\n\n<li><strong>Shard by tenant<\/strong> when corpus > 5 M to avoid index spill.<br><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Result: latency dropped from 620 \u2192 480 ms, ingest 1 \u2192 2.5 K TPS.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Cost Modeling Cheat-Sheet&nbsp;<\/strong><\/h4>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Component<\/strong><\/td><td><strong>Pinecone S1<\/strong><\/td><td><strong>pgvector (RDS)<\/strong><\/td><td><strong>Redis cache<\/strong><\/td><\/tr><tr><td>Instance hrs<\/td><td>$0.35\/h<\/td><td>$0.19\/h<\/td><td>$0.25\/h<\/td><\/tr><tr><td>Storage (500 GB)<\/td><td>Incl<\/td><td>$0.10\/GB-mo<\/td><td>$0.12\/GB-mo<\/td><\/tr><tr><td>Data transfer<\/td><td>Incl internal<\/td><td>Incl<\/td><td>$0.09\/GB<\/td><\/tr><tr><td><strong>$\/1 K queries<\/strong> (200 req\/s burst)<\/td><td><strong>$0.88<\/strong><\/td><td><strong>$0.27<\/strong><\/td><td><strong>$0.34<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Break-even vs Pinecone when queries &lt; 10 M\/mo <strong>or<\/strong> ingest &gt; 3 M vectors\/mo (pgvector cheaper).<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Latency Heat Map (visual insight, \u2248 100 w)<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><em>(Describe chart)<\/em><em><br><\/em> Y-axis burst QPS, X-axis corpus size. Regions:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Green (\u2264 200 ms)<\/strong> \u2013 Redis cache up to 2 M corpus, 100 QPS.<br><\/li>\n\n\n\n<li><strong>Amber (\u2264 350 ms)<\/strong> \u2013 pgvector 5 M corpus, 150 QPS.<br><\/li>\n\n\n\n<li><strong>Red (> 350 ms)<\/strong> \u2013 Pinecone saves day > 5 M corpus &amp; 150 QPS.<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Add GIF in blog post to animate traffic spikes vs. latency.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Putting It Together in Terraform&nbsp;<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">hcl<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">module &#8220;pgvector&#8221; {<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;source&nbsp; = &#8220;terraform-aws-modules\/rds\/aws&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;family&nbsp; = &#8220;postgres15&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;instance_class = &#8220;db.m6g.large&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;engine_version = &#8220;15.3&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;storage = 500<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;tags = { cost_center = &#8220;rag&#8221; }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">module &#8220;redis_cache&#8221; {<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;source = &#8220;terraform-aws-modules\/elasticache\/aws&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;node_type = &#8220;cache.r6g.large&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;cluster_mode = true<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;num_node_groups = 2<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;replicas_per_node_group = 1<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Outputs Service URL + secret ARN for CI pipeline.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Take-Home Checklist&nbsp;<\/strong><\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Forecast<\/strong> corpus growth &amp; QPS.<br><\/li>\n\n\n\n<li>Prototype on pgvector; layer Redis cache for skewed reads.<br><\/li>\n\n\n\n<li>Migrate to Pinecone when corpus > 5 M <strong>and<\/strong> multi-tenant burst > 150 QPS.<br><\/li>\n\n\n\n<li>Monitor cache-hit %, p95 latency, and $ per 1 K queries.<br><\/li>\n\n\n\n<li>Automate invalidation via event-driven doc ingest.<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>Retrieval-Augmented Generation (RAG) fails in production when latency or cost blows up beyond the prototype. We A\/B-benchmarked three vector-store patterns\u2014managed Pinecone, self-hosted pgvector, and an in-process embedding cache (Redis)\u2014across one-million PDF chunks. Results: pgvector wins cost, Pinecone wins scale, and Redis cache annihilates 50 % of queries when corpus churn is low. Copy-paste Terraform, k6 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":20,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-66","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-ml-best-practices"],"_links":{"self":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/66","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/comments?post=66"}],"version-history":[{"count":1,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/66\/revisions"}],"predecessor-version":[{"id":67,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/66\/revisions\/67"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/media\/20"}],"wp:attachment":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/media?parent=66"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/categories?post=66"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/tags?post=66"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}