the browser-facing portion of osu!
at master 7.0 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Libraries\Search; 7 8use App\Libraries\Elasticsearch\BoolQuery; 9use App\Libraries\Elasticsearch\RecordSearch; 10use App\Models\Solo\Score; 11use Ds\Set; 12use Exception; 13use LaravelRedis; 14 15class ScoreSearch extends RecordSearch 16{ 17 public $connectionName = 'solo_scores'; 18 19 protected $source = false; 20 21 public function __construct(?ScoreSearchParams $params = null) 22 { 23 parent::__construct( 24 $GLOBALS['cfg']['osu']['elasticsearch']['prefix'].'scores', 25 $params ?? new ScoreSearchParams(), 26 Score::class 27 ); 28 } 29 30 public function getActiveSchemas(): array 31 { 32 return LaravelRedis::smembers('osu-queue:score-index:'.$GLOBALS['cfg']['osu']['elasticsearch']['prefix'].'active-schemas'); 33 } 34 35 public function getQuery(): BoolQuery 36 { 37 $query = new BoolQuery(); 38 39 if ($this->params->isLegacy !== null) { 40 $query->filter(['term' => ['is_legacy' => $this->params->isLegacy]]); 41 } 42 if ($this->params->rulesetId !== null) { 43 $query->filter(['term' => ['ruleset_id' => $this->params->rulesetId]]); 44 } 45 if ($this->params->beatmapIds !== null && count($this->params->beatmapIds) > 0) { 46 $query->filter(['terms' => ['beatmap_id' => $this->params->beatmapIds]]); 47 } 48 if ($this->params->userId !== null) { 49 $query->filter(['term' => ['user_id' => $this->params->userId]]); 50 } 51 if ($this->params->excludeConverts) { 52 $query->filter(['term' => ['convert' => false]]); 53 } 54 if ($this->params->excludeMods !== null && count($this->params->excludeMods) > 0) { 55 foreach ($this->params->excludeMods as $excludedMod) { 56 $query->mustNot(['term' => ['mods' => $excludedMod]]); 57 } 58 } 59 if ($this->params->excludeWithoutPp === true) { 60 $query->filter(['exists' => ['field' => 'pp']]); 61 } 62 63 $this->addModsFilter($query); 64 65 switch ($this->params->getType()) { 66 case 'country': 67 $query->filter(['term' => ['country_code' => $this->params->getCountryCode()]]); 68 break; 69 case 'friend': 70 $query->filter(['terms' => ['user_id' => $this->params->getFriendIds()]]); 71 break; 72 } 73 74 $beforeTotalScore = $this->params->beforeTotalScore; 75 if ($beforeTotalScore === null && $this->params->beforeScore !== null) { 76 $beforeTotalScore = $this->params->isLegacy 77 ? $this->params->beforeScore->legacy_total_score 78 : $this->params->beforeScore->total_score; 79 } 80 if ($beforeTotalScore !== null) { 81 $scoreQuery = (new BoolQuery())->shouldMatch(1); 82 $scoreField = $this->params->isLegacy ? 'legacy_total_score' : 'total_score'; 83 $scoreQuery->should((new BoolQuery())->filter(['range' => [ 84 $scoreField => ['gt' => $beforeTotalScore], 85 ]])); 86 if ($this->params->beforeScore !== null) { 87 $scoreQuery->should((new BoolQuery()) 88 ->filter(['range' => ['id' => ['lt' => $this->params->beforeScore->getKey()]]]) 89 ->filter(['term' => [$scoreField => $beforeTotalScore]])); 90 } 91 92 $query->must($scoreQuery); 93 } 94 95 return $query; 96 } 97 98 public function indexWait(float $maxWaitSecond = 5): void 99 { 100 $count = Score::indexable()->count(); 101 $loopWait = 500000; // 0.5s in microsecond 102 $loops = (int) ceil($maxWaitSecond * 1000000.0 / $loopWait); 103 104 for ($i = 0; $i < $loops; $i++) { 105 usleep($loopWait); 106 $this->refresh(); 107 $indexedCount = $this->client()->count(['index' => $this->index])['count']; 108 if ($indexedCount === $count) { 109 return; 110 } 111 } 112 113 throw new Exception("Indexable and indexed score counts still don't match. Queue runner is probably either having problem, not running, or too slow"); 114 } 115 116 public function queueForIndex(?array $schemas, array $ids): void 117 { 118 $count = count($ids); 119 120 if ($count === 0) { 121 return; 122 } 123 124 $schemas ??= $this->getActiveSchemas(); 125 126 $values = array_map( 127 static fn (int $id): string => json_encode(['ScoreId' => $id]), 128 $ids, 129 ); 130 131 foreach ($schemas as $schema) { 132 LaravelRedis::lpush("osu-queue:{$schema}", ...$values); 133 } 134 } 135 136 public function setSchema(string $schema): void 137 { 138 LaravelRedis::set('osu-queue:score-index:'.$GLOBALS['cfg']['osu']['elasticsearch']['prefix'].'schema', $schema); 139 } 140 141 private function addModsFilter(BoolQuery $query): void 142 { 143 $mods = $this->params->mods; 144 if ($mods === null || count($mods) === 0) { 145 return; 146 } 147 148 $modsHelper = app('mods'); 149 $allMods = $this->params->rulesetId === null 150 ? $modsHelper->allIds 151 : new Set(array_keys($modsHelper->mods[$this->params->rulesetId])); 152 // CL is currently considered a "preference" mod 153 $allMods->remove('CL', 'PF', 'SD', 'MR'); 154 155 $allSearchMods = []; 156 foreach ($mods as $mod) { 157 if ($mod === 'NM') { 158 if (!isset($noModSubQuery)) { 159 $noModSubQuery = new BoolQuery(); 160 foreach ($allMods->toArray() as $excludedMod) { 161 $noModSubQuery->mustNot(['term' => ['mods' => $excludedMod]]); 162 } 163 } 164 continue; 165 } 166 $modsSubQuery ??= new BoolQuery(); 167 $searchMods = [$mod]; 168 $impliedBy = array_search_null($mod, $modsHelper::IMPLIED_MODS); 169 if ($impliedBy !== null) { 170 $searchMods[] = $impliedBy; 171 } 172 $modsSubQuery->filter(['terms' => ['mods' => $searchMods]]); 173 $allSearchMods = [...$allSearchMods, ...$searchMods]; 174 } 175 176 if (isset($modsSubQuery)) { 177 $excludedMods = array_values(array_diff($allMods->toArray(), $allSearchMods)); 178 if (count($excludedMods) > 0) { 179 foreach ($excludedMods as $excludedMod) { 180 $modsSubQuery->mustNot(['term' => ['mods' => $excludedMod]]); 181 } 182 } 183 } 184 185 foreach ([$noModSubQuery ?? null, $modsSubQuery ?? null] as $subQuery) { 186 if ($subQuery !== null) { 187 $shouldSubQueries ??= (new BoolQuery())->shouldMatch(1); 188 $shouldSubQueries->should($subQuery); 189 } 190 } 191 192 if (isset($shouldSubQueries)) { 193 $query->must($shouldSubQueries); 194 } 195 } 196}