ajoute la recherche plein texte dans les cours et labs (bloc C)

Elasticsearch interne (jamais exposé, sécurité x-pack désactivée,
cohérent avec le reste des services internes de ce serveur), tas JVM
fixé à 512 Mo plutôt que le calcul automatique (jusqu'à 50 % de la
RAM système, désastreux sur ce serveur à 31 Go réels). Appels HTTP
directs depuis Laravel, pas de bibliothèque cliente pour un besoin
aussi simple. Index reconstruit après chaque synchronisation du
catalogue, page de recherche commune aux profs et aux élèves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-14 13:24:13 +02:00
parent e549f03e1b
commit 777dd1cbc8
20 changed files with 545 additions and 0 deletions

View file

@ -0,0 +1,70 @@
# ADR 0009 — Elasticsearch interne, sécurité x-pack désactivée
- **Date** : 2026-10-14
- **Statut** : accepté
## Contexte
Bloc C du CLAUDE.md : « ElasticSearch : recherche plein texte dans les cours
et les labs (seulement si le besoin est réel). » Adam a confirmé le besoin
explicitement (« fonce »). Catalogue minuscule (quelques labs, un seul
`cours.md` aujourd'hui) : aucun besoin d'un cluster, d'un cache de
répertoires exotique ou d'une bibliothèque cliente PHP dédiée.
## Décisions
**Une instance Elasticsearch 8.19, locale, jamais exposée.** Installée
depuis le dépôt APT officiel Elastic (clé GPG vérifiée, même principe que
Ceph/ADR 0007). Consommée uniquement par le portail, en PHP, sur
`127.0.0.1:9200` — jamais par un navigateur.
**Sécurité x-pack désactivée (`xpack.security.enabled: false`).**
L'installation standard active TLS + authentification par défaut depuis la
8.x (mot de passe généré, certificats auto-signés) — pertinent pour un
cluster multi-nœuds exposé, disproportionné pour une instance mono-nœud qui
ne parle qu'à elle-même sur la boucle locale. Cohérent avec le reste de ce
serveur : l'API OpenStack interne (`10.10.0.1`) et le MariaDB/Redis dédiés
au portail (`127.0.0.1:3307`/`6380`) n'ont pas non plus de TLS entre
services — l'isolation vient du pare-feu (`policy drop` par défaut) et de
l'écoute en local, pas d'une authentification par service. Désactiver x-pack
évite aussi de devoir stocker et faire tourner un mot de passe Elasticsearch
supplémentaire pour rien.
**Tas JVM réduit à 512 Mo (`-Xms512m -Xmx512m`), pas le calcul automatique
d'Elasticsearch.** Par défaut, Elasticsearch vise jusqu'à 50 % de la RAM
système — sur ce serveur à 31 Go réels (voir ADR 0007), cela aurait pu
réserver ~15 Go à un service qui indexe quelques dizaines de documents.
Mesuré après coup : ~900 Mo de RSS au total pour le processus, RAM
disponible du serveur restée stable (~6 Go) pendant tout le chantier.
**Appels HTTP directs (`Illuminate\Support\Facades\Http`), pas le client
PHP officiel `elastic/elasticsearch`.** Le besoin se limite à indexer des
documents et faire une recherche plein texte avec surlignage — deux appels
REST simples. Ajouter une bibliothèque cliente complète (et ses
dépendances) pour ça aurait été une techno sans besoin réel (CLAUDE.md,
section 8).
**Analyseur `french` d'Elasticsearch sur les champs `titre`/`contenu`.**
Livré avec Elasticsearch, pas un plugin à installer : améliore réellement la
pertinence sur du contenu pédagogique en français (élision, racines des
mots) sans coût d'exploitation supplémentaire.
**Reconstruction complète de l'index à chaque synchronisation**
(`labs:synchroniser`, `cours:synchroniser`), pas une mise à jour
incrémentale. Volume du catalogue trop faible pour que ça coûte quoi que ce
soit ; plus simple et plus sûr (jamais de document orphelin si un lab est
retiré).
## Conséquences
- Recherche vérifiée en réel avec un compte élève jetable : les deux vrais
documents existants (labs catalogués + `08-deux-lan-routeur/cours.md`,
contenu réel) retrouvés par recherche plein texte, extrait surligné
affiché, compte supprimé ensuite.
- `App\Services\Recherche\IndexRecherche::disponible()` renvoie `false` si
`ELASTICSEARCH_HOTE` n'est pas renseigné : la page de recherche affiche
alors un message d'indisponibilité au lieu d'une erreur — même principe
que `StockageTravauxEleves::disponible()` pour le S3.
- En dette : pas de pagination des résultats (taille de catalogue bien en
dessous du besoin), pas de recherche à facettes (niveau, compétence) —
à ajouter si le catalogue grossit réellement.

View file

@ -40,3 +40,4 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
| Ceph : démo mono-serveur seulement (1 OSD sur fichier épars, pas de réplication, pas branché à Cinder/Glance) | `ceph_pool_default_size=1` et un seul OSD acceptés uniquement parce que c'est une démo de compréhension, sans donnée réelle. Le vrai cluster (plusieurs OSD, réplication, intégré à Cinder) attend le 2ᵉ serveur du CLAUDE.md. Voir [ADR 0007](adr/0007-ceph-demo-mono-serveur.md). | 2ᵉ serveur (bloc C) | 2026-10-14 |
| Ceph : bootstrap fait à la main, pas encore rejouable par Ansible | Comme Kolla à l'étape 2 : acceptable pour une démo, à automatiser si ce cluster doit durer. Le fichier systemd du loop device est suivi dans `infra/ceph/` mais déployé à la main. | Si la démo devient durable | 2026-10-14 |
| WebDAV de l'élève : stockage local, pas S3 ; pas de quota par élève | Dossier local (`storage/app/webdav/`), partage le disque serré avec OpenStack et le portail — pas de limite de taille par élève. rclone sait servir un backend S3 sans changement d'architecture (voir [ADR 0008](adr/0008-webdav-eleve-auth-request.md)) : à basculer en même temps que le bucket S3 des devoirs (`docs/runbooks/stockage-s3.md`), et à ajouter un quota si l'usage réel le demande. | Quand le bucket S3 existe / si l'usage le demande | 2026-10-14 |
| Recherche (Elasticsearch) : pas de pagination ni de facettes | Catalogue minuscule aujourd'hui (quelques labs, un seul vrai `cours.md`) : inutile tant que ça reste le cas. Voir [ADR 0009](adr/0009-elasticsearch-interne-sans-securite.md). | Si le catalogue grossit réellement | 2026-10-14 |

View file

@ -1251,3 +1251,26 @@ de haut en bas — une bonne question à savoir expliquer en entretien SRE.
**Suite**
Prochain chantier du bloc C : ElasticSearch.
## 2026-10-14 — Recherche plein texte, Elasticsearch (bloc C)
Troisième chantier du bloc C : recherche plein texte dans les cours et les
labs. Détail des choix : [ADR 0009](adr/0009-elasticsearch-interne-sans-securite.md).
**Fait**
- Elasticsearch 8.19 installé depuis le dépôt officiel (clé GPG vérifiée), instance strictement interne : écoute `127.0.0.1` seulement, sécurité x-pack désactivée (cohérent avec le reste du serveur — l'API OpenStack interne et le MariaDB/Redis du portail n'ont pas de TLS entre services non plus, l'isolation vient du pare-feu), tas JVM limité à 512 Mo (le calcul automatique d'Elasticsearch, jusqu'à 50 % de la RAM système, aurait pu réserver ~15 Go sur ce serveur à 31 Go réels).
- `App\Services\Recherche\IndexRecherche` : appels HTTP directs (pas de bibliothèque cliente, le besoin se limite à indexer et chercher), analyseur `french` d'Elasticsearch sur les champs texte, `disponible()` même principe que le stockage S3 (la page affiche juste « pas disponible » tant que ce n'est pas configuré, jamais une erreur).
- Index reconstruit automatiquement après `labs:synchroniser` et `cours:synchroniser` — pas de mise à jour incrémentale, le catalogue est trop petit pour que ça coûte quoi que ce soit.
- Page « Recherche » (menu du compte, profs et élèves), route commune aux deux guards (`auth:eleve,web`, déjà utilisée par « Mes cours »/« Paramètres »).
- 5 tests Pest (`Http::fake()`, jamais contre un vrai Elasticsearch) : indisponibilité propre, accès par les deux guards, surlignage affiché avec le bon lien vers la page du cours, reconstruction de l'index envoie bien un document par lab/cours, et n'envoie *aucun* appel réseau quand Elasticsearch n'est pas configuré.
**Vérifié en réel** : index reconstruit sur le serveur depuis les vraies données (2 documents : le lab 08 et son vrai `cours.md`), recherche du mot « routage » retrouve les deux avec le bon score, page `/recherche` testée avec un compte élève jetable (connexion réelle par cookies de session, pas juste un appel direct au contrôleur) — le mot recherché apparaît bien surligné (`<em>routage</em>`) et le lien vers le cours fonctionne. RAM disponible du serveur restée stable (~6 Go) pendant tout le chantier. Compte de test supprimé ensuite.
**Comprendre**
Le réflexe acquis avec Ceph et WebDAV (vérifier l'état réel avant d'agir, mesurer plutôt que deviner) a évité ici une vraie bêtise : Elasticsearch calcule par défaut un tas JVM jusqu'à 50 % de la RAM système, ce qui aurait été catastrophique sur ce serveur sans même un message d'erreur — juste un service qui prend toute la RAM disponible. Fixer explicitement le tas avant le premier démarrage, plutôt qu'après avoir constaté le problème, est le genre de précaution qui ne coûte rien et qui évite un incident.
**Suite**
Bloc C restant : WebDAV et Ceph faits, Elasticsearch fait. ElasticSearch avait été écarté du MCP développeurs par Adam (pas pour l'instant). Prochains chantiers confirmés par Adam : LTI Moodle 1.3, puis SSO ENT (Keycloak) — normalement des étapes V2/V3 du produit, avancées à la demande explicite d'Adam.

View file

@ -0,0 +1,64 @@
# Runbook — Elasticsearch (recherche dans les cours et labs)
Instance interne, jamais exposée, consommée seulement par le portail en PHP
(`App\Services\Recherche\IndexRecherche`). Détail des choix :
[ADR 0009](../adr/0009-elasticsearch-interne-sans-securite.md).
## Où sont les choses
| Élément | Emplacement |
|---|---|
| Config | `/etc/elasticsearch/elasticsearch.yml` (sécurité x-pack désactivée, écoute `127.0.0.1` seulement) |
| Tas JVM | `/etc/elasticsearch/jvm.options.d/heap.options` (512 Mo, voir ADR 0009) |
| Index | `paillasse_catalogue` |
| Déployé par | `ansible/elasticsearch.yml` (rôle `elasticsearch`, rejoue aussi `portail` pour écrire `.env`) |
```
cd ansible && script -q /dev/null ansible-playbook elasticsearch.yml -i inventory.ini
```
## Vérifier que ça fonctionne
```
systemctl is-active elasticsearch
curl -s http://127.0.0.1:9200/paillasse_catalogue/_count
```
Reconstruire l'index manuellement (après un changement de mapping, ou si le
catalogue semble désynchronisé) :
```
cd /var/www/portail
sudo -u www-data HOME=/tmp php8.3 artisan recherche:indexer
```
Autrement, l'index se reconstruit automatiquement après `labs:synchroniser`
et `cours:synchroniser` (voir ces deux commandes).
## Pourquoi la RAM est surveillée
Comme pour Ceph et WebDAV (voir les runbooks correspondants) : ce serveur a
réellement 31 Go de RAM, partagés avec OpenStack et le portail. Le tas JVM
est volontairement limité à 512 Mo (~900 Mo de RSS au total mesuré) — ne
jamais retirer `heap.options` ni laisser Elasticsearch calculer sa taille de
tas automatiquement sur ce serveur.
## Si le besoin grossit réellement
Pas de pagination ni de recherche à facettes pour l'instant (catalogue
minuscule). À ajouter dans `App\Services\Recherche\IndexRecherche` et
`RechercheController` si le catalogue de cours/labs grossit vraiment —
jamais avant, pour ne pas construire une fonction sans usage réel.
## Nettoyer entièrement (si besoin)
```
ssh paillasse
sudo systemctl disable --now elasticsearch
sudo rm -rf /var/lib/elasticsearch /etc/elasticsearch
sudo apt-get purge -y elasticsearch
sudo rm /etc/apt/sources.list.d/elastic-8.x.list /usr/share/keyrings/elastic.gpg
sudo apt-get update
```
Puis retirer `ELASTICSEARCH_HOTE`/`ELASTICSEARCH_PORT` du `.env` du portail
et redémarrer Horizon.