Production RAG on Google Cloud
Concept
Retrieval-Augmented Generation (RAG) grounds an LLM in your own data. At query time you retrieve relevant chunks from a vector (and/or keyword) store and pass them as context so the model answers from facts you control, with citations. On GCP you assemble it from Cloud Storage, Document AI, an embedding model, a vector store (Vertex AI Vector Search, AlloyDB AI, or others), and a Gemini model โ served from Cloud Run.
Problem it solves. LLMs hallucinate and don't know your private or fresh data. RAG injects authoritative context per query, giving grounded, cite-able answers without retraining โ and lets you update knowledge by re-indexing documents instead of fine-tuning.
โ When to use
- Answers must come from your private, changing, or citable data
- You need to update knowledge frequently without retraining
- You need source attribution and auditability
โ When not to
- The model's built-in knowledge already suffices (adds latency/cost for nothing)
- Data fits comfortably in the prompt and rarely changes (just prompt it)
- The task is pure reasoning/generation with no external facts
Use cases
- Document Q&A over policies, contracts, manuals
- Enterprise/internal knowledge search
- Customer-support assistants grounded in your docs
- Resume/candidate search, insurance document assistants
Production use cases
- Multi-tenant RAG platform with per-tenant isolation and metadata filtering
- Async event-driven ingestion of uploaded PDFs with OCR via Document AI
- Agentic RAG where an agent decides when and how to retrieve
Limitations
- Quality is bounded by retrieval โ bad chunks โ bad answers
- Ingestion is a data pipeline with its own failure/cost/latency concerns
- Vector store endpoints bill continuously; embeddings cost per token
- Requires evaluation to catch regressions (retrieval and faithfulness)
Bedrock Knowledge Bases + OpenSearch
Azure AI Search + Azure OpenAI
Interactive architecture
Accessible text description
flowchart TD
subgraph Ingest[Ingestion loop - async]
UP[Upload to Cloud Storage] --> EVT[Eventarc / Pub/Sub]
EVT --> JOB[Cloud Run Job]
JOB --> DOCAI[Document AI - parse / OCR]
DOCAI --> CHUNK[Chunk + enrich metadata]
CHUNK --> EMB[Embeddings - Vertex AI]
EMB --> IDX[(Vector store: Vector Search / AlloyDB AI)]
JOB --> STATE[(Ingestion state table)]
end
subgraph Query[Query loop - sync]
Q[User question] --> API[FastAPI on Cloud Run]
API --> QEMB[Embed query]
QEMB --> RET[Hybrid retrieve + metadata filter]
RET --> IDX
RET --> RR[Rerank]
RR --> CTX[Build context]
CTX --> GEN[Gemini - generate + cite]
GEN --> API
end
classDef gcp fill:#e8f0fe,stroke:#4285f4,color:#174ea6;
class UP,EVT,JOB,DOCAI,CHUNK,EMB,IDX,STATE,API,QEMB,RET,RR,CTX,GEN gcp; How it works
RAG has two loops: an ingestion loop (documents โ parse โ chunk โ embed โ index, usually asynchronous) and a query loop (question โ retrieve โ rerank โ build context โ generate โ cite). Production quality lives in the retrieval and evaluation details, not the generation call.
- 1 ยท Ingest & validateCloud Storage + Cloud Run Job
Files land in a bucket; an event triggers a Job that validates MIME type/size and (conceptually) scans before processing.
โ Reject invalid/oversized files to a quarantine prefix ๐ Ingestion state table, DLQ depth ๐ฐ Storage + job compute - 2 ยท Parse & extractDocument AI / parsers
Extract text (and layout) from PDFs/images via OCR; keep page/section metadata for citations.
โ OCR failures โ retry / dead-letter ๐ Parse success rate - 3 ยท Chunk & enrichChunker
Split into semantically coherent chunks with overlap; attach metadata (source, page, tenant, ACL) for filtering.
โ Bad chunking silently degrades retrieval ๐ Chunk size distribution - 4 ยท Embed & indexEmbeddings + vector store
Embed chunks and upsert into the vector store with metadata. Store the chunk text alongside for context assembly.
โ Partial index โ inconsistent answers; use idempotent upserts ๐ Index size, upsert errors ๐ฐ Embedding tokens; continuous index endpoint cost - 5 ยท RetrieveRetriever
Embed the query, do dense (+ optional sparse/keyword) search with metadata filters (e.g. tenant ACL), fetch top-k.
โ Low recall โ missing context; wrong filter โ data leakage ๐ Recall@k, latency - 6 ยท Rerank & build contextReranker
Reorder candidates by relevance, trim to the token budget, and construct a grounded prompt with source markers.
๐ Context relevance score - 7 ยท Generate & citeGemini via Vertex AI
Generate the answer constrained to the context, returning citations to the source chunks.
โ Hallucination if grounding is weak; handle safety blocks ๐ Faithfulness, citation accuracy, token cost
Hands-on example
Cloud Console
- Create a Cloud Storage bucket for source documents (uniform bucket-level access, public access prevention)
- Enable Vertex AI, Document AI, and (if used) Vector Search APIs
- Prototype prompts in Vertex AI Studio; decide on chunk size and top-k
- Choose a vector store: Vertex AI Vector Search, AlloyDB AI (pgvector), or Vertex AI RAG Engine (managed)
Code
PROJECT=my-project; REGION=us-central1
gcloud config set project "$PROJECT"
gcloud services enable aiplatform.googleapis.com documentai.googleapis.com \
run.googleapis.com eventarc.googleapis.com pubsub.googleapis.com
# Secure document bucket
gcloud storage buckets create gs://$PROJECT-rag-docs \
--location=$REGION --uniform-bucket-level-access --public-access-prevention
# Deploy the ingestion worker as a Cloud Run Job
gcloud run jobs deploy rag-ingest \
--image "$REGION-docker.pkg.dev/$PROJECT/apps/rag-ingest:latest" \
--region "$REGION" \
--service-account rag-ingest-sa@$PROJECT.iam.gserviceaccount.com \
--set-env-vars EMBED_MODEL=text-embedding-005
# Trigger the Job when objects are finalized in the bucket (Eventarc)
gcloud eventarc triggers create rag-ingest-trigger \
--location=$REGION \
--destination-run-job=rag-ingest \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=$PROJECT-rag-docs" \
--service-account=eventarc-sa@$PROJECT.iam.gserviceaccount.com # pip install google-genai
import os
from google import genai
from google.genai import types
client = genai.Client() # Vertex AI via ADC
GEN_MODEL = os.getenv("GENAI_MODEL", "gemini-2.0-flash")
EMBED_MODEL = os.getenv("EMBED_MODEL", "text-embedding-005")
def embed_query(q: str) -> list[float]:
r = client.models.embed_content(model=EMBED_MODEL, contents=[q])
return r.embeddings[0].values
def retrieve(vector: list[float], tenant: str, k: int = 8) -> list[dict]:
"""Query your vector store (AlloyDB AI / Vector Search).
ALWAYS filter by tenant to prevent cross-tenant leakage."""
# Pseudocode for AlloyDB AI (pgvector):
# SELECT chunk_id, source, page, text
# FROM chunks
# WHERE tenant = %s
# ORDER BY embedding <=> %s LIMIT %s
...
return [] # [{chunk_id, source, page, text}, ...]
def answer(question: str, tenant: str) -> dict:
chunks = retrieve(embed_query(question), tenant)
# Build a grounded prompt with numbered sources for citations
context = "\n\n".join(f"[{i+1}] ({c['source']} p.{c['page']}) {c['text']}"
for i, c in enumerate(chunks))
system = ("Answer ONLY from the sources. Cite sources as [n]. "
"If the answer isn't in the sources, say you don't know.")
resp = client.models.generate_content(
model=GEN_MODEL,
contents=f"{system}\n\nSources:\n{context}\n\nQuestion: {question}",
config=types.GenerateContentConfig(temperature=0.1, max_output_tokens=1024),
)
return {
"answer": resp.text,
"citations": [{"n": i + 1, "source": c["source"], "page": c["page"]}
for i, c in enumerate(chunks)],
"tokens": resp.usage_metadata.total_token_count,
} # Retrieval hits your vector store's API; generation uses the Vertex AI
# generateContent endpoint (see the Vertex AI topic for the exact URL pattern). resource "google_storage_bucket" "docs" {
name = "${var.project_id}-rag-docs"
location = var.region
uniform_bucket_level_access = true
public_access_prevention = "enforced"
versioning { enabled = true }
}
# AlloyDB as the vector store (enable the vector extension in SQL after creation)
resource "google_alloydb_cluster" "rag" {
cluster_id = "rag-cluster"
location = var.region
network_config { network = google_compute_network.vpc.id }
}
resource "google_alloydb_instance" "primary" {
cluster = google_alloydb_cluster.rag.name
instance_id = "rag-primary"
instance_type = "PRIMARY"
machine_config { cpu_count = 2 }
} Verify
- Upload a PDF โ confirm the Cloud Run Job runs and the ingestion state table updates
- Query a known fact โ confirm the answer includes correct [n] citations
- Query something absent โ confirm it says 'I don't know' (grounding works)
- Query as tenant A for tenant B's data โ confirm zero cross-tenant results
- Run your eval set โ check recall@k and faithfulness scores
Cleanup
- Delete Vector Search index endpoints (continuous billing) or the AlloyDB cluster
- Empty and delete the document bucket
- Delete the Cloud Run Job, Eventarc trigger, and service accounts
Troubleshooting
- Answers cite nothing / say 'I don't know' for known facts โ retrieval recall problem (chunking, k, embedding model, filters)
- Hallucinated facts โ weaken by tightening the system prompt and lowering temperature; verify context actually reached the model
- Cross-tenant leakage โ missing/incorrect metadata filter on retrieval
- Slow queries โ cache query embeddings, reduce k, add a reranker only where it pays off
- Ingestion stuck โ check the DLQ, Job logs, and idempotent upsert logic
Production considerations
Scalability
Decouple ingestion (async Jobs/Batch, scales independently) from query (Cloud Run autoscaling). Batch embeddings and re-embed only changed docs.
Availability
Query path availability follows Cloud Run + vector store + Vertex AI; add model fallback and cache. Ingestion can tolerate delay if it's async with a DLQ.
Performance
Cache query embeddings and frequent answers (semantic cache in Memorystore); keep top-k and context lean; rerank only when it improves quality enough to justify latency.
Reliability
Idempotent upserts, checkpointed ingestion, dead-letter topics, and retries with backoff. Make re-runs safe.
Networking
Keep buckets and vector store private; reach them via Direct VPC egress/PSC; VPC-SC for exfiltration protection.
Security
Per-tenant metadata filtering is mandatory; validate/scan uploads; CMEK for sensitive data; least-privilege SAs for ingest vs query; redact PII in logs.
Monitoring
Retrieval metrics (recall@k, MRR, nDCG), generation metrics (faithfulness, groundedness, citation accuracy, hallucination rate), latency, and cost per query. Add feedback capture.
Cost
Optimize embeddings (batch, dedupe, re-embed deltas), right-size the vector store, cache, and choose a smaller generation model where quality allows. Report cost per query.
Quotas
Embedding and generation token quotas per region; Vector Search index/endpoint limits.
Data protection
Bucket versioning + retention; encrypt with CMEK; define deletion/retention per tenant; keep an audit trail of ingested documents.
Disaster recovery
Source documents in Cloud Storage are the source of truth โ you can always rebuild the index. Replicate buckets cross-region; snapshot the vector store.
Multi-region
Ingest once, replicate the index or re-embed per region; route queries to the nearest region; mind cross-region egress.
What interviewers expect you to know
Definitions
- RAG, chunking, dense vs sparse vs hybrid retrieval, reranking, grounding, faithfulness
- Recall@k, MRR, nDCG (retrieval); faithfulness, groundedness, citation accuracy (generation)
Design questions
- Design an async, event-driven ingestion pipeline for uploaded PDFs at scale
- Design a multi-tenant RAG platform with strict tenant isolation
- How would you evaluate and monitor RAG quality continuously?
Trade-offs
- Vertex AI Vector Search (managed scale) vs AlloyDB AI (SQL + transactional + hybrid) vs RAG Engine (fully managed)
- Bigger chunks (more context) vs smaller chunks (precision)
- RAG vs long-context prompting vs fine-tuning
Failure scenarios
- Retrieval quality drops after a data update โ how do you detect and fix it?
- Cross-tenant data leaks in results โ root cause and prevention?
- Ingestion backlog grows โ how do you diagnose and drain it?
Common follow-ups
- How do you cite sources and prove groundedness?
- How do you keep cost per query bounded?
- How do you re-index only what changed?
Misconceptions
- 'RAG is just embeddings + a prompt' โ production is ingestion, filtering, eval, and monitoring
- 'Bigger context replaces retrieval' โ cost, latency, and lost-in-the-middle disagree
- 'One eval at launch is enough' โ you need continuous/online evaluation
๐ฌ A strong answer
I'd split RAG into an async ingestion loop and a sync query loop. Uploads land in a private Cloud Storage bucket; an Eventarc trigger runs a Cloud Run Job that validates, parses with Document AI, chunks with overlap and tenant/ACL metadata, embeds, and idempotently upserts into the vector store (AlloyDB AI when I want SQL + hybrid + transactional data, Vector Search for very large scale). The query path embeds the question, does hybrid retrieval with a mandatory tenant filter, reranks, builds a grounded prompt, and generates with Gemini returning [n] citations at low temperature. I'd measure recall@k and faithfulness on a golden set in CI, monitor cost per query, cache query embeddings, and capture user feedback for online eval. Source docs in Cloud Storage are the source of truth, so I can always rebuild the index.
Common mistakes
- Skipping evaluation โ shipping RAG with no recall/faithfulness metrics
- No per-tenant metadata filter โ cross-tenant data leakage
- Poor chunking (too big/small, no overlap, no metadata) tanking retrieval
- Re-embedding everything on every change instead of deltas (cost blowup)
- Synchronous ingestion in the request path (should be async Jobs/Batch)
- Leaving Vector Search index endpoints deployed and forgotten (continuous cost)
- Not returning citations, so answers can't be trusted or audited
- Ignoring safety/blocked responses and PII in logs
Comparisons
Vertex AI Vector Search vs AlloyDB AI
| Dimension | Vertex AI Vector Search | AlloyDB AI (pgvector) |
|---|---|---|
| Managed ingestion | Purpose-built ANN service | You manage schema + upserts in Postgres |
| SQL + transactional data | No โ vectors only | Yes โ join vectors with relational data |
| Hybrid (keyword+vector) | Limited | Natural in SQL |
| Scale | Very large, low-latency ANN | Large, bounded by instance sizing |
| Best for | Massive corpora, pure similarity | Multi-tenant apps needing SQL, filters, transactions |
Verdict. AlloyDB AI when you want SQL, hybrid search, and transactional data together; Vector Search for very large pure-similarity workloads.
Knowledge check
Uploaded PDFs must be ingested at scale without blocking the upload API. Best design?
In a multi-tenant RAG system, the single most important control to prevent data leakage is:
Users report the assistant now misses answers that used to work, right after a document refresh. First thing to check?
Which change most reduces RAG cost at scale without hurting quality much?