feat: Ranking Engine V2 — intelligent scoring with shares, authority, decay & velocity\n\n- Add ArtworkRankingService with V2 formula:\n ranking_score = (base × authority × decay) + velocity_boost\n Base: views×0.2 + downloads×1.5 + favourites×2.5 + comments×3.0 + shares×4.0\n Authority: 1 + (log10(1+followers) + fav_received/1000) × 0.05\n Decay: 1 / (1 + hours/48)\n Velocity: 24h signals × velocity_weights × 0.5\n\n- Add nova:recalculate-rankings command (--chunk, --sync-rank-scores, --skip-index)\n- Add migration: ranking_score, engagement_velocity, shares/comments counts to artwork_stats\n- Upgrade RankingService.computeScores() with shares + comments + velocity\n- Update Meilisearch sortableAttributes: ranking_score, shares_count, engagement_velocity, comments_count\n- Update toSearchableArray() to expose V2 fields\n- Schedule every 30 min with overlap protection\n- Verified: 49733 artworks scored successfully"
This commit is contained in:
81
app/Console/Commands/RecalculateRankingsCommand.php
Normal file
81
app/Console/Commands/RecalculateRankingsCommand.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user