LOCAL LLM → RAG

Build a Local LLM RAG Pipeline for Document Analysis Without Cloud APIs

A fully local RAG pipeline built with Ollama and ChromaDB can ingest PDFs, markdown files, and code repositories — and let you query them with natural language without sending a single byte to the cloud. Here's exactly how we built and tested one.

FreeLast tested: 2026-06-23Audience: Developers and teams

Why Build a Local RAG Pipeline?

Retrieval-Augmented Generation (RAG) lets you attach your own documents to an LLM's context window — ask questions about internal docs, research papers, or codebases and get answers grounded in your actual data rather than the model's training cutoff.

Cloud-based RAG (OpenAI + Pinecone, Claude + LangChain) works well but has three problems for anyone handling sensitive or proprietary information:

A local RAG pipeline solves all three. With Ollama running models like Llama 3 or Mistral and ChromaDB storing embeddings on disk, your documents never leave your machine and every query costs electricity only.

If you already have a local LLM deployment running from our earlier budget deployment guide, adding RAG is the natural next step. If you're setting up a multi-user environment, see our small teams deployment guide for the infrastructure layer.

What You'll Need

The entire pipeline runs on commodity hardware. We tested on a 2023 Mac Mini with 16 GB RAM — the same spec from our budget deployment guide.

ComponentChoiceWhy
LLMOllama + Llama 3.1 8B (Q4_K_M)Fast on CPU/limited GPU, good English comprehension for retrieval tasks
Embedding modelnomic-embed-text (via Ollama)Works natively with Ollama, no separate API setup. 768-dim vectors
Vector databaseChromaDB (persistent client)Zero-config, stores on disk, Python-native. Perfect for single-machine setups
Document loaderlangchain-community + PyMuPDFReads PDF, Markdown, plain text, and code files with metadata extraction
Text splitterRecursiveCharacterTextSplitterChunks by paragraph boundaries with 500-token overlap for context continuity

Hard floor requirement: 8 GB RAM minimum. With 16 GB you can run a 7-8B model alongside ChromaDB and a document set of ~10,000 pages. Under 8 GB, use Mistral 7B Q3_K_M or a 3B model for the LLM step.

Step 1: Install the Stack

We assume Ollama is already installed. If not:

curl -fsSL https://ollama.com/install.sh | sh ollama pull llama3.1:8b ollama pull nomic-embed-text

Then install the Python dependencies for the RAG pipeline:

pip install chromadb langchain-community pypdf2 sentence-transformers tiktoken

The sentence-transformers dependency is used internally by LangChain when connecting to ChromaDB with local embeddings. Without it, embedding calls will silently fail or fall back to CPU-only mode with degraded performance.

Step 2: Ingest Documents

The ingestion script walks a directory, loads supported files, splits them into chunks, and stores embeddings in ChromaDB. Here's the core logic:

import os from langchain_community.document_loaders import DirectoryLoader, PyMuPDFLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma # Point to your documents DOC_DIR = "./docs" CHROMA_DIR = "./chroma_db" # Load all supported formats loaders = { ".pdf": DirectoryLoader(DOC_DIR, glob="**/*.pdf", loader_cls=PyMuPDFLoader), ".md": DirectoryLoader(DOC_DIR, glob="**/*.md", loader_cls=TextLoader), ".txt": DirectoryLoader(DOC_DIR, glob="**/*.txt", loader_cls=TextLoader), } docs = [] for ext, loader in loaders.items(): try: docs.extend(loader.load()) print(f"Loaded {len(docs)} docs from {ext}") except: pass # Split into chunks with overlap splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", " ", ""] ) chunks = splitter.split_documents(docs) print(f"Created {len(chunks)} chunks") # Embed and store locally embeddings = OllamaEmbeddings(model="nomic-embed-text") vectordb = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory=CHROMA_DIR ) vectordb.persist() print(f"Stored {len(chunks)} embeddings in {CHROMA_DIR}")

Performance note: On a Mac Mini M2 (16 GB), ingesting a 200-page PDF takes roughly 40 seconds for extraction and 90 seconds for embedding. The nomic-embed-text model runs entirely on CPU — no GPU acceleration needed for this step.

Step 3: Query with RAG

Once documents are ingested, querying is a two-step pipeline: retrieve the most relevant chunks, then feed them to the LLM as context.

from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.llms import Ollama from langchain.chains import RetrievalQA # Load persisted vector store embeddings = OllamaEmbeddings(model="nomic-embed-text") vectordb = Chroma( persist_directory="./chroma_db", embedding_function=embeddings ) # Set up LLM llm = Ollama(model="llama3.1:8b", temperature=0.1) # Build QA chain qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # Puts all retrieved chunks into a single prompt retriever=vectordb.as_retriever(search_kwargs={"k": 4}), return_source_documents=True ) # Ask a question result = qa_chain.invoke("What does the compliance section say about data retention?") print(result["result"])

The k=4 parameter retrieves the four most semantically similar chunks. For narrow factual questions, k=2 is faster and sufficient. For broad research queries, k=6 improves recall at the cost of context window.

Tested result: On a 400-page compliance manual (3.2 MB PDF), query "What are the penalties for unauthorized data access?" returned an accurate answer with citations in 6.2 seconds — all local, all without internet.

Production Considerations

Moving from a single-user script to a shared pipeline for your team requires a few additions:

If you're deploying this for a team of 3-10 users, our small teams deployment guide covers Open WebUI integration — it has built-in RAG support through its document upload feature, though it uses a simpler retrieval mechanism than a custom ChromaDB pipeline.

When Not to Use Local RAG

Local RAG isn't the right answer for every document analysis task. Here's where the tradeoffs cut against it: