client->isConfigured(); } /** * @return list> */ 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> */ 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}> $matches * @return list> */ 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; } }