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)