QdrantClient.php

Code Hygiene Score: 97

Keine Issues gefunden.

Dependencies 2

Klassen 1

Funktionen 8

Verwendet von 7

Code

<?php

declare(strict_types=1);

namespace Infrastructure\AI;

// @responsibility: HTTP-Client für Qdrant Vector-Datenbank

use Infrastructure\Config\CredentialService;
use RuntimeException;

final class QdrantClient
{
    private const int TIMEOUT = 60;

    private string $host;

    public function __construct(?string $host = null)
    {
        $this->host = $host ?? CredentialService::getQdrantHost();
    }

    /**
     * Ensures a collection exists with proper configuration.
     */
    public function ensureCollection(string $collection, int $vectorSize = 1024): bool
    {
        $url = sprintf('%s/collections/%s', $this->host, $collection);

        try {
            $response = $this->request($url, [], 'GET');
            if (isset($response['result'])) {
                return true;
            }
        } catch (RuntimeException) {
            // Collection doesn't exist, create it
        }

        $payload = [
            'vectors' => [
                'size' => $vectorSize,
                'distance' => 'Cosine',
            ],
        ];

        try {
            $this->request($url, $payload, 'PUT');

            return true;
        } catch (RuntimeException $e) {
            throw new RuntimeException('Failed to create collection: ' . $e->getMessage());
        }
    }

    /**
     * Gets collection statistics.
     *
     * @return array{points_count: int, status: string}|null
     */
    public function getCollectionStats(string $collection): ?array
    {
        $url = sprintf('%s/collections/%s', $this->host, $collection);

        try {
            $response = $this->request($url, [], 'GET');

            if (!isset($response['result'])) {
                return null;
            }

            return [
                'points_count' => (int) ($response['result']['points_count'] ?? 0),
                'status' => (string) ($response['result']['status'] ?? 'unknown'),
            ];
        } catch (RuntimeException) {
            return null;
        }
    }

    /**
     * Upserts a point to a collection.
     *
     * @param array<int, float> $vector
     * @param array<string, mixed> $payload
     */
    public function upsertPoint(string $collection, string $id, array $vector, array $payload): bool
    {
        $url = sprintf('%s/collections/%s/points', $this->host, $collection);

        $data = [
            'points' => [
                [
                    'id' => $id,
                    'vector' => array_values($vector),
                    'payload' => $payload,
                ],
            ],
        ];

        try {
            $this->request($url, $data, 'PUT');

            return true;
        } catch (RuntimeException) {
            return false;
        }
    }

    /**
     * Searches for similar vectors.
     *
     * @param array<int, float> $vector
     * @param array<string, mixed>|null $filter
     * @return array<array<string, mixed>>
     */
    public function search(string $collection, array $vector, int $limit = 5, ?array $filter = null): array
    {
        $url = sprintf('%s/collections/%s/points/search', $this->host, $collection);

        $payload = [
            'vector' => array_values($vector),
            'limit' => $limit,
            'with_payload' => true,
        ];

        if ($filter !== null) {
            $payload['filter'] = $filter;
        }

        $response = $this->request($url, $payload, 'POST');

        if (!isset($response['result']) || !is_array($response['result'])) {
            return [];
        }

        return $response['result'];
    }

    /**
     * Deletes points by IDs.
     *
     * @param array<string> $ids
     */
    public function deletePoints(string $collection, array $ids): bool
    {
        $url = sprintf('%s/collections/%s/points/delete', $this->host, $collection);

        $data = [
            'points' => $ids,
        ];

        try {
            $this->request($url, $data, 'POST');

            return true;
        } catch (RuntimeException) {
            return false;
        }
    }

    /**
     * Generates a UUID v4.
     */
    public function generateUuid(): string
    {
        $data = random_bytes(16);
        $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
        $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }

    /**
     * Makes an HTTP request to Qdrant.
     *
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private function request(string $url, array $payload, string $method): array
    {
        $ch = curl_init($url);

        if ($ch === false) {
            throw new RuntimeException('Failed to initialize cURL');
        }

        $headers = ['Content-Type: application/json'];

        if ($method === 'GET') {
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_CUSTOMREQUEST => 'GET',
            ]);
        } else {
            $jsonPayload = json_encode($payload);

            if ($jsonPayload === false) {
                curl_close($ch);

                throw new RuntimeException('Failed to encode JSON payload');
            }

            $headers[] = 'Content-Length: ' . strlen($jsonPayload);

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_CUSTOMREQUEST => $method,
                CURLOPT_POSTFIELDS => $jsonPayload,
            ]);
        }

        $result = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);

        curl_close($ch);

        if ($result === false) {
            throw new RuntimeException(sprintf('cURL request failed: %s', $curlError ?: 'Unknown error'));
        }

        if ($httpCode >= 400) {
            throw new RuntimeException(sprintf('Qdrant API returned HTTP %d: %s', $httpCode, $result));
        }

        $decoded = json_decode((string) $result, true);

        return is_array($decoded) ? $decoded : [];
    }
}
← Übersicht Graph