Algorithme de recommandation des Éclaireurs (EE)
Objectif
À un lycéen qui a terminé son questionnaire, on propose une liste d'Éclaireurs classés par pertinence, qu'il peut contacter. La liste est recalculée à chaque requête — il n'y a ni table de précalcul, ni invalidation de cache à gérer : quand le lycéen met à jour son questionnaire, la reco est automatiquement à jour à la requête suivante.
Comportement par page
| Page | Route front | Endpoint | Randomisation | Pagination |
|---|---|---|---|---|
| Accueil (carousel) | /accueil |
GET /eclaireurs/recommandes?limit=10&seed=<aléatoire> |
Oui — seed aléatoire à chaque chargement de page | Non (carousel 10 EE) |
| Tous les EE (défaut) | /eclaireurs |
GET /users/eclaireurs (hors algo reco) |
Non — tri createdAt DESC |
Oui (9/page desktop) |
| Tous les EE (recherche) | /eclaireurs?search=... |
GET /eclaireurs/search (Elasticsearch texte) |
Non | Non |
| EE d'une piste | /eclaireurs/:pisteSlug |
GET /eclaireurs/recommandes?pisteSlug=...&limit=100 |
Non — seed = lyceenId (stable) | Oui (100/page) |
| Vue piste (carousel) | /pistes/:slug |
GET /eclaireurs/recommandes?pisteSlug=...&limit=10 |
Non — seed = lyceenId (stable) | Non (carousel 10 EE) |
Critères
| # | Critère (côté lycéen) | Condition (côté EE) | Coef |
|---|---|---|---|
| 1 | isBoursierSecondaire ∈ {oui, ne-sait-pas} OU boostBudget ∈ {oui, unPeu} |
l'EE a au moins une sourceOfFinanceStudies dans {boursePrivee, crous} |
×1.5 |
| 2 | pisteSlug fourni en paramètre (vue "EE recommandés dans une piste") |
parcoursEclaireurs.pisteEtude = pisteSlug |
filtre dur |
| 3 | — | lastActivityAt >= now - 1M (connecté/actif dans le mois) |
×2 |
| 4 | — | unansweredConversationCount = N (messages non lus) |
×0.5ᴺ, cap N=4 |
| — | Randomisation seedée | — | ×[0.9,1.2] |
Non-cumul du critère #1 : un lycéen boursier et qui a répondu "oui" à la question budget exprime la même intention économique. On n'applique qu'une seule fois le ×1.5, sinon on double-compte le même signal et on déséquilibre vis-à-vis d'un lycéen qui n'en a coché qu'un.
Non-cumul sur sourceOfFinanceStudies : si un EE a à la fois crous et
boursePrivee, il reçoit ×1.5 une fois, pas ×2.25. On récompense le profil
(EE qui sait parler financement boursé), pas le nombre de cases cochées.
Malus de réactivité (#4) : unansweredConversationCount est le nombre de
conversations où l'EE n'a pas encore répondu au lycéen. Ce nombre N est
transformé en pénalité exponentielle ×0.5ᴺ, de façon à pénaliser davantage
les EE qui laissent plusieurs convs sans réponse. Exemples :
- N=0 : ×1.0 — pas de malus
- N=1 : ×0.5 — score ÷2
- N=2 : ×0.25 — score ÷4
- … jusqu'au cap N≥4 : ×0.0625 — score ÷16 (plancher)
Le cap à N=4 (0.5⁴ = 0.0625) empêche une exclusion de facto : un EE très
peu réactif descend en bas du classement mais reste visible, et peut remonter
dès qu'il répond.
Décisions Elasticsearch
score_mode: multiply et boost_mode: replace
score_mode: multiply→ les coefs desfunctionsse multiplient entre eux. Ça rend les effets indépendants combinables proprement.boost_mode: replace→ le score final = produit des fonctions uniquement (le score de la querybool filtervaut toujours 1, on le remplace plutôt que de le multiplier —multiplydonnerait le même résultat maisreplaceexprime mieux l'intention).
Randomisation maîtrisée
- Seed = lyceenId (pages piste, vue piste) → stable entre visites, différente entre lycéens. La pagination ne remontre pas d'EE déjà vus.
- Seed aléatoire (accueil) → chaque chargement de la home donne un ordre différent, ce qui évite que les mêmes EE soient toujours en tête du carousel.
weight: 1.2→ la randomisation module le score dans[0.9, 1.2]environ, insuffisant pour faire remonter un EE sans coef au-dessus d'un EE avec coef bourse (×1.5). La randomisation départage des égalités, elle ne bouscule pas les signaux forts.