PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / matching / NearMissRecorder.php

NearMissRecorder.php in 404 Solution trunk, at includes/matching/NearMissRecorder.php

101 lines 4.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Carries the best rejected match from the matching engines to the captured-404
9 * insert, within one request.
10 *
11 * The producer and the consumer sit five frames apart -- SpellChecker rejects a
12 * candidate deep inside a Levenshtein scan, and NotFoundResponseService writes
13 * the captured row after the whole engine run has already returned null -- and
14 * every layer in between (MatchingEngine::match(), the orchestrator, the auto
15 * redirect handler, the frontend pipeline) has no use for the value. Threading
16 * it through them would put a field nobody reads into four signatures and into
17 * the public engine interface; a single recorder both sides resolve from the
18 * container keeps the contract at the two ends that care, and lets any future
19 * engine record a near miss without an interface change.
20 *
21 * Two admission rules make a wrong score impossible rather than unlikely:
22 *
23 * 1. Records are keyed by the requested URL and read back by the same key.
24 * A lookup for a different URL returns null, so a leftover record can
25 * never be attributed to the wrong captured row -- the failure mode is
26 * "no score", which is exactly the behaviour that predates this class.
27 * 2. Only a score inside the (0, 100] band is kept. The spelling score is
28 * 100 - (distance / basis * 100), which goes negative for a candidate far
29 * longer than the request; a negative or absurd confidence is not a
30 * confidence, and NULL says so honestly.
31 *
32 * Only the single best (highest) near miss per URL is kept: that is the closest
33 * the plugin came to redirecting, which is the number the admin needs.
34 */
35 class ABJ_404_Solution_NearMissRecorder {
36
37 /** Scores must be above this to be recorded (exclusive). */
38 const MIN_RECORDABLE_SCORE = 0.0;
39
40 /** Scores must be at or below this to be recorded (inclusive). */
41 const MAX_RECORDABLE_SCORE = 100.0;
42
43 /** @var string|null The URL the current record belongs to. */
44 private $requestedURL = null;
45
46 /** @var ABJ_404_Solution_NearMissMatch|null Best near miss for that URL. */
47 private $best = null;
48
49 /**
50 * Record a match an engine found and then rejected for scoring under its
51 * threshold. Out-of-band scores are dropped, so every engine can call this
52 * unconditionally on its reject branch without vetting the number first.
53 *
54 * @param array{requestedURL: string, score: float, engineName: string} $match
55 * requestedURL is the URL being resolved, as the frontend
56 * pipeline spells it (MatchRequest::getRequestedURL(), which is the
57 * same string NotFoundResponseService::sendTo404Page() receives).
58 * @return void
59 */
60 public function record(array $match): void {
61 $requestedURL = $match['requestedURL'];
62 $score = $match['score'];
63 $engineName = $match['engineName'];
64 if (!self::isRecordableScore($score)) {
65 return;
66 }
67
68 if ($this->requestedURL !== $requestedURL) {
69 $this->requestedURL = $requestedURL;
70 $this->best = null;
71 }
72
73 if ($this->best === null || $score > $this->best->getScore()) {
74 $this->best = ABJ_404_Solution_NearMissMatch::create($score, $engineName);
75 }
76 }
77
78 /**
79 * The best near miss recorded for this exact URL, or null when none was
80 * recorded for it (automatic matching off, no candidate at all, or a
81 * record that belongs to a different URL).
82 *
83 * @param string $requestedURL
84 * @return ABJ_404_Solution_NearMissMatch|null
85 */
86 public function getBestFor(string $requestedURL): ?ABJ_404_Solution_NearMissMatch {
87 if ($this->requestedURL !== $requestedURL) {
88 return null;
89 }
90 return $this->best;
91 }
92
93 /**
94 * @param float $score
95 * @return bool
96 */
97 public static function isRecordableScore(float $score): bool {
98 return $score > self::MIN_RECORDABLE_SCORE && $score <= self::MAX_RECORDABLE_SCORE;
99 }
100 }
101