Production RAG on GCP ยท Generative AI

Production RAG on Google Cloud

Advanced โฑ 10โ€“12 hours 22 lessons 9 labs 26 interview Qs Production: Critical Cert: Medium

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)
Alternatives
Long-context promptingFine-tuningVertex AI RAG Engine (managed)Agentic retrieval
Related services
Cloud StorageDocument AIVertex AIVector SearchAlloyDB AICloud RunPub/SubCloud Run Jobs
AWS equivalent

Bedrock Knowledge Bases + OpenSearch

Azure equivalent

Azure AI Search + Azure OpenAI

Interactive architecture

Production RAG on Google Cloud โ€” architecture
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;
Drag to pan ยท scroll to zoom
Two loops: asynchronous ingestion (upload โ†’ event โ†’ Cloud Run Job โ†’ parse โ†’ chunk โ†’ embed โ†’ index) and synchronous query (embed โ†’ hybrid retrieve โ†’ rerank โ†’ context โ†’ generate with citations).
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. 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. 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. 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. 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. 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. 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. 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

  1. Create a Cloud Storage bucket for source documents (uniform bucket-level access, public access prevention)
  2. Enable Vertex AI, Document AI, and (if used) Vector Search APIs
  3. Prototype prompts in Vertex AI Studio; decide on chunk size and top-k
  4. Choose a vector store: Vertex AI Vector Search, AlloyDB AI (pgvector), or Vertex AI RAG Engine (managed)

Code

bash
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
python
# 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,
    }
bash
# Retrieval hits your vector store's API; generation uses the Vertex AI
# generateContent endpoint (see the Vertex AI topic for the exact URL pattern).
hcl
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 }
}
โš ๏ธ Three cost centers: embeddings (per token, at ingest and per query), the vector store (index endpoints / AlloyDB bill continuously), and generation (per token). Track cost per query and re-embed only changed documents.
๐Ÿ”’ Enforce tenant/ACL metadata filtering on every retrieval or you'll leak data across tenants. Validate & scan uploads, keep buckets private, use CMEK for sensitive data, and never log full document contents with PII.

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

DimensionVertex AI Vector SearchAlloyDB AI (pgvector)
Managed ingestionPurpose-built ANN serviceYou manage schema + upserts in Postgres
SQL + transactional dataNo โ€” vectors onlyYes โ€” join vectors with relational data
Hybrid (keyword+vector)LimitedNatural in SQL
ScaleVery large, low-latency ANNLarge, bounded by instance sizing
Best forMassive corpora, pure similarityMulti-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

architectureQ1

Uploaded PDFs must be ingested at scale without blocking the upload API. Best design?

securityQ2

In a multi-tenant RAG system, the single most important control to prevent data leakage is:

troubleshootingQ3

Users report the assistant now misses answers that used to work, right after a document refresh. First thing to check?

costQ4

Which change most reduces RAG cost at scale without hurting quality much?

Answer the questions to check your understanding.