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 / NGramRebuilder.php

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

185 lines 6.7 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 * Batch + incremental N-gram cache regeneration.
9 *
10 * Two entry points: rebuildCache() walks the permalink cache in batches
11 * for full reseeding, and updateNGramsForPages() handles per-post events
12 * (slug change, post insert) from SpellPostListeners. Both compose the
13 * extractor and cache repository; coverage invalidation happens once per
14 * batch (rebuild) or per-write (update) via the repository's own write
15 * hooks.
16 */
17 class ABJ_404_Solution_NGramRebuilder {
18
19 /** @var ABJ_404_Solution_DatabaseCore */
20 private $dbCore;
21
22 /** @var ABJ_404_Solution_Logging */
23 private $logger;
24
25 /** @var ABJ_404_Solution_Functions */
26 private $f;
27
28 /** @var ABJ_404_Solution_NGramExtractor */
29 private $extractor;
30
31 /** @var ABJ_404_Solution_NGramCacheRepository */
32 private $repo;
33
34 /** @var ABJ_404_Solution_NGramCoveragePolicy */
35 private $coveragePolicy;
36
37 /**
38 * @param ABJ_404_Solution_NGramRebuilderDependencies|null $deps
39 */
40 public function __construct(?ABJ_404_Solution_NGramRebuilderDependencies $deps = null) {
41 $deps = $deps ?? new ABJ_404_Solution_NGramRebuilderDependencies();
42 $this->dbCore = $deps->dbCore !== null ? $deps->dbCore : abj_service('db_core');
43 $loggingResolved = $deps->logging !== null ? $deps->logging : abj_service('logging');
44 $functionsResolved = $deps->functions !== null ? $deps->functions : abj_service('functions');
45 $extractorResolved = $deps->extractor !== null ? $deps->extractor : abj_service('ngram_extractor');
46 $repoResolved = $deps->repo !== null ? $deps->repo : abj_service('ngram_cache_repository');
47 $coverageResolved = $deps->coveragePolicy !== null ? $deps->coveragePolicy : abj_service('ngram_coverage_policy');
48 if (!$loggingResolved instanceof ABJ_404_Solution_Logging
49 || !$functionsResolved instanceof ABJ_404_Solution_Functions
50 || !$extractorResolved instanceof ABJ_404_Solution_NGramExtractor
51 || !$repoResolved instanceof ABJ_404_Solution_NGramCacheRepository
52 || !$coverageResolved instanceof ABJ_404_Solution_NGramCoveragePolicy) {
53 throw new RuntimeException('NGramRebuilder requires fully-wired collaborators.');
54 }
55 $this->logger = $loggingResolved;
56 $this->f = $functionsResolved;
57 $this->extractor = $extractorResolved;
58 $this->repo = $repoResolved;
59 $this->coveragePolicy = $coverageResolved;
60 }
61
62 /**
63 * Update N-grams for specific pages (incremental update).
64 *
65 * @param array<int, int> $pageIds
66 * @return array{processed: int, success: int, failed: int}
67 */
68 public function updateNGramsForPages($pageIds) {
69 if (empty($pageIds) || !is_array($pageIds)) {
70 return ['processed' => 0, 'success' => 0, 'failed' => 0];
71 }
72
73 $permalinkCacheTable = $this->dbCore->tableNameResolver()->getPrefixedTableName('abj404_permalink_cache');
74
75 $placeholders = implode(',', array_fill(0, count($pageIds), '%d'));
76 $pageResult = $this->dbCore->queryAndGetResults(
77 "SELECT id, url FROM {$permalinkCacheTable} WHERE id IN ({$placeholders})",
78 ['query_params' => array_values($pageIds)]
79 );
80 $pages = isset($pageResult['rows']) && is_array($pageResult['rows']) ? $pageResult['rows'] : [];
81
82 if (empty($pages)) {
83 return ['processed' => 0, 'success' => 0, 'failed' => 0];
84 }
85
86 $stats = ['processed' => 0, 'success' => 0, 'failed' => 0];
87
88 foreach ($pages as $page) {
89 if (is_object($page)) {
90 $page = (array) $page;
91 }
92 if (!is_array($page) || !isset($page['id'], $page['url'])) {
93 continue;
94 }
95 $pageId = is_scalar($page['id']) ? (int)$page['id'] : 0;
96 $url = is_scalar($page['url']) ? (string)$page['url'] : '';
97
98 $urlNormalized = $this->f->strtolower(trim($url));
99 $ngrams = $this->extractor->extractNGrams($urlNormalized);
100 $success = $this->repo->storeNGrams($pageId, $url, $urlNormalized, $ngrams);
101
102 $stats['processed']++;
103 if ($success) {
104 $stats['success']++;
105 } else {
106 $stats['failed']++;
107 }
108 }
109
110 $this->logger->debugMessage(sprintf(
111 "Incremental N-gram update: %d pages, %d success, %d failed",
112 $stats['processed'],
113 $stats['success'],
114 $stats['failed']
115 ));
116
117 return $stats;
118 }
119
120 /**
121 * Rebuild the N-gram cache for all pages (background batch).
122 *
123 * @param int $batchSize
124 * @param int $offset
125 * @return array{processed: int, success: int, failed: int}
126 */
127 public function rebuildCache($batchSize = 100, $offset = 0) {
128 $permalinkCacheTable = $this->dbCore->tableNameResolver()->getPrefixedTableName('abj404_permalink_cache');
129
130 $batchResult = $this->dbCore->queryAndGetResults(
131 "SELECT id, url FROM {$permalinkCacheTable} LIMIT %d OFFSET %d",
132 ['query_params' => [$batchSize, $offset]]
133 );
134 $pages = isset($batchResult['rows']) && is_array($batchResult['rows']) ? $batchResult['rows'] : [];
135
136 $stats = [
137 'processed' => 0,
138 'success' => 0,
139 'failed' => 0
140 ];
141
142 foreach ($pages as $page) {
143 if (is_object($page)) {
144 $page = (array) $page;
145 }
146 if (!is_array($page) || !isset($page['id'], $page['url'])) {
147 continue;
148 }
149 $pageId = is_scalar($page['id']) ? (int)$page['id'] : 0;
150 $url = is_scalar($page['url']) ? (string)$page['url'] : '';
151
152 $urlNormalized = $this->f->strtolower(trim($url));
153 $ngrams = $this->extractor->extractNGrams($urlNormalized);
154
155 // Skip per-item invalidation for bulk efficiency; invalidate at batch end.
156 $success = $this->repo->storeNGrams($pageId, $url, $urlNormalized, $ngrams, 'post', true);
157
158 $stats['processed']++;
159 if ($success) {
160 $stats['success']++;
161 } else {
162 $stats['failed']++;
163 }
164 }
165
166 if ($stats['success'] > 0) {
167 $this->coveragePolicy->invalidateCoverageCaches();
168 $this->repo->resetMemo();
169 }
170
171 // Log every 1000 pages to reduce verbosity
172 if ($offset % 1000 == 0) {
173 $this->logger->debugMessage(sprintf(
174 "N-gram cache rebuild batch (offset %d): %d processed, %d success, %d failed",
175 $offset,
176 $stats['processed'],
177 $stats['success'],
178 $stats['failed']
179 ));
180 }
181
182 return $stats;
183 }
184 }
185