Files
SkinbaseNova/app/Console/Commands/RecalculateRankingsCommand.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);
}
});
}
}