ProductionRAGLLMOps

Privacy-First Local RAG

Offline LLM Inference · Full MLOps Stack

0external API calls — fully offline RAG for regulated teams

Desktop RAG application built for organisations where data must not leave the premises. Runs entirely offline with Ollama local inference (Gemma 2), ChromaDB for vector retrieval, and a complete MLOps stack for experiment tracking, dataset versioning, and drift monitoring.

Engagement

Status
Production (local/offline environment)
Primary KPI
0 external API calls for inference
Starting point
Cloud-only RAG blocked by data residency rules
Scope
Regulated teams handling sensitive documents
My role
AI engineer (local RAG stack + MLOps instrumentation)

01 Overview

From cloud-only RAG blocked by data residency rules to 0 external API calls for inference.

Before

Cloud-only RAG blocked by data residency rules

After

0 external API calls for inference

How it works

A fully offline RAG application designed for air-gapped or regulated environments. Ollama runs a local Gemma 2 inference server with optional CUDA or Metal GPU acceleration, completely eliminating outbound API calls. Documents are ingested, chunked, and stored in a local ChromaDB vector store via a FastAPI backend, with DVC tracking each ingestion run for reproducibility. MLflow logs experiment results and Evidently AI monitors for embedding and query distribution drift over time.

What I built

  • 100% local inference — no data leaves the machine (CUDA / Metal acceleration)
  • DVC-tracked dataset versioning for reproducible ingestion runs
  • MLflow experiment tracking with metric comparison across model versions
  • Evidently AI monitors embedding drift and query distribution shifts

02 The system

Architecture at a glance.

FIG. 02 — SYSTEM ARCHITECTUREon-premises · zero egressretrievegenerateReact UITypeScriptFastAPIRAG backendChromaDBvector storeGemma 2Ollama · localDVC ingestversioned docsMLflow+ Evidently drift
Everything runs inside the machine boundary: versioned ingestion, local embeddings and generation, with MLflow and Evidently watching quality and drift.
  1. 01

    Ollama / Gemma 2 Inference

    Local LLM server with CUDA/Metal support — all inference stays on the host machine with no external API calls.

  2. 02

    ChromaDB Vector Store

    Persistent local vector database storing document embeddings for semantic retrieval.

  3. 03

    FastAPI Backend

    Stateless REST API handling ingestion, querying, and experiment management endpoints.

  4. 04

    React + TypeScript Frontend

    Desktop-style UI for document upload, chat interface, and experiment tracking views.

  5. 05

    DVC Ingestion Pipeline

    Tracks dataset versions and pipeline parameters so every ingestion run is fully reproducible from a single command.

  6. 06

    MLflow + Evidently AI

    MLflow logs retrieval metrics per experiment; Evidently monitors embedding drift and query distribution shifts over time.

Stack

Models
Ollama / Gemma 2
Backend
FastAPI
Data
ChromaDB
MLOps
DVCMLflowEvidently AIDocker
Frontend
React + TypeScript

03 Engineering decisions

The decision that made it production-grade.

Each decision opens with the business problem, contrasts the common approach with what I shipped, then walks through the real code — the highlighted lines follow the step you are reading.

  1. 01Semantic Knowledge Foundation

Decision 01 · Local ML · Regulated Environments

Semantic Knowledge Foundation

In plain termsThis is the knowledge retrieval engine — it finds the most relevant information from private documents using conceptual understanding rather than keyword matching, enabling accurate AI responses without sending data to external servers.

The problem

Every cloud API call is a data residency violation in regulated industries.

Standard RAG pipelines send document text and queries to OpenAI or Cohere. In healthcare, legal, or finance, that egress fails data residency requirements before the first document is indexed — and per-query costs accumulate at scale.

Before · Common approach

openai.Embedding.create() for vectors, GPT-4o for generation. Simple, but every query leaves the premises.

✗ Unusable in air-gapped environments. API key required. Data egress on every call.

After · This implementation

Ollama local inference for both embeddings (nomic-embed-text) and generation (Gemma 2). ChromaDB for vectors. Zero outbound calls.

✓ Runs on CUDA or Metal with no external dependencies. Same retrieval interface as cloud.

How it worksFully Local RAG — Zero Egress
  1. 01Embed documents with Ollama (nomic-embed-text)No API key, no cloud egress, no per-call cost
  2. 02Persist vectors to ChromaDB on local diskCosine similarity index survives process restarts
  3. 03Query: embed → cosine similarity searchSame model at ingestion and query time
  4. 04Generate answer with local Gemma 2 (ollama.chat)CUDA/Metal bring latency close to cloud APIs

✓ Zero data egress · compliant with healthcare / finance data residency

Code walkthrough — 3 steps · scroll or click

local-rag/rag_engine.py
"""
Local RAG engine — privacy-first inference with Ollama + ChromaDB.

WHAT IT DEMONSTRATES:
  How to build a fully offline RAG system where no data leaves the machine.
  Ollama handles local LLM inference (Gemma 2), while ChromaDB provides
  vector retrieval. The ingestion pipeline is DVC-tracked for reproducibility.

WHY IT'S INTERESTING:
  Most RAG tutorials assume cloud APIs. This design swaps every cloud
  dependency for a local equivalent — enabling deployment in air-gapped
  or data-sensitive environments (healthcare, legal, finance) without
  changing the retrieval or generation logic.

NOVELTY:
  The non-trivial engineering decision is preserving the same retrieval and
  generation interface whether backed by Ollama or a cloud LLM. A naive
  implementation hard-codes the inference call; this abstraction means the
  same RAG logic runs offline (Ollama/Gemma 2 via CUDA or Metal) or online
  without branching in the retrieval pipeline. The DVC-tracked ingestion
  layer — often omitted in RAG demos — is what makes the system reproducible:
  any team member can recreate the exact vector index from a versioned dataset
  snapshot, which is essential for regulated environments that require
  auditable knowledge bases.
"""

from dataclasses import dataclass
from pathlib import Path
from typing import Optional
import chromadb
import ollama


@dataclass
class RetrievedChunk:
    content: str
    source: str
    distance: float


class LocalRAGEngine:
    """
    Retrieval-Augmented Generation using only local resources.

    Components:
      - ChromaDB     : persistent vector store (HNSW cosine similarity)
      - Ollama       : local LLM inference (Gemma 2 2B / 9B)
      - nomic-embed  : local embedding model (no OpenAI key required)
    """

    def __init__(
        self,
        collection_name: str,
        embed_model: str = "nomic-embed-text",
        llm_model: str = "gemma2:9b",
        top_k: int = 5,
        persist_dir: str = ".chroma_db",
    ):
        self.embed_model = embed_model
        self.llm_model = llm_model
        self.top_k = top_k

        self.client = chromadb.PersistentClient(path=persist_dir)
        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            metadata={"hnsw:space": "cosine"},
        )

    # ── Ingestion ──────────────────────────────────────────────────────────

    def ingest(self, documents: list[dict]) -> int:
        """
        Add documents to the local vector store.

        Args:
            documents: List of {"id": str, "content": str, "source": str}

        Returns:
            Number of documents indexed.
        """
        ids = [d["id"] for d in documents]
        contents = [d["content"] for d in documents]
        metadatas = [{"source": d["source"]} for d in documents]

        # Generate embeddings locally — no API call, no egress
        embeddings = [
            ollama.embeddings(model=self.embed_model, prompt=text)["embedding"]
            for text in contents
        ]

        self.collection.upsert(
            ids=ids,
            documents=contents,
            embeddings=embeddings,
            metadatas=metadatas,
        )
        return len(documents)

    # ── Retrieval ──────────────────────────────────────────────────────────

    def retrieve(self, query: str) -> list[RetrievedChunk]:
        """Embed the query locally and fetch nearest neighbours."""
        query_embedding = ollama.embeddings(
            model=self.embed_model, prompt=query
        )["embedding"]

        results = self.collection.query(
            query_embeddings=[query_embedding],
            n_results=self.top_k,
            include=["documents", "metadatas", "distances"],
        )

        chunks = []
        for content, meta, dist in zip(
            results["documents"][0],
            results["metadatas"][0],
            results["distances"][0],
        ):
            chunks.append(RetrievedChunk(
                content=content,
                source=meta.get("source", "unknown"),
                distance=dist,
            ))
        return chunks

    # ── Generation ─────────────────────────────────────────────────────────

    def chat(self, query: str, system_prompt: Optional[str] = None) -> str:
        """Retrieve context then generate a grounded answer via Ollama."""
        chunks = self.retrieve(query)
        context = "\n\n".join(
            f"[Source: {c.source}]\n{c.content}" for c in chunks
        )

        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({
            "role": "user",
            "content": f"Context:\n{context}\n\nQuestion: {query}",
        })

        response = ollama.chat(model=self.llm_model, messages=messages)
        return response["message"]["content"]
import openai

class RAGEngine:
    def ingest(self, documents):
        for doc in documents:
            emb = openai.Embedding.create(   # cloud
                input=doc["content"],
                model="text-embedding-3-small"
            )["data"][0]["embedding"]

    def retrieve(self, query):
        q_emb = openai.Embedding.create(    # cloud
            input=query,
            model="text-embedding-3-small"
        )["data"][0]["embedding"]

The Impact

0external API callsAll inference stays on the host machine
100%offline capableCUDA or Metal acceleration, no internet required
1shared interfaceSame retrieve() call works offline or with a cloud backend

Replacing every cloud dependency with a local equivalent — nomic-embed-text for embeddings, Gemma 2 via Ollama for generation, ChromaDB for vectors — enables RAG in regulated industries without changing retrieval or generation logic.

04 Production hardening

What breaks, why, and what keeps it fixed.

The failure modes this system had to survive — each with its root cause, the fix that shipped, and the guardrail that stops it coming back.

  1. 01
    Failure modeFailure mode · Semantic Knowledge Foundation

    Unusable in air-gapped environments. API key required. Data egress on every call.

    Root cause

    Every cloud API call is a data residency violation in regulated industries.

    Fix shipped

    Ollama local inference for both embeddings (nomic-embed-text) and generation (Gemma 2). ChromaDB for vectors. Zero outbound calls.

    Guardrail

    Zero data egress · compliant with healthcare / finance data residency

05 Outcomes

What changed for the business.

  1. 01

    Zero data egress for sensitive workflows

  2. 02

    Reduced external LLM API spend

  3. 03

    Enabled AI use in regulated environments with offline deployment constraints.

Key benefits

  • 100% offline operation — no data leaves the machine, satisfying strict data residency requirements
  • CUDA and Metal acceleration delivers near-cloud inference speed on local hardware
  • DVC-versioned ingestion ensures any historical run can be reproduced exactly
  • Drift monitoring flags model degradation before it affects end-user answer quality
  • Eliminates recurring cloud API costs for high-volume internal document queries

Metrics are from project-specific observation windows. Codebases are private; excerpts are curated for illustration.

Want the full story behind Privacy-First Local RAG?

I’m happy to walk through the architecture, trade-offs and numbers in an interview.