Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
@@ -8,12 +8,18 @@ use App\Models\Artwork;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class AiArtworkVectorSearchService
|
||||
{
|
||||
private const MAX_SIMILAR_RESULTS = 120;
|
||||
/** qdrant-svc /vectors/search* reject limit > 100. */
|
||||
private const VECTOR_GATEWAY_MAX_SEARCH_RESULTS = 100;
|
||||
|
||||
/**
|
||||
* Public similar-ai may request at most 99 items because we fetch one extra
|
||||
* candidate so the source artwork can be excluded from the result set.
|
||||
*/
|
||||
private const MAX_ARTWORK_SIMILAR_RESULTS = 99;
|
||||
|
||||
public function __construct(
|
||||
private readonly VectorGatewayClient $client,
|
||||
@@ -31,12 +37,13 @@ final class AiArtworkVectorSearchService
|
||||
*/
|
||||
public function similarToArtwork(Artwork $artwork, int $limit = 12): array
|
||||
{
|
||||
$safeLimit = max(1, min(self::MAX_SIMILAR_RESULTS, $limit));
|
||||
$safeLimit = max(1, min(self::MAX_ARTWORK_SIMILAR_RESULTS, $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 {
|
||||
$matches = $this->searchMatchesForArtwork($artwork, $safeLimit + 1);
|
||||
$gatewayLimit = min(self::VECTOR_GATEWAY_MAX_SEARCH_RESULTS, $safeLimit + 1);
|
||||
$matches = $this->searchMatchesForArtwork($artwork, $gatewayLimit);
|
||||
|
||||
return $this->resolveMatches($matches, $safeLimit, $artwork->id);
|
||||
});
|
||||
@@ -47,7 +54,7 @@ final class AiArtworkVectorSearchService
|
||||
*/
|
||||
public function searchByUploadedImage(UploadedFile $file, int $limit = 12): array
|
||||
{
|
||||
$safeLimit = max(1, min(self::MAX_SIMILAR_RESULTS, $limit));
|
||||
$safeLimit = max(1, min(self::VECTOR_GATEWAY_MAX_SEARCH_RESULTS, $limit));
|
||||
$matches = $this->client->searchByUploadedFile($file, $safeLimit);
|
||||
|
||||
return $this->resolveMatches($matches, $safeLimit);
|
||||
@@ -93,15 +100,15 @@ final class AiArtworkVectorSearchService
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->client->circuitOpen()) {
|
||||
throw new RuntimeException('Vector gateway temporarily unavailable (circuit open after a recent failure).');
|
||||
}
|
||||
$this->client->assertCircuitClosed();
|
||||
|
||||
$fileFailure = null;
|
||||
$fileGatewayAttempted = false;
|
||||
|
||||
try {
|
||||
$payload = $this->downloadArtworkImage($artwork, $url);
|
||||
if ($payload !== null) {
|
||||
$fileGatewayAttempted = true;
|
||||
return $this->client->searchByFileContents($payload['contents'], $payload['filename'], $limit);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
@@ -110,24 +117,69 @@ final class AiArtworkVectorSearchService
|
||||
|
||||
try {
|
||||
return $this->client->searchByUrl($url, $limit);
|
||||
} catch (Throwable $e) {
|
||||
throw $this->normalizeSearchFailure($fileFailure, $e);
|
||||
} catch (Throwable $fallbackFailure) {
|
||||
if ($this->shouldTripSimilarAiCircuit($fileGatewayAttempted, $fileFailure, $fallbackFailure)) {
|
||||
$this->client->tripIfCircuitWorthy(
|
||||
[$fileFailure, $fallbackFailure],
|
||||
'similar_ai',
|
||||
['artwork_id' => (int) $artwork->id],
|
||||
);
|
||||
}
|
||||
|
||||
throw $this->normalizeSearchFailure($fileFailure, $fallbackFailure);
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeSearchFailure(?Throwable $fileFailure, Throwable $fallbackFailure): RuntimeException
|
||||
private function normalizeSearchFailure(?Throwable $fileFailure, Throwable $fallbackFailure): VectorGatewayException
|
||||
{
|
||||
if ($fileFailure === null) {
|
||||
return $fallbackFailure instanceof RuntimeException
|
||||
? $fallbackFailure
|
||||
: new RuntimeException($fallbackFailure->getMessage(), 0, $fallbackFailure);
|
||||
if ($fallbackFailure instanceof VectorGatewayException && $fileFailure === null) {
|
||||
return $fallbackFailure;
|
||||
}
|
||||
|
||||
return new RuntimeException(sprintf(
|
||||
'Vector search failed via file endpoint (%s) and URL fallback (%s).',
|
||||
$fileFailure->getMessage(),
|
||||
$fallbackFailure->getMessage(),
|
||||
), 0, $fallbackFailure);
|
||||
$status = $fallbackFailure instanceof VectorGatewayException
|
||||
? $fallbackFailure->httpStatus
|
||||
: null;
|
||||
$circuitWorthy = $this->isCircuitWorthyFailure($fileFailure)
|
||||
&& $this->isCircuitWorthyFailure($fallbackFailure);
|
||||
|
||||
if ($fileFailure === null) {
|
||||
return $fallbackFailure instanceof VectorGatewayException
|
||||
? $fallbackFailure
|
||||
: new VectorGatewayException(
|
||||
'Vector gateway search failed.',
|
||||
'search_url',
|
||||
$status,
|
||||
$this->isCircuitWorthyFailure($fallbackFailure),
|
||||
$fallbackFailure,
|
||||
);
|
||||
}
|
||||
|
||||
return new VectorGatewayException(
|
||||
'Vector gateway search failed.',
|
||||
'search_combined',
|
||||
$status,
|
||||
$circuitWorthy,
|
||||
$fallbackFailure,
|
||||
);
|
||||
}
|
||||
|
||||
private function isCircuitWorthyFailure(Throwable $failure): bool
|
||||
{
|
||||
return $failure instanceof VectorGatewayException && $failure->circuitWorthy;
|
||||
}
|
||||
|
||||
private function shouldTripSimilarAiCircuit(
|
||||
bool $fileGatewayAttempted,
|
||||
?Throwable $fileFailure,
|
||||
Throwable $fallbackFailure,
|
||||
): bool {
|
||||
return $fileGatewayAttempted
|
||||
&& $fileFailure instanceof VectorGatewayException
|
||||
&& $fileFailure->operation === 'search_file'
|
||||
&& $fileFailure->circuitWorthy
|
||||
&& $fallbackFailure instanceof VectorGatewayException
|
||||
&& $fallbackFailure->operation === 'search_url'
|
||||
&& $fallbackFailure->circuitWorthy;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user