Leçon 10 — Projet final : le moteur produits

Assembler, tester et livrer le moteur

5 min de lecture

Assembler, tester et livrer le moteur

Le déroulé complet

sequenceDiagram
  participant D as Développeur
  participant T as pytest (sans clé)
  participant M as moteur.py
  participant G as gemini-embedding-2
  participant E as evaluer.py
  D->>T: pytest -q
  T-->>D: 8 passed
  D->>M: python moteur.py indexer
  M->>G: 3 lots de 20 fiches
  G-->>M: 60 vecteurs
  D->>M: chercher / similaires / doublons / clusters
  D->>E: python evaluer.py
  E->>G: 31 requêtes (2 lots)
  E-->>D: rappel@5 par langue, BM25, dimensions
terminal
cd files
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env            # mettez votre clé dans GOOGLE_API_KEY
pytest -q
python generer_produits.py
python moteur.py indexer
python moteur.py chercher "faire le ménage sans y penser"
python moteur.py doublons
python evaluer.py

Tester sans clé : un faux embedder

Le faux embedder transforme chaque mot en une case d'un vecteur de 64 nombres (par hachage). Il ne « comprend » rien, mais il est déterministe et suffit pour tester toute la mécanique : cache, persistance, top k, doublons, k-means, rappel.

files/test_moteur.py (extrait)
def faux_embed(textes, dim=64):
    m = np.zeros((len(textes), dim), dtype=np.float32)
    for i, t in enumerate(textes):
        for mot in re.findall(r"\w+", t.lower()):
            m[i, zlib.crc32(mot.encode()) % dim] += 1
    return moteur.normaliser(m + 1e-6)


def test_index_persistant_et_cache(tmp_path, produits):
    e = Compteur()
    assert moteur.indexer(produits, e, tmp_path) == 60
    assert moteur.indexer(produits, e, tmp_path) == 0          # rien n'a changé
    produits[4]["description"] += " Nouveau coloris."
    assert moteur.indexer(produits, e, tmp_path) == 1          # une seule fiche recalculée
TestCe qu'il protège
test_cosinus_et_top_kclassement et exclusion
test_index_persistant_et_cache0 appel si rien ne change, 1 si une fiche change
test_recherche_similaires_doublonsrecherche, exclusion de soi, détection d'un doublon connu
test_kmeans_separe_deux_groupesk-means sur deux nuages évidents
test_rappel_at_kla formule d'évaluation
test_embedder_envoie_une_content_par_textele piège de l'agrégation, les lots de 20, la normalisation
test_prefixes, test_pureteformatage des textes, score de pureté

Sur Vertex AI

Même code, autres variables. Deux différences constatées le 2026-10-15 : gemini-embedding-2 est servi en global (pas en europe-west1), et un seul contenu est accepté par requête, ce que embedder_gemini gère automatiquement :

files/moteur.py (extrait)
client = client or genai.Client()
if getattr(client, "vertexai", False):
    lot = 1  # sur Vertex AI, gemini-embedding-2 n'accepte qu'un contenu par requête
terminal
gcloud auth application-default login
export GOOGLE_GENAI_USE_VERTEXAI=TRUE GOOGLE_CLOUD_PROJECT=mon-projet GOOGLE_CLOUD_LOCATION=global
unset GOOGLE_API_KEY
python moteur.py indexer && python evaluer.py

Pourquoi le faux embedder suffit-il pour tester le cache de l'index ?

Sur Vertex AI, pourquoi l'indexation de 60 fiches fait-elle 60 requêtes au lieu de 3 ?

Exercice : un critère qui casse

Passez EMBED_DIM=128 dans .env, relancez python moteur.py indexer puis python evaluer.py. Combien de fiches sont recalculées, et le critère « rappel@5 ≥ 0,90 » tient-il ?

Voir la solution

Les 60 fiches sont recalculées : la dimension fait partie de l'empreinte. D'après la troncature mesurée par evaluer.py (ligne dimension 128), le rappel@5 attendu tourne autour de 0,93 : le critère global tient encore, mais de peu. C'est le genre de compromis (4 fois moins de stockage contre quelques points de rappel) qu'on ne tranche qu'avec une mesure.

Résultat obtenu

Le 2026-10-15, tests sans clé :

$ pytest -q
........                                                                 [100%]
8 passed in 0.64s

Vérification des critères d'acceptation avec l'API Gemini (sorties détaillées dans les leçons précédentes) :

CritèreRésultat
Rappel@5 global ≥ 0,900,99
Anglais / espagnol / allemand ≥ 0,901,00 / 1,00 / 1,00
Meilleur que BM25 sur chaque langueoui (BM25 en fr / en / es / de : 0,65 / 0,00 / 0,00 / 0,50)
Doublons connus retrouvés3 sur 3, aucun faux positif au seuil 0,8
Réindexation sans changement0 fiche recalculée

Et la même chaîne sur Vertex AI (GOOGLE_CLOUD_LOCATION=global, chemin du dossier raccourci en index/) :

$ python moteur.py indexer   # Vertex AI, location global
Index à jour dans index/ (60 fiche(s) recalculée(s), modèle gemini-embedding-2, 768 dims)
$ python evaluer.py
langue   n   rappel@5 embeddings   rappel@5 BM25
fr      24                0.98            0.65
en       3                1.00            0.00
es       2                1.00            0.00
de       2                1.00            0.50
toutes  31                0.99            0.54

Mêmes chiffres qu'avec la clé d'API, au centième près.

Je sais faire

  • Dérouler le projet de l'installation à l'évaluation
  • Tester toute la mécanique du moteur avec un faux embedder déterministe
  • Faire tourner le même code sur l'API Gemini et sur Vertex AI
  • Vérifier des critères d'acceptation chiffrés avant de livrer