optimizations
This commit is contained in:
129
app/Services/NovaCards/NovaCardRelatedCardsService.php
Normal file
129
app/Services/NovaCards/NovaCardRelatedCardsService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user