Tuto — Microservice d'embeddings en Python (FastAPI)
Public visé : développeur PHP/Symfony qui découvre Python. Objectif : exposer un petit service HTTP qui transforme du texte en vecteurs (embeddings), consommé par le cœur RAG côté Symfony.
Contexte : ce service est le composant « Embeddings » de la spec Lot 0 — cœur RAG. Pour comprendre les concepts, voir la fiche Embeddings et recherche sémantique.
Pourquoi un service séparé en Python ?
- L'écosystème d'inférence IA (modèles,
sentence-transformers) est mûr en Python. - On isole l'inférence dans un service stateless appelé en HTTP → la frontière avec Symfony est nette, et chaque brique reste dans son langage de prédilection.
- Le modèle tourne en local (CPU suffisant pour de l'embedding) : gratuit, on ne paie l'API que pour la génération.
Prérequis
- Python 3.10+ (
python3 --version). - Accès réseau pour télécharger le modèle au premier lancement.
- Modèle retenu :
intfloat/multilingual-e5-base(multilingue, adapté au français).
1. Environnement isolé
mkdir embeddings-service && cd embeddings-service python3 -m venv .venv source .venv/bin/activate # Windows : .venv\Scripts\activate pip install --upgrade pip pip install "fastapi" "uvicorn[standard]" "sentence-transformers"
Bon réflexe : figer les versions ensuite avec
pip freeze > requirements.txt.
2. Le service (app.py)
from typing import Literal from fastapi import FastAPI from pydantic import BaseModel, Field from sentence_transformers import SentenceTransformer MODEL_NAME = "intfloat/multilingual-e5-base" model = SentenceTransformer(MODEL_NAME) # chargé une fois au démarrage app = FastAPI(title="Embeddings service") class EmbedRequest(BaseModel): type: Literal["query", "passage"] # 422 automatique si autre valeur texts: list[str] = Field(min_length=1) # 422 si la liste est vide @app.get("/health") def health(): return { "status": "ok", "model": MODEL_NAME, "dim": model.get_sentence_embedding_dimension(), } @app.post("/embed") def embed(req: EmbedRequest): # Convention e5 : préfixer selon l'usage (gérée côté service) prefix = "query: " if req.type == "query" else "passage: " inputs = [prefix + t for t in req.texts] # normalize_embeddings=True → vecteurs prêts pour la similarité cosinus vectors = model.encode(inputs, normalize_embeddings=True).tolist() return { "model": MODEL_NAME, "dim": model.get_sentence_embedding_dimension(), "vectors": vectors, }
Points clés :
- Le modèle est chargé une seule fois (au démarrage), pas à chaque requête.
- Les préfixes e5 (
query:/passage:) sont indispensables à la qualité — on les ajoute ici, le client Symfony n'a pas à s'en soucier. - Validation stricte (
Literal+textsnon vide) : une entrée invalide renvoie HTTP 422 automatiquement — pas de fallback silencieux qui produirait un mauvais préfixe. normalize_embeddings=Truesimplifie la similarité cosinus côté index.
3. Lancer le service
uvicorn app:app --host 127.0.0.1 --port 8001
On écoute sur
127.0.0.1(local) : le service ne doit pas être exposé publiquement. En production, on le place derrière le reverse proxy / on le garde sur la boucle locale.
4. Tester
# Santé curl http://127.0.0.1:8001/health # Embeddings d'un passage curl -X POST http://127.0.0.1:8001/embed \ -H "Content-Type: application/json" \ -d '{"type":"passage","texts":["Comment réinitialiser mon mot de passe ?"]}'
La réponse contient dim (768 pour e5-base) et vectors (un vecteur par texte). Le contrat correspond à la spec ia-coeur.md §4.1.
5. Appel depuis Symfony (côté cœur)
// Implémentation de EmbeddingClientInterface (extrait) $response = $this->httpClient->request('POST', $this->serviceUrl . '/embed', [ 'json' => ['type' => $type, 'texts' => $texts], 'timeout' => 30, ]); $data = $response->toArray(); return $data['vectors']; // float[][]
L'URL du service (rag.embedding.service_url) est injectée par configuration.
6. Déploiement (note)
- Sur le VPS (CPU only), le service tourne en tâche de fond — par exemple via un service systemd dédié, dans son venv.
- Surveiller la mémoire : un seul modèle chargé ; éviter d'en charger plusieurs.
- Exposer
/healthpour la supervision (la commandeapp:rag:statscôté Symfony l'interroge).
Bonnes pratiques retenues
- Stateless : aucune donnée persistée par le service ; il calcule et répond.
- Local par défaut : pas d'exposition publique, pas de données sensibles (cf. cadre IA).
- Contrat stable :
/healthet/embeddocumentés, alignés sur la spec du cœur. - Versions figées (
requirements.txt) pour la reproductibilité.
Pour aller plus loin
- Spec du socle :
specs/ia-coeur.md - Concepts : Embeddings et recherche sémantique
- Déploiement local vs API : fiche 2.5