| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* N-gram filter usage telemetry. |
| 9 |
* |
| 10 |
* Records per-query counters (total queries, entries examined, candidates |
| 11 |
* returned, total duration in ms, rolling avg reduction) into a single |
| 12 |
* WordPress option. Self-resets monthly to bound option size. Read path |
| 13 |
* derives avg-per-query stats from the same option. |
| 14 |
* |
| 15 |
* Used by NGramFilter::findSimilarPages() (write) and the admin diagnostics |
| 16 |
* UI (read via getUsageStats). |
| 17 |
*/ |
| 18 |
class ABJ_404_Solution_NGramUsageTelemetry { |
| 19 |
|
| 20 |
const OPTION_KEY = 'abj404_ngram_usage_stats'; |
| 21 |
|
| 22 |
/** Reset stats after 30 days to avoid unbounded growth */ |
| 23 |
const RESET_INTERVAL_SECONDS = 2592000; // 30 * 24 * 60 * 60 |
| 24 |
|
| 25 |
/** |
| 26 |
* Track a single findSimilarPages invocation. |
| 27 |
* |
| 28 |
* @param int $totalInCache Total entries in cache |
| 29 |
* @param int $examined Number of entries examined |
| 30 |
* @param int $candidates Number of candidates returned |
| 31 |
* @param float $duration Time in milliseconds |
| 32 |
* @return void |
| 33 |
*/ |
| 34 |
public function trackNGramUsage($totalInCache, $examined, $candidates, $duration) { |
| 35 |
$defaultStats = [ |
| 36 |
'total_queries' => 0, |
| 37 |
'total_entries_examined' => 0, |
| 38 |
'total_candidates_returned' => 0, |
| 39 |
'total_duration_ms' => 0, |
| 40 |
'avg_reduction_percent' => 0, |
| 41 |
'last_reset' => abj_clock()->now(), |
| 42 |
]; |
| 43 |
$statsRaw = get_option(self::OPTION_KEY, $defaultStats); |
| 44 |
/** @var array<string, mixed> $stats */ |
| 45 |
$stats = is_array($statsRaw) ? $statsRaw : $defaultStats; |
| 46 |
|
| 47 |
$stats['total_queries'] = (isset($stats['total_queries']) && is_numeric($stats['total_queries'])) ? (int)$stats['total_queries'] + 1 : 1; |
| 48 |
$stats['total_entries_examined'] = (isset($stats['total_entries_examined']) && is_numeric($stats['total_entries_examined'])) ? (int)$stats['total_entries_examined'] + $examined : $examined; |
| 49 |
$stats['total_candidates_returned'] = (isset($stats['total_candidates_returned']) && is_numeric($stats['total_candidates_returned'])) ? (int)$stats['total_candidates_returned'] + $candidates : $candidates; |
| 50 |
$stats['total_duration_ms'] = (isset($stats['total_duration_ms']) && is_numeric($stats['total_duration_ms'])) ? (float)$stats['total_duration_ms'] + $duration : $duration; |
| 51 |
|
| 52 |
if ($totalInCache > 0) { |
| 53 |
$reductionPercent = (($totalInCache - $examined) / $totalInCache) * 100; |
| 54 |
$prevAvgReduction = (isset($stats['avg_reduction_percent']) && is_numeric($stats['avg_reduction_percent'])) ? (float)$stats['avg_reduction_percent'] : 0; |
| 55 |
$totalQueries = (int)$stats['total_queries']; |
| 56 |
$stats['avg_reduction_percent'] = (($prevAvgReduction * ($totalQueries - 1)) + $reductionPercent) / $totalQueries; |
| 57 |
} |
| 58 |
|
| 59 |
$monthAgo = abj_clock()->now() - self::RESET_INTERVAL_SECONDS; |
| 60 |
$lastReset = (isset($stats['last_reset']) && is_numeric($stats['last_reset'])) ? (int)$stats['last_reset'] : 0; |
| 61 |
if ($lastReset < $monthAgo) { |
| 62 |
$stats = [ |
| 63 |
'total_queries' => 1, |
| 64 |
'total_entries_examined' => $examined, |
| 65 |
'total_candidates_returned' => $candidates, |
| 66 |
'total_duration_ms' => $duration, |
| 67 |
'avg_reduction_percent' => ($totalInCache > 0) ? (($totalInCache - $examined) / $totalInCache) * 100 : 0, |
| 68 |
'last_reset' => abj_clock()->now(), |
| 69 |
]; |
| 70 |
} |
| 71 |
|
| 72 |
update_option(self::OPTION_KEY, $stats); |
| 73 |
} |
| 74 |
|
| 75 |
/** |
| 76 |
* Get usage statistics with derived per-query averages. |
| 77 |
* |
| 78 |
* @return array<string, mixed> |
| 79 |
*/ |
| 80 |
public function getUsageStats() { |
| 81 |
$defaultStats = [ |
| 82 |
'total_queries' => 0, |
| 83 |
'total_entries_examined' => 0, |
| 84 |
'total_candidates_returned' => 0, |
| 85 |
'total_duration_ms' => 0, |
| 86 |
'avg_reduction_percent' => 0, |
| 87 |
'last_reset' => abj_clock()->now(), |
| 88 |
]; |
| 89 |
$statsRaw = get_option(self::OPTION_KEY, $defaultStats); |
| 90 |
/** @var array<string, mixed> $stats */ |
| 91 |
$stats = is_array($statsRaw) ? $statsRaw : $defaultStats; |
| 92 |
|
| 93 |
$totalQueries = (isset($stats['total_queries']) && is_numeric($stats['total_queries'])) ? (int)$stats['total_queries'] : 0; |
| 94 |
$totalExamined = (isset($stats['total_entries_examined']) && is_numeric($stats['total_entries_examined'])) ? (float)$stats['total_entries_examined'] : 0; |
| 95 |
$totalCandidates = (isset($stats['total_candidates_returned']) && is_numeric($stats['total_candidates_returned'])) ? (float)$stats['total_candidates_returned'] : 0; |
| 96 |
$totalDuration = (isset($stats['total_duration_ms']) && is_numeric($stats['total_duration_ms'])) ? (float)$stats['total_duration_ms'] : 0; |
| 97 |
|
| 98 |
if ($totalQueries > 0) { |
| 99 |
$stats['avg_examined_per_query'] = round($totalExamined / $totalQueries, 1); |
| 100 |
$stats['avg_candidates_per_query'] = round($totalCandidates / $totalQueries, 1); |
| 101 |
$stats['avg_duration_ms'] = round($totalDuration / $totalQueries, 2); |
| 102 |
} else { |
| 103 |
$stats['avg_examined_per_query'] = 0; |
| 104 |
$stats['avg_candidates_per_query'] = 0; |
| 105 |
$stats['avg_duration_ms'] = 0; |
| 106 |
} |
| 107 |
|
| 108 |
return $stats; |
| 109 |
} |
| 110 |
} |
| 111 |
|