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 / ngram / TermCandidateSource.php

TermCandidateSource.php in 404 Solution trunk, at includes/ngram/TermCandidateSource.php

133 lines 4.8 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 * Sources candidate taxonomy-term rows (categories or tags) for a 404 match.
9 *
10 * Single responsibility: decide HOW to obtain the candidate term rows for a
11 * given type and 404 query, then return them in the uniform term-row shape
12 * (objects with term_id, name, slug, taxonomy, url).
13 *
14 * - When the term n-gram cache for the type is trustworthy (coverage policy
15 * ready), route through the bounded n-gram prefilter and load only the
16 * matching term ids via PublishedTermsProvider. This keeps steady-state work
17 * bounded to a small candidate set instead of scanning every published term.
18 * - On a cold or low-coverage cache, fall back to the full
19 * getPublishedCategories()/getPublishedTags() scan (historical behavior).
20 *
21 * Extracted so both CategoryTagMatchingEngine and SpellSuggestionScorer share
22 * one implementation of this bounded-vs-fallback policy rather than duplicating
23 * it. The collaborators are optional; when any is absent (legacy/unit
24 * construction) the source always uses the full-taxonomy scan.
25 */
26 class ABJ_404_Solution_TermCandidateSource {
27
28 /** N-gram cache type for category archives. */
29 const TYPE_CATEGORY = 'category';
30
31 /** N-gram cache type for tag archives. */
32 const TYPE_TAG = 'tag';
33
34 /** Dice threshold for the term n-gram prefilter. Matches the post path's
35 * SpellNGramPrefilter::NGRAM_PREFILTER_THRESHOLD: terms and posts share the
36 * same URL-shaped n-gram cache, so the same 0.3 recall floor applies. */
37 const NGRAM_PREFILTER_THRESHOLD = 0.3;
38
39 /** Max bounded term candidates to pull from the prefilter. Matches the post
40 * path's NGRAM_PREFILTER_MAX_CANDIDATES; downstream scoring narrows further. */
41 const NGRAM_PREFILTER_MAX_CANDIDATES = 500;
42
43 /** @var mixed Object exposing getPublishedCategories()/getPublishedTags() (the full-scan fallback). */
44 private $contentRepo;
45
46 /** @var ABJ_404_Solution_NGramFilter|null */
47 private $ngramFilter;
48
49 /** @var ABJ_404_Solution_PublishedTermsProvider|null */
50 private $publishedTermsProvider;
51
52 /** @var ABJ_404_Solution_TermNGramCoveragePolicy|null */
53 private $termCoveragePolicy;
54
55 /**
56 * @param mixed $contentRepo Object exposing getPublishedCategories()/getPublishedTags().
57 * @param ABJ_404_Solution_NGramFilter|null $ngramFilter
58 * @param ABJ_404_Solution_PublishedTermsProvider|null $publishedTermsProvider
59 * @param ABJ_404_Solution_TermNGramCoveragePolicy|null $termCoveragePolicy
60 */
61 public function __construct(
62 $contentRepo,
63 $ngramFilter = null,
64 $publishedTermsProvider = null,
65 $termCoveragePolicy = null
66 ) {
67 $this->contentRepo = $contentRepo;
68 $this->ngramFilter = $ngramFilter;
69 $this->publishedTermsProvider = $publishedTermsProvider;
70 $this->termCoveragePolicy = $termCoveragePolicy;
71 }
72
73 /**
74 * Resolve candidate term rows for a taxonomy type.
75 *
76 * @param string $type 'category' or 'tag' (n-gram cache type).
77 * @param string $query The URL slug / cleaned URL to prefilter against.
78 * @return array<int, object> Uniform term-row shape; same either way.
79 */
80 public function getCandidateTermRows(string $type, string $query): array {
81 if ($query !== ''
82 && $this->ngramFilter !== null
83 && $this->publishedTermsProvider !== null
84 && $this->termCoveragePolicy !== null
85 && $this->termCoveragePolicy->isReady($type)) {
86 $candidates = $this->ngramFilter->findSimilarTermIds(
87 $query,
88 $type,
89 self::NGRAM_PREFILTER_THRESHOLD,
90 self::NGRAM_PREFILTER_MAX_CANDIDATES
91 );
92 return $this->publishedTermsProvider->getTermsByIds(array_keys($candidates), $type);
93 }
94
95 return $this->fullScan($type);
96 }
97
98 /**
99 * @param string $type
100 * @return array<int, object>
101 */
102 private function fullScan(string $type): array {
103 $repo = $this->contentRepo;
104 if (!is_object($repo)) {
105 return array();
106 }
107 if ($type === self::TYPE_CATEGORY && method_exists($repo, 'getPublishedCategories')) {
108 return $this->objectRows($repo->getPublishedCategories());
109 }
110 if ($type === self::TYPE_TAG && method_exists($repo, 'getPublishedTags')) {
111 return $this->objectRows($repo->getPublishedTags());
112 }
113 return array();
114 }
115
116 /**
117 * @param mixed $rows
118 * @return array<int, object>
119 */
120 private function objectRows($rows): array {
121 if (!is_array($rows)) {
122 return array();
123 }
124 $objects = array();
125 foreach ($rows as $row) {
126 if (is_object($row)) {
127 $objects[] = $row;
128 }
129 }
130 return $objects;
131 }
132 }
133