storing analytics data
This commit is contained in:
121
app/Http/Controllers/Api/SimilarArtworksController.php
Normal file
121
app/Http/Controllers/Api/SimilarArtworksController.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkSearchService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* GET /api/art/{id}/similar
|
||||
*
|
||||
* Returns up to 12 similar artworks based on:
|
||||
* 1. Tag overlap (primary signal)
|
||||
* 2. Same category
|
||||
* 3. Similar orientation
|
||||
*
|
||||
* Uses Meilisearch via ArtworkSearchService for fast retrieval.
|
||||
* Current artwork and its creator are excluded from results.
|
||||
*/
|
||||
final class SimilarArtworksController extends Controller
|
||||
{
|
||||
private const LIMIT = 12;
|
||||
private const CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
public function __construct(private readonly ArtworkSearchService $search) {}
|
||||
|
||||
public function __invoke(int $id): JsonResponse
|
||||
{
|
||||
$artwork = Artwork::public()
|
||||
->published()
|
||||
->with(['tags:id,slug', 'categories:id,slug'])
|
||||
->find($id);
|
||||
|
||||
if (! $artwork) {
|
||||
return response()->json(['error' => 'Artwork not found'], 404);
|
||||
}
|
||||
|
||||
$cacheKey = "api.similar.{$artwork->id}";
|
||||
|
||||
$items = Cache::remember($cacheKey, self::CACHE_TTL, function () use ($artwork) {
|
||||
return $this->findSimilar($artwork);
|
||||
});
|
||||
|
||||
return response()->json(['data' => $items]);
|
||||
}
|
||||
|
||||
private function findSimilar(Artwork $artwork): array
|
||||
{
|
||||
$tagSlugs = $artwork->tags->pluck('slug')->values()->all();
|
||||
$categorySlugs = $artwork->categories->pluck('slug')->values()->all();
|
||||
$orientation = $this->orientation($artwork);
|
||||
|
||||
// Build Meilisearch filter: exclude self and same creator
|
||||
$filterParts = [
|
||||
'is_public = true',
|
||||
'is_approved = true',
|
||||
'id != ' . $artwork->id,
|
||||
'author_id != ' . $artwork->user_id,
|
||||
];
|
||||
|
||||
// Filter by same orientation (landscape/portrait) — improves visual coherence
|
||||
if ($orientation !== 'square') {
|
||||
$filterParts[] = 'orientation = "' . $orientation . '"';
|
||||
}
|
||||
|
||||
// Priority 1: tag overlap (OR match across tags)
|
||||
if ($tagSlugs !== []) {
|
||||
$tagFilter = implode(' OR ', array_map(
|
||||
fn (string $t): string => 'tags = "' . addslashes($t) . '"',
|
||||
$tagSlugs
|
||||
));
|
||||
$filterParts[] = '(' . $tagFilter . ')';
|
||||
} elseif ($categorySlugs !== []) {
|
||||
// Fallback to category if no tags
|
||||
$catFilter = implode(' OR ', array_map(
|
||||
fn (string $c): string => 'category = "' . addslashes($c) . '"',
|
||||
$categorySlugs
|
||||
));
|
||||
$filterParts[] = '(' . $catFilter . ')';
|
||||
}
|
||||
|
||||
$results = Artwork::search('')
|
||||
->options([
|
||||
'filter' => implode(' AND ', $filterParts),
|
||||
'sort' => ['trending_score_7d:desc', 'likes:desc'],
|
||||
])
|
||||
->paginate(self::LIMIT);
|
||||
|
||||
return $results->getCollection()
|
||||
->map(fn (Artwork $a): array => [
|
||||
'id' => $a->id,
|
||||
'title' => $a->title,
|
||||
'slug' => $a->slug,
|
||||
'thumb' => $a->thumbUrl('md'),
|
||||
'url' => '/art/' . $a->id . '/' . $a->slug,
|
||||
'author_id' => $a->user_id,
|
||||
'orientation' => $this->orientation($a),
|
||||
'width' => $a->width,
|
||||
'height' => $a->height,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function orientation(Artwork $artwork): string
|
||||
{
|
||||
if (! $artwork->width || ! $artwork->height) {
|
||||
return 'square';
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$artwork->width > $artwork->height => 'landscape',
|
||||
$artwork->height > $artwork->width => 'portrait',
|
||||
default => 'square',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user