Kenshu Dev
🔍Article

RAG local complet : Ollama + pgvector en 15 min

Pipeline 100% offline : embeddings avec nomic-embed-text, stockage dans PostgreSQL, requêtes en Python. Docker Compose + code prêt à l'emploi.

18 mai 2025·8 min de lecture

RAG local complet : Ollama + pgvector

Un RAG (Retrieval-Augmented Generation) qui tourne 100% sur ta machine. Tes documents restent privés, zéro frais d'API.

Architecture

Tes docs (PDF, MD, TXT)
    ↓  chunking
nomic-embed-text (Ollama)  →  vecteurs
    ↓  stockage
PostgreSQL + pgvector
    ↓  recherche sémantique
qwen3:8b (Ollama)  →  réponse

1. Docker Compose (PostgreSQL + pgvector)

# docker-compose.yml
services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: rag_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
docker compose up -d

2. Modèles Ollama

ollama pull nomic-embed-text   # embeddings (274 Mo)
ollama pull qwen3:8b           # génération de réponses

3. Setup Python

pip install psycopg2-binary openai langchain-text-splitters

4. Indexation des documents

import psycopg2
from openai import OpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Client Ollama (compatible OpenAI)
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# Connexion DB
conn = psycopg2.connect("postgresql://user:password@localhost/rag_db")
cur = conn.cursor()

# Init pgvector
cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
cur.execute("""
    CREATE TABLE IF NOT EXISTS documents (
        id SERIAL PRIMARY KEY,
        content TEXT,
        embedding vector(768),
        source TEXT
    )
""")
conn.commit()

def embed(text: str) -> list[float]:
    resp = client.embeddings.create(model="nomic-embed-text", input=text)
    return resp.data[0].embedding

def index_document(path: str):
    text = open(path).read()
    splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    chunks = splitter.split_text(text)
    for chunk in chunks:
        vec = embed(chunk)
        cur.execute(
            "INSERT INTO documents (content, embedding, source) VALUES (%s, %s, %s)",
            (chunk, vec, path)
        )
    conn.commit()
    print(f"Indexé : {len(chunks)} chunks depuis {path}")

index_document("mon_document.md")

5. Requêtes RAG

def rag_query(question: str, top_k: int = 3) -> str:
    q_vec = embed(question)

    cur.execute("""
        SELECT content, source,
               1 - (embedding <=> %s::vector) AS similarity
        FROM documents
        ORDER BY embedding <=> %s::vector
        LIMIT %s
    """, (q_vec, q_vec, top_k))

    results = cur.fetchall()
    context = "\n\n---\n\n".join(
        f"Source: {r[1]}\n{r[0]}" for r in results
    )

    resp = client.chat.completions.create(
        model="qwen3:8b",
        messages=[
            {"role": "system", "content": f"Réponds en utilisant ce contexte:\n\n{context}"},
            {"role": "user", "content": question}
        ]
    )
    return resp.choices[0].message.content

print(rag_query("Quelles sont les étapes de déploiement ?"))

Optimisations

Index HNSW (requêtes plus rapides sur gros volumes) :

CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

Cas d'usage

  • RAG sur ta codebase pour expliquer du code legacy
  • Chatbot sur ta documentation interne
  • Recherche dans tes notes Obsidian
  • Q&A sur des PDFs confidentiels (RGPD friendly)
#rag#ollama#pgvector#postgres#python#local-ai
← Retour à Signal