optimizations
This commit is contained in:
143
app/Services/Vision/AiArtworkVectorSearchService.php
Normal file
143
app/Services/Vision/AiArtworkVectorSearchService.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Vision;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
|
||||
final class AiArtworkVectorSearchService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VectorGatewayClient $client,
|
||||
private readonly ArtworkVisionImageUrl $imageUrl,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->client->isConfigured();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function similarToArtwork(Artwork $artwork, int $limit = 12): array
|
||||
{
|
||||
$safeLimit = max(1, min(24, $limit));
|
||||
$cacheKey = sprintf('rec:artwork:%d:similar-ai:%d', $artwork->id, $safeLimit);
|
||||
$ttl = max(60, (int) config('recommendations.ttl.similar_artworks', 30 * 60));
|
||||
|
||||
return Cache::remember($cacheKey, $ttl, function () use ($artwork, $safeLimit): array {
|
||||
$url = $this->imageUrl->fromArtwork($artwork);
|
||||
if ($url === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$matches = $this->client->searchByUrl($url, $safeLimit + 1);
|
||||
|
||||
return $this->resolveMatches($matches, $safeLimit, $artwork->id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function searchByUploadedImage(UploadedFile $file, int $limit = 12): array
|
||||
{
|
||||
$safeLimit = max(1, min(24, $limit));
|
||||
$path = $file->store('ai-search/tmp', 'public');
|
||||
|
||||
if (! is_string($path) || $path === '') {
|
||||
throw new RuntimeException('Unable to persist uploaded image for vector search.');
|
||||
}
|
||||
|
||||
$publicBaseUrl = rtrim((string) config('filesystems.disks.public.url', ''), '/');
|
||||
if ($publicBaseUrl === '') {
|
||||
Storage::disk('public')->delete($path);
|
||||
throw new RuntimeException('Public disk URL is not configured for vector search uploads.');
|
||||
}
|
||||
|
||||
$url = $publicBaseUrl . '/' . ltrim($path, '/');
|
||||
|
||||
try {
|
||||
$matches = $this->client->searchByUrl($url, $safeLimit);
|
||||
|
||||
return $this->resolveMatches($matches, $safeLimit);
|
||||
} finally {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{id: int|string, score: float, metadata: array<string, mixed>}> $matches
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function resolveMatches(array $matches, int $limit, ?int $excludeArtworkId = null): array
|
||||
{
|
||||
$orderedIds = [];
|
||||
$scores = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$artworkId = (int) ($match['id'] ?? 0);
|
||||
if ($artworkId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($excludeArtworkId !== null && $artworkId === $excludeArtworkId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($scores[$artworkId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$orderedIds[] = $artworkId;
|
||||
$scores[$artworkId] = (float) ($match['score'] ?? 0.0);
|
||||
}
|
||||
|
||||
if ($orderedIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$artworks = Artwork::query()
|
||||
->whereIn('id', $orderedIds)
|
||||
->public()
|
||||
->published()
|
||||
->with(['user:id,name', 'user.profile:user_id,avatar_hash'])
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$items = [];
|
||||
foreach ($orderedIds as $artworkId) {
|
||||
/** @var Artwork|null $artwork */
|
||||
$artwork = $artworks->get($artworkId);
|
||||
if ($artwork === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
'id' => $artwork->id,
|
||||
'title' => $artwork->title,
|
||||
'slug' => $artwork->slug,
|
||||
'thumb' => $artwork->thumbUrl('md'),
|
||||
'url' => '/art/' . $artwork->id . '/' . $artwork->slug,
|
||||
'author' => $artwork->user?->name ?? 'Artist',
|
||||
'author_avatar' => $artwork->user?->profile?->avatar_url,
|
||||
'author_id' => $artwork->user_id,
|
||||
'score' => round((float) ($scores[$artworkId] ?? 0.0), 5),
|
||||
'source' => 'vector_gateway',
|
||||
];
|
||||
|
||||
if (count($items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user