82 lines
3.1 KiB
PHP
82 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Services\Ranking\ArtworkRankingService;
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* php artisan nova:recalculate-rankings [--chunk=500] [--sync-rank-scores] [--skip-index]
|
|
*
|
|
* Ranking Engine V2 — recalculates ranking_score and engagement_velocity
|
|
* for all public, approved artworks. Designed to run every 30 minutes.
|
|
*/
|
|
class RecalculateRankingsCommand extends Command
|
|
{
|
|
protected $signature = 'nova:recalculate-rankings
|
|
{--chunk=500 : DB chunk size for batch processing}
|
|
{--sync-rank-scores : Also update rank_artwork_scores table with V2 formula}
|
|
{--skip-index : Skip dispatching Meilisearch re-index jobs}';
|
|
|
|
protected $description = 'Recalculate V2 ranking scores (engagement + shares + decay + authority + velocity)';
|
|
|
|
public function __construct(private readonly ArtworkRankingService $ranking)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
public function handle(): int
|
|
{
|
|
$chunkSize = (int) $this->option('chunk');
|
|
$syncRankScores = (bool) $this->option('sync-rank-scores');
|
|
$skipIndex = (bool) $this->option('skip-index');
|
|
|
|
// ── Step 1: Recalculate ranking_score + engagement_velocity ─────
|
|
$this->info('Ranking V2: recalculating scores …');
|
|
$start = microtime(true);
|
|
$updated = $this->ranking->recalculateAll($chunkSize);
|
|
$elapsed = round(microtime(true) - $start, 2);
|
|
$this->info(" ✓ {$updated} artworks scored in {$elapsed}s");
|
|
|
|
// ── Step 2 (optional): Sync to rank_artwork_scores ─────────────
|
|
if ($syncRankScores) {
|
|
$this->info('Syncing to rank_artwork_scores …');
|
|
$start2 = microtime(true);
|
|
$synced = $this->ranking->syncToRankScores($chunkSize);
|
|
$elapsed2 = round(microtime(true) - $start2, 2);
|
|
$this->info(" ✓ {$synced} rank scores synced in {$elapsed2}s");
|
|
}
|
|
|
|
// ── Step 3 (optional): Trigger Meilisearch re-index ────────────
|
|
if (! $skipIndex) {
|
|
$this->info('Dispatching Meilisearch index jobs …');
|
|
$this->dispatchIndexJobs();
|
|
$this->info(' ✓ Index jobs dispatched');
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Dispatch IndexArtworkJob for artworks updated in the last 24 hours
|
|
* (or recently scored). Keeps the search index current.
|
|
*/
|
|
private function dispatchIndexJobs(): void
|
|
{
|
|
\App\Models\Artwork::query()
|
|
->select('id')
|
|
->where('is_public', true)
|
|
->where('is_approved', true)
|
|
->whereNull('deleted_at')
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '>=', now()->subDays(30)->toDateTimeString())
|
|
->chunkById(500, function ($artworks): void {
|
|
foreach ($artworks as $artwork) {
|
|
\App\Jobs\IndexArtworkJob::dispatch($artwork->id);
|
|
}
|
|
});
|
|
}
|
|
}
|