Current state with latest updates
This commit is contained in:
@@ -7,11 +7,14 @@ namespace App\Services\Vision;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
final class VectorGatewayClient
|
||||
{
|
||||
private const CIRCUIT_KEY = 'vision.vector_gateway.circuit_open';
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return (bool) config('vision.vector_gateway.enabled', true)
|
||||
@@ -19,9 +22,25 @@ final class VectorGatewayClient
|
||||
&& $this->apiKey() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* True while the gateway is presumed down after a recent failure — callers
|
||||
* on the request path should skip the network round trip entirely.
|
||||
*/
|
||||
public function circuitOpen(): bool
|
||||
{
|
||||
return Cache::has(self::CIRCUIT_KEY);
|
||||
}
|
||||
|
||||
public function tripCircuit(): void
|
||||
{
|
||||
$seconds = max(1, (int) config('vision.vector_gateway.circuit_breaker_seconds', 30));
|
||||
Cache::put(self::CIRCUIT_KEY, true, $seconds);
|
||||
}
|
||||
|
||||
public function upsertByUrl(string $imageUrl, int|string $id, array $metadata = []): array
|
||||
{
|
||||
$response = $this->postJson(
|
||||
$this->request(),
|
||||
$this->url((string) config('vision.vector_gateway.upsert_endpoint', '/vectors/upsert')),
|
||||
[
|
||||
'url' => $imageUrl,
|
||||
@@ -65,15 +84,24 @@ final class VectorGatewayClient
|
||||
*/
|
||||
public function searchByUrl(string $imageUrl, int $limit = 5): array
|
||||
{
|
||||
$response = $this->postJson(
|
||||
$this->url((string) config('vision.vector_gateway.search_endpoint', '/vectors/search')),
|
||||
[
|
||||
'url' => $imageUrl,
|
||||
'limit' => max(1, $limit),
|
||||
]
|
||||
);
|
||||
$this->guardCircuit();
|
||||
|
||||
try {
|
||||
$response = $this->postJson(
|
||||
$this->searchRequest(),
|
||||
$this->url((string) config('vision.vector_gateway.search_endpoint', '/vectors/search')),
|
||||
[
|
||||
'url' => $imageUrl,
|
||||
'limit' => max(1, $limit),
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->tripCircuit();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
$this->tripCircuit();
|
||||
throw new RuntimeException($this->failureMessage('Vector search', $response));
|
||||
}
|
||||
|
||||
@@ -85,16 +113,24 @@ final class VectorGatewayClient
|
||||
*/
|
||||
public function searchByFileContents(string $contents, string $filename, int $limit = 5): array
|
||||
{
|
||||
$response = $this->request()
|
||||
->attach('file', $contents, $filename)
|
||||
->post(
|
||||
$this->url((string) config('vision.vector_gateway.search_file_endpoint', '/vectors/search/file')),
|
||||
[
|
||||
'limit' => max(1, $limit),
|
||||
]
|
||||
);
|
||||
$this->guardCircuit();
|
||||
|
||||
try {
|
||||
$response = $this->searchRequest()
|
||||
->attach('file', $contents, $filename)
|
||||
->post(
|
||||
$this->url((string) config('vision.vector_gateway.search_file_endpoint', '/vectors/search/file')),
|
||||
[
|
||||
'limit' => max(1, $limit),
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->tripCircuit();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
$this->tripCircuit();
|
||||
throw new RuntimeException($this->failureMessage('Vector search', $response));
|
||||
}
|
||||
|
||||
@@ -122,6 +158,7 @@ final class VectorGatewayClient
|
||||
public function deleteByIds(array $ids): array
|
||||
{
|
||||
$response = $this->postJson(
|
||||
$this->request(),
|
||||
$this->url((string) config('vision.vector_gateway.delete_endpoint', '/vectors/delete')),
|
||||
[
|
||||
'ids' => array_values(array_map(static fn (int|string $id): string => (string) $id, $ids)),
|
||||
@@ -137,6 +174,10 @@ final class VectorGatewayClient
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by upsert/delete — only ever called from queued/console indexing
|
||||
* jobs, so a more generous budget is fine.
|
||||
*/
|
||||
private function request(): PendingRequest
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
@@ -156,12 +197,42 @@ final class VectorGatewayClient
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by search — runs synchronously inside web requests, so it gets a
|
||||
* tight timeout budget and no retries to avoid pinning FPM workers.
|
||||
*/
|
||||
private function searchRequest(): PendingRequest
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
throw new RuntimeException('Vision vector gateway is not configured. Set VISION_VECTOR_GATEWAY_URL and VISION_VECTOR_GATEWAY_API_KEY.');
|
||||
}
|
||||
|
||||
return Http::acceptJson()
|
||||
->withHeaders([
|
||||
'X-API-Key' => $this->apiKey(),
|
||||
])
|
||||
->connectTimeout(max(1, (int) config('vision.vector_gateway.search_connect_timeout_seconds', 2)))
|
||||
->timeout(max(1, (int) config('vision.vector_gateway.search_timeout_seconds', 6)))
|
||||
->retry(
|
||||
max(0, (int) config('vision.vector_gateway.search_retries', 0)),
|
||||
max(0, (int) config('vision.vector_gateway.retry_delay_ms', 250)),
|
||||
throw: false,
|
||||
);
|
||||
}
|
||||
|
||||
private function guardCircuit(): void
|
||||
{
|
||||
if ($this->circuitOpen()) {
|
||||
throw new RuntimeException('Vector gateway temporarily unavailable (circuit open after a recent failure).');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function postJson(string $url, array $payload): Response
|
||||
private function postJson(PendingRequest $request, string $url, array $payload): Response
|
||||
{
|
||||
$response = $this->request()->post($url, $payload);
|
||||
$response = $request->post($url, $payload);
|
||||
|
||||
if (! $response instanceof Response) {
|
||||
throw new RuntimeException('Vector gateway request did not return an HTTP response.');
|
||||
|
||||
Reference in New Issue
Block a user