optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Services\NovaCards;
use App\Models\NovaCard;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
/**
* Computes and returns related cards for a given card using multiple
* similarity signals: template family, category, mood tags, format,
* style family, palette family, and creator.
*/
class NovaCardRelatedCardsService
{
private const CACHE_TTL = 600;
private const LIMIT = 8;
public function related(NovaCard $card, int $limit = self::LIMIT, bool $cached = true): Collection
{
if ($cached) {
return Cache::remember(
'nova_cards.related.' . $card->id . '.' . $limit,
self::CACHE_TTL,
fn () => $this->compute($card, $limit),
);
}
return $this->compute($card, $limit);
}
public function invalidateForCard(NovaCard $card): void
{
foreach ([4, 6, 8, 12] as $limit) {
Cache::forget('nova_cards.related.' . $card->id . '.' . $limit);
}
}
private function compute(NovaCard $card, int $limit): Collection
{
$card->loadMissing(['tags', 'category', 'template']);
$tagIds = $card->tags->pluck('id')->all();
$templateId = $card->template_id;
$categoryId = $card->category_id;
$format = $card->format;
$styleFamily = $card->style_family;
$paletteFamily = $card->palette_family;
$creatorId = $card->user_id;
$query = NovaCard::query()
->publiclyVisible()
->where('nova_cards.id', '!=', $card->id)
->select(['nova_cards.*'])
->selectRaw('0 AS relevance_score');
// We build a union-ranked set via scored sub-queries, then re-aggregate
// in PHP (simpler than scoring in MySQL without a full-text index).
$candidates = NovaCard::query()
->publiclyVisible()
->where('nova_cards.id', '!=', $card->id)
->where(function ($q) use ($tagIds, $templateId, $categoryId, $format, $styleFamily, $paletteFamily, $creatorId): void {
$q->whereHas('tags', fn ($tq) => $tq->whereIn('nova_card_tags.id', $tagIds))
->orWhere('template_id', $templateId)
->orWhere('category_id', $categoryId)
->orWhere('format', $format)
->orWhere('style_family', $styleFamily)
->orWhere('palette_family', $paletteFamily)
->orWhere('user_id', $creatorId);
})
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags'])
->limit(80)
->get();
// Score in PHP — lightweight for this candidate set size.
$scored = $candidates->map(function (NovaCard $c) use ($tagIds, $templateId, $categoryId, $format, $styleFamily, $paletteFamily, $creatorId): array {
$score = 0;
// Tag overlap: up to 10 points
$overlap = count(array_intersect($c->tags->pluck('id')->all(), $tagIds));
$score += min($overlap * 2, 10);
// Same template: 5 pts
if ($templateId && $c->template_id === $templateId) {
$score += 5;
}
// Same category: 3 pts
if ($categoryId && $c->category_id === $categoryId) {
$score += 3;
}
// Same format: 2 pts
if ($c->format === $format) {
$score += 2;
}
// Same style family: 4 pts
if ($styleFamily && $c->style_family === $styleFamily) {
$score += 4;
}
// Same palette: 3 pts
if ($paletteFamily && $c->palette_family === $paletteFamily) {
$score += 3;
}
// Same creator (more cards by creator): 1 pt
if ($c->user_id === $creatorId) {
$score += 1;
}
// Engagement quality boost (saves + remixes weighted)
$engagementBoost = min(($c->saves_count + $c->remixes_count * 2) * 0.1, 3.0);
$score += $engagementBoost;
return ['card' => $c, 'score' => $score];
});
return $scored
->sortByDesc('score')
->take($limit)
->pluck('card')
->values();
}
}