Admin searches : trier par visiteurs uniques plutôt que par hits #41

Closed
opened 2026-05-13 22:09:58 +00:00 by cedricAbonnel · 0 comments
Owner

Problème

L'onglet /admin/searches trie les termes recherchés par nombre d'occurrences brutes (hits). Si le même visiteur tape le même terme 10 fois, ça compte 10. La colonne affiche d'ailleurs « Fois ».

Le tri par visiteurs uniques (IPs distinctes) est plus représentatif de l'intérêt réel pour un terme.


Analyse technique

Format des logs Apache

192.168.100.95 - - [13/May/2026:00:56:28 +0200] "GET /search?q=home+assistant HTTP/1.1" 200 25843 ...

L'IP est le premier champ avant le premier espace — facilement extractible.

Code actuel (src/SearchLogParser.php)

La méthode parseLine() (ligne 109) incrémente un compteur brut :

private function parseLine(string $line, array &$counts): void
{
    // ...
    $counts[$q] = ($counts[$q] ?? 0) + 1;  // ← hits bruts
}

La signature de topTerms() retourne array<string,int> (terme → count), inchangée.

Format du cache

Le cache JSON (_cache/search_terms.json) stocke {"terme": count}. Le format reste identique — seule la sémantique de count change (visiteurs uniques au lieu de hits).

Note : le cache doit être invalidé/supprimé manuellement après le déploiement pour recalculer avec la nouvelle logique.


Implémentation

1. src/SearchLogParser.phpparseLine()

Changer la signature du tableau accumulateur de &$counts à &$visitors et extraire l'IP :

private function parseLine(string $line, array &$visitors): void
{
    if (!str_contains($line, 'GET /search?')) {
        return;
    }
    if (!preg_match('/^(\S+) \S+ \S+ \[[^\]]+\] "GET \/search\?([^"]*) HTTP\//', $line, $m)) {
        return;
    }

    $ip = $m[1];
    parse_str($m[2], $params);
    $q = trim(urldecode($params['q'] ?? ''));

    if ($q === '' || mb_strlen($q) > 200) {
        return;
    }
    $q = mb_strtolower($q);
    $visitors[$q][$ip] = true;  // set d'IPs par terme
}

2. src/SearchLogParser.phptopTerms() et parseFile()

Après le parsing, convertir le set d'IPs en comptage :

public function topTerms(int $limit = 100): array
{
    // ... (cache check inchangé)

    $visitors = [];  // terme => [ip => true]
    foreach ($this->logFiles() as $file) {
        $this->parseFile($file, $visitors);
    }

    // Convertir en counts
    $counts = [];
    foreach ($visitors as $term => $ips) {
        $counts[$term] = count($ips);
    }
    arsort($counts);

    // Cache et retour inchangés
    file_put_contents($this->cacheFile, json_encode($counts, JSON_UNESCAPED_UNICODE));
    return array_slice($counts, 0, $limit, true);
}

parseFile() doit passer $visitors au lieu de $counts — renommer le paramètre &$counts en &$visitors.

3. templates/admin.php — colonne

Dans l'onglet searches (autour de la ligne avec <th>Fois</th>) :

// Avant
<th style="width:6rem" class="text-end">Fois</th>

// Après
<th style="width:6rem" class="text-end">Visiteurs</th>

4. Invalidation du cache

Après déploiement, supprimer le cache existant :

rm /var/www/lan.acegrp.varlog/data/_cache/search_terms.json

Ou ajouter un bouton admin « Vider le cache » (à faire dans un ticket séparé si besoin).


Considérations

Mémoire : en parcourant 14 jours de logs, le tableau $visitors peut contenir beaucoup d'IPs uniques par terme. En pratique, sur un blog personnel, ça reste négligeable (< 1 Mo en mémoire).

Vie privée : les IPs ne sont jamais persistées — elles servent uniquement à la déduplication en mémoire pendant le calcul. Le cache JSON ne stocke que des entiers.

Même terme, même visiteur, jours différents : les IPs ne sont pas dédupliquées par jour, mais globalement sur les 14 jours. C'est acceptable pour un blog personnel.


Fichiers concernés

  • src/SearchLogParser.phpparseLine(), parseFile(), topTerms() (logique centrale)
  • templates/admin.php — en-tête de colonne « Fois » → « Visiteurs »

Migré depuis varlog#56

## Problème L'onglet `/admin/searches` trie les termes recherchés par **nombre d'occurrences brutes** (hits). Si le même visiteur tape le même terme 10 fois, ça compte 10. La colonne affiche d'ailleurs « Fois ». Le tri par **visiteurs uniques** (IPs distinctes) est plus représentatif de l'intérêt réel pour un terme. --- ## Analyse technique ### Format des logs Apache ``` 192.168.100.95 - - [13/May/2026:00:56:28 +0200] "GET /search?q=home+assistant HTTP/1.1" 200 25843 ... ``` L'IP est le premier champ avant le premier espace — facilement extractible. ### Code actuel (`src/SearchLogParser.php`) La méthode `parseLine()` (ligne 109) incrémente un compteur brut : ```php private function parseLine(string $line, array &$counts): void { // ... $counts[$q] = ($counts[$q] ?? 0) + 1; // ← hits bruts } ``` La signature de `topTerms()` retourne `array<string,int>` (terme → count), inchangée. ### Format du cache Le cache JSON (`_cache/search_terms.json`) stocke `{"terme": count}`. Le format reste identique — seule la sémantique de `count` change (visiteurs uniques au lieu de hits). **Note** : le cache doit être invalidé/supprimé manuellement après le déploiement pour recalculer avec la nouvelle logique. --- ## Implémentation ### 1. `src/SearchLogParser.php` — `parseLine()` Changer la signature du tableau accumulateur de `&$counts` à `&$visitors` et extraire l'IP : ```php private function parseLine(string $line, array &$visitors): void { if (!str_contains($line, 'GET /search?')) { return; } if (!preg_match('/^(\S+) \S+ \S+ \[[^\]]+\] "GET \/search\?([^"]*) HTTP\//', $line, $m)) { return; } $ip = $m[1]; parse_str($m[2], $params); $q = trim(urldecode($params['q'] ?? '')); if ($q === '' || mb_strlen($q) > 200) { return; } $q = mb_strtolower($q); $visitors[$q][$ip] = true; // set d'IPs par terme } ``` ### 2. `src/SearchLogParser.php` — `topTerms()` et `parseFile()` Après le parsing, convertir le set d'IPs en comptage : ```php public function topTerms(int $limit = 100): array { // ... (cache check inchangé) $visitors = []; // terme => [ip => true] foreach ($this->logFiles() as $file) { $this->parseFile($file, $visitors); } // Convertir en counts $counts = []; foreach ($visitors as $term => $ips) { $counts[$term] = count($ips); } arsort($counts); // Cache et retour inchangés file_put_contents($this->cacheFile, json_encode($counts, JSON_UNESCAPED_UNICODE)); return array_slice($counts, 0, $limit, true); } ``` `parseFile()` doit passer `$visitors` au lieu de `$counts` — renommer le paramètre `&$counts` en `&$visitors`. ### 3. `templates/admin.php` — colonne Dans l'onglet searches (autour de la ligne avec `<th>Fois</th>`) : ```php // Avant <th style="width:6rem" class="text-end">Fois</th> // Après <th style="width:6rem" class="text-end">Visiteurs</th> ``` ### 4. Invalidation du cache Après déploiement, supprimer le cache existant : ```bash rm /var/www/lan.acegrp.varlog/data/_cache/search_terms.json ``` Ou ajouter un bouton admin « Vider le cache » (à faire dans un ticket séparé si besoin). --- ## Considérations **Mémoire** : en parcourant 14 jours de logs, le tableau `$visitors` peut contenir beaucoup d'IPs uniques par terme. En pratique, sur un blog personnel, ça reste négligeable (< 1 Mo en mémoire). **Vie privée** : les IPs ne sont jamais persistées — elles servent uniquement à la déduplication en mémoire pendant le calcul. Le cache JSON ne stocke que des entiers. **Même terme, même visiteur, jours différents** : les IPs ne sont pas dédupliquées par jour, mais globalement sur les 14 jours. C'est acceptable pour un blog personnel. --- ## Fichiers concernés - `src/SearchLogParser.php` — `parseLine()`, `parseFile()`, `topTerms()` (logique centrale) - `templates/admin.php` — en-tête de colonne « Fois » → « Visiteurs » --- *Migré depuis [varlog#56](https://git.abonnel.fr/cedricAbonnel/varlog/issues/56)*
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: cedricAbonnel/folio#41