185 lines
6.4 KiB
Markdown
185 lines
6.4 KiB
Markdown
# Architecture du cache — Moteur Folio
|
|
|
|
## Contexte et problème initial
|
|
|
|
Le moteur stocke chaque article dans un sous-répertoire `data/{uuid}/` contenant deux fichiers :
|
|
- `meta.json` — métadonnées (titre, slug, catégorie, cover, dates…)
|
|
- `index.md` — contenu Markdown
|
|
|
|
Avec 1 000+ articles, chaque vue de page déclenchait **3 appels à `getAll()`** (via `getBySlug()`, `getCategories()` et directement pour les articles liés), ce qui représentait ~6 000 lectures de fichiers par requête. La page mettait **+5 secondes** à charger.
|
|
|
|
---
|
|
|
|
## Les quatre niveaux de cache
|
|
|
|
### 1. Cache mémoire de requête — `$allCache` et `$searchIndexCache`
|
|
|
|
**Scope** : durée de vie d'une requête PHP (in-process).
|
|
|
|
`ArticleManager` mémoïse deux tableaux en propriétés privées :
|
|
|
|
- `$allCache` — résultat de `loadAll()` (tous les articles avec contenu)
|
|
- `$searchIndexCache` — contenu de `search_index.json`
|
|
|
|
```php
|
|
// Premier appel : scan disque + construction du tableau
|
|
$this->allCache = $this->loadAll();
|
|
|
|
// Appels suivants dans la même requête : tableau déjà en mémoire
|
|
return $this->allCache;
|
|
```
|
|
|
|
**Invalidation** : `writeMeta()` et `delete()` mettent les deux propriétés à `null`.
|
|
|
|
---
|
|
|
|
### 2. Cache disque par article — `_cache/articles/{uuid}.json`
|
|
|
|
**Scope** : persistant entre les requêtes, jusqu'à modification de l'article.
|
|
|
|
`loadArticle()` vérifie si le cache est plus récent que `meta.json` avant de lire les sources :
|
|
|
|
```
|
|
_cache/articles/{uuid}.json <-- filemtime >= meta.json ? → utiliser le cache
|
|
→ lire meta.json + index.md, écrire le cache
|
|
```
|
|
|
|
Le fichier cache contient toutes les données de l'article (métadonnées + contenu), ce qui réduit les lectures de **2 fichiers à 1** par article chargé.
|
|
|
|
**Invalidation** : `writeMeta()` supprime `_cache/articles/{uuid}.json` avant d'écrire le nouveau `meta.json`. `delete()` le supprime aussi.
|
|
|
|
---
|
|
|
|
### 3. Index slug → UUID — `_cache/slug_index.json`
|
|
|
|
**Scope** : persistant, mis à jour incrémentalement.
|
|
|
|
Permet à `getBySlug()` de trouver un article en **O(1)** (lecture de l'index + lecture du cache article) au lieu de parcourir tous les articles.
|
|
|
|
```
|
|
slug_index.json : {"mon-article": "uuid-xxxx", "autre-article": "uuid-yyyy", ...}
|
|
```
|
|
|
|
**Construction** : à la première utilisation, `buildSlugIndex()` lit le `search_index.json` (un seul fichier) pour construire la correspondance. Si le search_index n'existe pas encore, il tombe en repli sur `loadAll()`.
|
|
|
|
**Invalidation** : `writeMeta()` supprime le fichier (reconstruction automatique à la prochaine requête). `delete()` fait de même.
|
|
|
|
> Suppression plutôt que mise à jour incrémentale : la reconstruction depuis `search_index.json` est quasi instantanée (lecture d'un seul fichier JSON), donc il n'y a pas d'intérêt à maintenir des mises à jour partielles.
|
|
|
|
---
|
|
|
|
### 4. Index de recherche — `search_index.json`
|
|
|
|
**Scope** : persistant, reconstruit après chaque modification d'article.
|
|
|
|
Fichier JSON plat contenant un tableau de tous les articles avec leurs champs essentiels et leur texte brut pré-calculé (`plain`, sans syntaxe Markdown). Utilisé pour :
|
|
|
|
- La recherche plein-texte (`SearchEngine`)
|
|
- La liste des articles publiés pour les articles liés/similaires (évite `getAll()`)
|
|
- Les catégories (`getCategories()`)
|
|
- La construction du slug index
|
|
|
|
**Champs stockés** :
|
|
|
|
```json
|
|
{
|
|
"uuid": "...",
|
|
"slug": "...",
|
|
"title": "...",
|
|
"category": "...",
|
|
"author": "...",
|
|
"cover": "...",
|
|
"published": true,
|
|
"published_at": "2026-01-15 10:00:00",
|
|
"created_at": "2026-01-14 09:30:00",
|
|
"updated_at": "2026-01-15 10:00:00",
|
|
"plain": "texte brut de l'article sans markdown..."
|
|
}
|
|
```
|
|
|
|
**Rebuild automatique** : si le fichier ne contient pas le champ `cover` (format antérieur à la v2 du cache), `getSearchIndex()` déclenche automatiquement un rebuild.
|
|
|
|
**Invalidation** : `rebuildSearchIndex()` (appelé par `create()`, `update()`, `delete()`).
|
|
|
|
---
|
|
|
|
## Chemin d'une requête de vue d'article après optimisation
|
|
|
|
```
|
|
GET /post/{slug}
|
|
│
|
|
├── getBySlug(slug)
|
|
│ ├── Lire slug_index.json [1 lecture]
|
|
│ ├── → UUID trouvé
|
|
│ └── getByUuid(uuid)
|
|
│ └── Lire _cache/articles/{uuid}.json [1 lecture]
|
|
│
|
|
├── getCategories()
|
|
│ └── getSearchIndex() [lecture de search_index.json, mise en cache mémoire]
|
|
│
|
|
├── $_allPublished (articles liés + similaires)
|
|
│ └── getSearchIndex() [déjà en cache mémoire → 0 lecture]
|
|
│
|
|
├── scorePool(mots_du_titre, $_allPublished)
|
|
│ └── Tokenisation unique par article, pas de re-calcul par mot
|
|
│
|
|
└── getBacklinks(slug)
|
|
└── Lire _cache/backlinks.json [1 lecture]
|
|
|
|
Total : ~4 lectures de fichiers, indépendamment du nombre total d'articles.
|
|
```
|
|
|
|
Avant optimisation (1 062 articles) : ~6 300 lectures de fichiers.
|
|
|
|
---
|
|
|
|
## Performances mesurées
|
|
|
|
| Scénario | Avant | Après |
|
|
|---|---|---|
|
|
| Cold cache (aucun cache disque) | +5 s | ~0,6 s |
|
|
| Warm cache (cache disque présent) | +5 s | ~0,4 s |
|
|
|
|
---
|
|
|
|
## Scalabilité
|
|
|
|
| Volume d'articles | Lectures de fichiers par vue (après) |
|
|
|---|---|
|
|
| 1 000 | ~4 |
|
|
| 100 000 | ~4 |
|
|
| 500 000 | ~4 |
|
|
|
|
Le nombre de lectures est **constant** : le chemin de vue ne dépend plus du nombre total d'articles, seulement de la présence des fichiers de cache.
|
|
|
|
La seule opération encore en O(N) est `rebuildSearchIndex()`, mais elle n'est déclenchée que sur écriture (création, modification, suppression d'article), jamais sur lecture.
|
|
|
|
---
|
|
|
|
## Invalidation — résumé
|
|
|
|
| Événement | Caches invalidés |
|
|
|---|---|
|
|
| `writeMeta()` (toute écriture d'article) | `$allCache`, `$searchIndexCache`, cache article (`{uuid}.json`), slug index |
|
|
| `delete()` | idem + suppression physique du cache article |
|
|
| `rebuildSearchIndex()` | `$searchIndexCache` (remplacé par les nouvelles données) |
|
|
|
|
---
|
|
|
|
## Maintenance
|
|
|
|
### Vider manuellement les caches disque
|
|
|
|
En cas de besoin (migration, incohérence) :
|
|
|
|
```bash
|
|
ssh varlog "sudo rm -rf /var/www/lan.acegrp.varlog/data/_cache/articles/"
|
|
ssh varlog "sudo rm /var/www/lan.acegrp.varlog/data/_cache/slug_index.json"
|
|
```
|
|
|
|
Les caches se reconstruisent automatiquement à la première requête suivante.
|
|
|
|
### Forcer un rebuild du search_index
|
|
|
|
Modifier et sauvegarder n'importe quel article depuis l'interface admin déclenche un rebuild complet. Il n'existe pas de commande CLI dédiée pour l'instant.
|