150 lines
4.6 KiB
PHP
150 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Services\RankingService;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class RankBuildScopeListsJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public int $timeout = 300;
|
|
public int $tries = 2;
|
|
|
|
private const LIST_TYPES = ['trending', 'new_hot', 'best'];
|
|
|
|
public function __construct(
|
|
public readonly string $scopeType,
|
|
public readonly int $scopeId,
|
|
) {}
|
|
|
|
public function handle(RankingService $ranking): void
|
|
{
|
|
$modelVersion = config('ranking.model_version', 'rank_v1');
|
|
$maxPerAuthor = (int) config('ranking.diversity.max_per_author', 3);
|
|
$listSize = (int) config('ranking.diversity.list_size', 50);
|
|
$candidatePool = (int) config('ranking.diversity.candidate_pool', 200);
|
|
|
|
foreach (self::LIST_TYPES as $listType) {
|
|
$this->buildAndStore(
|
|
$ranking,
|
|
$listType,
|
|
$this->scopeType,
|
|
$this->scopeId,
|
|
$modelVersion,
|
|
$maxPerAuthor,
|
|
$listSize,
|
|
$candidatePool
|
|
);
|
|
}
|
|
|
|
Log::info('RankBuildScopeListsJob: finished', [
|
|
'scope_type' => $this->scopeType,
|
|
'scope_id' => $this->scopeId,
|
|
'model_version' => $modelVersion,
|
|
]);
|
|
}
|
|
|
|
private function buildAndStore(
|
|
RankingService $ranking,
|
|
string $listType,
|
|
string $scopeType,
|
|
int $scopeId,
|
|
string $modelVersion,
|
|
int $maxPerAuthor,
|
|
int $listSize,
|
|
int $candidatePool
|
|
): void {
|
|
$scoreCol = $this->scoreColumn($listType);
|
|
$candidates = $this->fetchCandidates(
|
|
$scopeType, $scopeId, $scoreCol, $candidatePool, $modelVersion
|
|
);
|
|
|
|
$diverse = $ranking->applyDiversity(
|
|
$candidates->all(), $maxPerAuthor, $listSize
|
|
);
|
|
|
|
$ids = array_map(
|
|
fn ($item) => (int) ($item->artwork_id ?? $item['artwork_id']),
|
|
$diverse
|
|
);
|
|
|
|
DB::table('rank_lists')->upsert(
|
|
[[
|
|
'scope_type' => $scopeType,
|
|
'scope_id' => $scopeId,
|
|
'list_type' => $listType,
|
|
'model_version' => $modelVersion,
|
|
'artwork_ids' => json_encode($ids),
|
|
'computed_at' => now()->toDateTimeString(),
|
|
]],
|
|
['scope_type', 'scope_id', 'list_type', 'model_version'],
|
|
['artwork_ids', 'computed_at']
|
|
);
|
|
|
|
$ranking->bustCache($scopeType, $scopeId === 0 ? null : $scopeId, $listType);
|
|
}
|
|
|
|
/**
|
|
* @return \Illuminate\Support\Collection<int, object>
|
|
*/
|
|
private function fetchCandidates(
|
|
string $scopeType,
|
|
int $scopeId,
|
|
string $scoreCol,
|
|
int $limit,
|
|
string $modelVersion
|
|
): \Illuminate\Support\Collection {
|
|
$query = DB::table('rank_artwork_scores as ras')
|
|
->select(['ras.artwork_id', 'a.user_id', "ras.{$scoreCol}"])
|
|
->join('artworks as a', function ($join): void {
|
|
$join->on('a.id', '=', 'ras.artwork_id')
|
|
->where('a.is_public', 1)
|
|
->where('a.is_approved', 1)
|
|
->whereNull('a.deleted_at');
|
|
})
|
|
->where('ras.model_version', $modelVersion)
|
|
->orderByDesc("ras.{$scoreCol}")
|
|
->limit($limit);
|
|
|
|
if ($scopeType === 'category' && $scopeId > 0) {
|
|
$query->join(
|
|
'artwork_category as ac',
|
|
fn ($j) => $j->on('ac.artwork_id', '=', 'a.id')
|
|
->where('ac.category_id', $scopeId)
|
|
);
|
|
}
|
|
|
|
if ($scopeType === 'content_type' && $scopeId > 0) {
|
|
$query->join(
|
|
'artwork_category as ac',
|
|
'ac.artwork_id', '=', 'a.id'
|
|
)->join(
|
|
'categories as cat',
|
|
fn ($j) => $j->on('cat.id', '=', 'ac.category_id')
|
|
->where('cat.content_type_id', $scopeId)
|
|
->whereNull('cat.deleted_at')
|
|
);
|
|
}
|
|
|
|
return $query->get();
|
|
}
|
|
|
|
private function scoreColumn(string $listType): string
|
|
{
|
|
return match ($listType) {
|
|
'new_hot' => 'score_new_hot',
|
|
'best' => 'score_best',
|
|
default => 'score_trending',
|
|
};
|
|
}
|
|
} |