|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace App\Console\Commands; |
| 4 | + |
| 5 | +use Illuminate\Console\Command; |
| 6 | +use Illuminate\Support\Facades\Http; |
| 7 | +use Illuminate\Support\Facades\Log; |
| 8 | +use App\Wiki; |
| 9 | +use App\WikiSetting; |
| 10 | +use App\QueryserviceNamespace; |
| 11 | +use App\Jobs\SpawnQueryserviceUpdaterJob; |
| 12 | + |
| 13 | +class RebuildQueryserviceData extends Command |
| 14 | +{ |
| 15 | + private const NAMESPACE_ITEM = 120; |
| 16 | + private const NAMESPACE_PROPERTY = 122; |
| 17 | + private const NAMESPACE_LEXEME = 146; |
| 18 | + |
| 19 | + protected $signature = 'wbs-qs:rebuild {--domain=*} {--chunkSize=50} {--sparqlUrlFormat=http://queryservice.default.svc.cluster.local:9999/bigdata/namespace/%s/sparql}'; |
| 20 | + |
| 21 | + protected $description = 'Rebuild the queryservice data for a certain wiki or all wikis'; |
| 22 | + |
| 23 | + protected int $chunkSize; |
| 24 | + protected string $apiUrl; |
| 25 | + protected string $sparqlUrlFormat; |
| 26 | + |
| 27 | + public function handle() |
| 28 | + { |
| 29 | + $this->chunkSize = intval($this->option('chunkSize')); |
| 30 | + $this->sparqlUrlFormat = $this->option('sparqlUrlFormat'); |
| 31 | + $this->apiUrl = getenv('PLATFORM_MW_BACKEND_HOST').'/w/api.php'; |
| 32 | + |
| 33 | + $wikiDomains = $this->option('domain'); |
| 34 | + $exitCode = 0; |
| 35 | + |
| 36 | + $wikis = count($wikiDomains) !== 0 |
| 37 | + ? Wiki::whereIn('domain', $wikiDomains)->get() |
| 38 | + : Wiki::query()->get(); |
| 39 | + |
| 40 | + $jobTotal = 0; |
| 41 | + $skippedWikis = 0; |
| 42 | + $processedWikis = 0; |
| 43 | + foreach ($wikis as $wiki) { |
| 44 | + try { |
| 45 | + $entities = $this->getEntitiesForWiki($wiki); |
| 46 | + $sparqlUrl = $this->getSparqlUrl($wiki); |
| 47 | + } catch (\Exception $ex) { |
| 48 | + Log::error( |
| 49 | + 'Failed to get prerequisites for enqueuing wiki '.$wiki->domain.', will not dispatch jobs.', |
| 50 | + [$ex], |
| 51 | + ); |
| 52 | + $exitCode = 1; |
| 53 | + $skippedWikis += 1; |
| 54 | + break; |
| 55 | + } |
| 56 | + |
| 57 | + $entityChunks = array_chunk($entities, $this->chunkSize); |
| 58 | + foreach ($entityChunks as $entityChunk) { |
| 59 | + dispatch( |
| 60 | + new SpawnQueryserviceUpdaterJob( |
| 61 | + $wiki->domain, |
| 62 | + implode(',', $entityChunk), |
| 63 | + $sparqlUrl, |
| 64 | + ) |
| 65 | + ); |
| 66 | + } |
| 67 | + $jobTotal += count($entityChunks); |
| 68 | + $processedWikis += 1; |
| 69 | + Log::info('Dispatched '.count($entityChunks).' job(s) for wiki '.$wiki->domain.'.'); |
| 70 | + } |
| 71 | + |
| 72 | + Log::info( |
| 73 | + 'Done. Jobs dispatched: '.$jobTotal.' Wikis processed: '.$processedWikis.' Wikis skipped: '.$skippedWikis |
| 74 | + ); |
| 75 | + return $exitCode; |
| 76 | + } |
| 77 | + |
| 78 | + private function getEntitiesForWiki (Wiki $wiki): array |
| 79 | + { |
| 80 | + $items = $this->fetchPagesInNamespace($wiki->domain, self::NAMESPACE_ITEM); |
| 81 | + $properties = $this->fetchPagesInNamespace($wiki->domain, self::NAMESPACE_PROPERTY); |
| 82 | + |
| 83 | + $lexemesSetting = WikiSetting::where( |
| 84 | + [ |
| 85 | + 'wiki_id' => $wiki->id, |
| 86 | + 'name' => WikiSetting::wwExtEnableWikibaseLexeme |
| 87 | + ], |
| 88 | + )->first(); |
| 89 | + $hasLexemesEnabled = $lexemesSetting !== null && $lexemesSetting->value === '1'; |
| 90 | + $lexemes = $hasLexemesEnabled |
| 91 | + ? $this->fetchPagesInNamespace($wiki->domain, self::NAMESPACE_LEXEME) |
| 92 | + : []; |
| 93 | + |
| 94 | + $merged = array_merge($items, $properties, $lexemes); |
| 95 | + return $this->stripPrefixes($merged); |
| 96 | + } |
| 97 | + |
| 98 | + private function getSparqlUrl (Wiki $wiki): string |
| 99 | + { |
| 100 | + $match = QueryserviceNamespace::where(['wiki_id' => $wiki->id])->first(); |
| 101 | + if (!$match) { |
| 102 | + throw new \Exception( |
| 103 | + 'Unable to find queryservice namespace record for wiki '.$wiki->domain |
| 104 | + ); |
| 105 | + } |
| 106 | + return sprintf($this->sparqlUrlFormat, $match->namespace); |
| 107 | + } |
| 108 | + |
| 109 | + private function fetchPagesInNamespace(string $wikiDomain, int $namespace): array |
| 110 | + { |
| 111 | + $titles = []; |
| 112 | + $cursor = ''; |
| 113 | + while (true) { |
| 114 | + $response = Http::withHeaders([ |
| 115 | + 'host' => $wikiDomain |
| 116 | + ])->get( |
| 117 | + $this->apiUrl, |
| 118 | + [ |
| 119 | + 'action' => 'query', |
| 120 | + 'list' => 'allpages', |
| 121 | + 'apnamespace' => $namespace, |
| 122 | + 'apcontinue' => $cursor, |
| 123 | + 'aplimit' => 'max', |
| 124 | + 'format' => 'json', |
| 125 | + ], |
| 126 | + ); |
| 127 | + |
| 128 | + if ($response->failed()) { |
| 129 | + throw new \Exception( |
| 130 | + 'Failed to fetch allpages for wiki '.$wikiDomain |
| 131 | + ); |
| 132 | + } |
| 133 | + |
| 134 | + $jsonResponse = $response->json(); |
| 135 | + $error = data_get($jsonResponse, 'error'); |
| 136 | + if ($error !== null) { |
| 137 | + throw new \Exception( |
| 138 | + 'Error response fetching allpages for wiki '.$wikiDomain.': '.$error |
| 139 | + ); |
| 140 | + } |
| 141 | + |
| 142 | + $pages = data_get($jsonResponse, 'query.allpages', []); |
| 143 | + $titles = array_merge($titles, array_map(function (array $page) { |
| 144 | + return $page['title']; |
| 145 | + }, $pages)); |
| 146 | + |
| 147 | + $nextCursor = data_get($jsonResponse, 'continue.apcontinue'); |
| 148 | + if ($nextCursor === null) { |
| 149 | + break; |
| 150 | + } |
| 151 | + $cursor = $nextCursor; |
| 152 | + } |
| 153 | + |
| 154 | + return $titles; |
| 155 | + } |
| 156 | + |
| 157 | + private static function stripPrefixes (array $items): array |
| 158 | + { |
| 159 | + return array_map(function (string $item) { |
| 160 | + return preg_replace('/^[a-zA-Z]+:/', '', $item); |
| 161 | + }, $items); |
| 162 | + } |
| 163 | +} |
0 commit comments