RAG with DeepSeek + Ollama

DeepSeekOllamaRAGTutorial

Build production-ready RAG systems using DeepSeek models with Ollama for local inference.

RAG with DeepSeek + Ollama

Overview

Build a production-ready RAG system using DeepSeek's powerful models with Ollama for local inference. This tutorial covers the complete pipeline from data ingestion to semantic search.

Prerequisites

  • Python 3.10+
  • Ollama installed locally
  • DeepSeek API key (optional for cloud fallback)
  • ~8GB RAM for local models

Installation

# Install Ollama
# macOS
brew install ollama

# Linux
curl -fsSL https://ollama.com/install.sh | sh

# Windows: Download from https://ollama.com

# Pull required models
ollama pull deepseek-coder:33b
ollama pull nomic-embed-text:latest

# Install Python dependencies
pip install langchain langchain-community sentence-transformers chromadb

Architecture Overview

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Documents │────▶│  Embedding   │────▶│   Vector    │
│   (PDF,MD)  │     │   Model      │     │  Database   │
└─────────────┘     └──────────────┘     └─────────────┘
                                                  │
                                                  ▼
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Response  │◀────│  DeepSeek    │◀────│  Retrieval  │
│   (Claude)  │     │  / Ollama    │     │   Query     │
└─────────────┘     └──────────────┘     └─────────────┘

Step 1: Data Ingestion

Document Loading

# src/ingestion.py
from langchain_community.document_loaders import (
    PyPDFLoader,
    DirectoryLoader,
    TextLoader,
    MarkdownLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter

def load_documents(directory: str) -> list:
    """Load documents from directory."""
    loaders = [
        DirectoryLoader(directory, glob="**/*.pdf", loader_cls=PyPDFLoader),
        DirectoryLoader(directory, glob="**/*.md", loader_cls=MarkdownLoader),
        DirectoryLoader(directory, glob="**/*.txt", loader_cls=TextLoader),
    ]
    
    documents = []
    for loader in loaders:
        documents.extend(loader.load())
    
    return documents

def split_documents(documents: list, chunk_size: int = 1000, chunk_overlap: int = 200) -> list:
    """Split documents into chunks."""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", ".", " ", ""]
    )
    return splitter.split_documents(documents)

Metadata Extraction

from pathlib import Path

def extract_metadata(doc) -> dict:
    """Extract metadata from document."""
    return {
        'source': str(doc.metadata.get('source', '')),
        'page': doc.metadata.get('page', 0),
        'file_type': Path(doc.metadata.get('source', '')).suffix,
        'chunk_index': 0,
    }

Step 2: Embedding Generation

Using Nomic Embed (Local)

# src/embeddings.py
from langchain_community.embeddings import OllamaEmbeddings

def get_local_embeddings() -> OllamaEmbeddings:
    """Get local embeddings using Ollama."""
    return OllamaEmbeddings(
        model="nomic-embed-text",
        base_url="http://localhost:11434"
    )

# Generate embeddings
embeddings = get_local_embeddings()
texts = ["Document chunk 1", "Document chunk 2"]
embeddings_list = embeddings.embed_documents(texts)

Using DeepSeek Embeddings (Cloud)

# For higher quality embeddings
from langchain_openai import OpenAIEmbeddings

def get_deepseek_embeddings(api_key: str) -> OpenAIEmbeddings:
    """Get embeddings using DeepSeek API."""
    return OpenAIEmbeddings(
        model="deepseek-embed",
        api_key=api_key,
        base_url="https://api.deepseek.com/v1"
    )

Step 3: Vector Database Setup

ChromaDB Configuration

# src/vectorstore.py
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings

def create_vectorstore(
    documents: list,
    embeddings: OllamaEmbeddings,
    persist_directory: str = "./chroma_db"
) -> Chroma:
    """Create and persist vector store."""
    vectorstore = Chroma.from_documents(
        documents=documents,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    return vectorstore

def load_vectorstore(
    persist_directory: str,
    embeddings: OllamaEmbeddings
) -> Chroma:
    """Load existing vector store."""
    return Chroma(
        persist_directory=persist_directory,
        embedding_function=embeddings
    )

Hybrid Search Setup

def enable_hybrid_search(vectorstore: Chroma):
    """Configure hybrid search (keyword + semantic)."""
    # Chroma supports hybrid search via BM25
    from langchain_community.retrievers import BM25Retriever
    
    # Create keyword retriever
    texts = [doc.page_content for doc in vectorstore.similarity_search("", k=1000)]
    bm25_retriever = BM25Retriever.from_texts(texts)
    bm25_retriever.k = 10
    
    return bm25_retriever

Step 4: Retrieval Pipeline

Basic Retrieval

# src/retriever.py
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker

def create_retriever(vectorstore, top_k: int = 5) -> ContextualCompressionRetriever:
    """Create retrieval pipeline with reranking."""
    
    # Base retriever
    base_retriever = vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": top_k * 2}  # Get more for reranking
    )
    
    # Reranker using local model
    reranker = CrossEncoderReranker(
        model="cross-encoder/ms-marco-MiniLM-L-6-v2",
        top_n=top_k
    )
    
    # Create compression retriever
    retriever = ContextualCompressionRetriever(
        base_compressor=reranker,
        base_retriever=base_retriever
    )
    
    return retriever

Multi-Query Retrieval

from langchain.retrievers import MultiQueryRetriever

def create_multi_query_retriever(vectorstore, llm) -> MultiQueryRetriever:
    """Generate multiple queries for better coverage."""
    
    retriever = MultiQueryRetriever.from_llm(
        retriever=vectorstore.as_retriever(),
        llm=llm,
        parser_key="lines"
    )
    
    return retriever

Step 5: Generation with DeepSeek

Local Generation with Ollama

# src/generator.py
from langchain_community.llms import Ollama

def get_local_llm() -> Ollama:
    """Get local LLM using Ollama."""
    return Ollama(
        model="deepseek-coder:33b",
        base_url="http://localhost:11434",
        temperature=0.7,
        num_ctx=4096
    )

def generate_response(
    llm: Ollama,
    query: str,
    context: str
) -> str:
    """Generate response with context."""
    
    prompt = f"""Based on the following context, answer the question.
    
Context:
{context}

Question: {query}

Answer:"""
    
    return llm.invoke(prompt)

Cloud Generation with DeepSeek API

from langchain_openai import ChatOpenAI

def get_deepseek_llm(api_key: str) -> ChatOpenAI:
    """Get DeepSeek cloud model."""
    return ChatOpenAI(
        model="deepseek-chat",
        api_key=api_key,
        base_url="https://api.deepseek.com/v1",
        temperature=0.7
    )

Step 6: Complete RAG Pipeline

End-to-End Implementation

# src/rag_pipeline.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class RAGPipeline:
    vectorstore: Chroma
    retriever: ContextualCompressionRetriever
    llm: Ollama
    
    def query(self, question: str, top_k: int = 3) -> dict:
        """Execute complete RAG query."""
        
        # Retrieve relevant documents
        docs = self.retriever.get_relevant_documents(question)
        
        # Format context
        context = "\n\n".join([d.page_content for d in docs])
        
        # Generate answer
        prompt = f"""You are a helpful assistant. Answer the question using only the provided context.
        
Context:
{context}

Question: {question}

Answer:"""
        
        answer = self.llm.invoke(prompt)
        
        return {
            'answer': answer,
            'sources': [d.metadata['source'] for d in docs],
            'contexts': [d.page_content for d in docs]
        }

# Usage
pipeline = RAGPipeline(
    vectorstore=load_vectorstore("./chroma_db", get_local_embeddings()),
    retriever=create_retriever(load_vectorstore("./chroma_db", get_local_embeddings())),
    llm=get_local_llm()
)

result = pipeline.query("How do I implement authentication?")
print(result['answer'])

Advanced Features

Streaming Responses

async def stream_response(pipeline: RAGPipeline, question: str):
    """Stream response for better UX."""
    
    docs = pipeline.retriever.get_relevant_documents(question)
    context = "\n\n".join([d.page_content for d in docs])
    
    prompt = f"""Context:
{context}

Question: {question}

Answer:"""
    
    async for chunk in pipeline.llm.astream(prompt):
        print(chunk, end='', flush=True)

Citation Generation

def add_citations(answer: str, docs: list) -> str:
    """Add citations to answer."""
    
    citation_map = {}
    for i, doc in enumerate(docs):
        source = doc.metadata.get('source', 'unknown')
        if source not in citation_map:
            citation_map[source] = len(citation_map) + 1
    
    # Add citations to answer
    for source, num in citation_map.items():
        answer = answer.replace(source, f"{source}[^{num}]")
    
    return answer, citation_map

Query Rewriting

def rewrite_query(query: str, history: list[str]) -> str:
    """Rewrite query based on conversation history."""
    
    if not history:
        return query
    
    prompt = f"""Given the conversation history and the latest question, 
rewrite the question to be standalone.

History:
{'; '.join(history)}

Latest Question: {query}

Standalone Question:"""
    
    return llm.invoke(prompt)

Performance Optimization

Caching

from functools import lru_cache
import hashlib

@lru_cache(maxsize=100)
def cached_query(query_hash: str, vectorstore: Chroma) -> list:
    """Cache query results."""
    return vectorstore.similarity_search(query_hash, k=5)

def get_query_hash(query: str) -> str:
    """Generate hash for query caching."""
    return hashlib.md5(query.encode()).hexdigest()

Batch Processing

async def batch_ingest(documents: list, batch_size: int = 100):
    """Ingest documents in batches."""
    
    for i in range(0, len(documents), batch_size):
        batch = documents[i:i + batch_size]
        await vectorstore.aadd_documents(batch)
        print(f"Processed {i + len(batch)} / {len(documents)} documents")

Troubleshooting

Common Issues

Ollama not responding:

# Check Ollama is running
ollama list

# Restart if needed
ollama serve

Embedding dimension mismatch:

# Ensure embedding model matches vectorstore
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# nomic-embed-text produces 768-dim vectors

Slow retrieval:

# Use HNSW index for faster search
vectorstore = Chroma.from_documents(
    documents=documents,
    embedding=embeddings,
    collection_settings={
        "hnsw:space": "cosine",
        "hnsw:construction_ef": 128,
        "hnsw:search_ef": 64
    }
)

Resources


Last updated: May 2026