Backup #1955

ID1955
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Infrastructure/AI/QdrantService.php
Version10
Typ modified
Größe10.7 KB
Hash15a943024618e426b193394bd50044aef32a4d85a46a25e88c03cb3090211fd6
Datum2025-12-28 02:26:50
Geändert vonclaude-code-hook
GrundClaude Code Pre-Hook Backup vor Edit-Operation
Datei existiert Ja

Dateiinhalt

<?php

declare(strict_types=1);

namespace Infrastructure\AI;

// @responsibility: Qdrant-Vektor-DB für Similarity-Search

use Infrastructure\Config\CredentialService;
use RuntimeException;

final class QdrantService
{
    /**
     * Default timeout for HTTP requests in seconds.
     */
    private const int DEFAULT_TIMEOUT = 30;

    /**
     * Health check timeout in seconds.
     */
    private const int HEALTH_CHECK_TIMEOUT = 5;

    private readonly string $host;

    /**
     * Constructs a new QdrantService instance.
     *
     * @param string|null $host The Qdrant API host URL (uses env QDRANT_HOST if null)
     */
    public function __construct(?string $host = null)
    {
        $this->host = $host ?? CredentialService::getQdrantHost();
    }

    /**
     * Searches for similar vectors in a Qdrant collection.
     *
     * @param array<int, float> $vector     Query embedding vector
     * @param string            $collection Collection to search (default: documents)
     * @param int               $limit      Max results (default: 5)
     * @return array<int, array{id: int|string, score: float, payload: array<string, mixed>}>
     * @throws RuntimeException If API request fails
     */
    public function search(array $vector, string $collection = 'documents', int $limit = 5): array
    {
        $url = sprintf('%s/collections/%s/points/search', $this->host, urlencode($collection));

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

        $response = $this->makeRequest($url, $payload, self::DEFAULT_TIMEOUT);

        if (!isset($response['result']) || !is_array($response['result'])) {
            throw new RuntimeException('Invalid search response from Qdrant API');
        }

        return array_map(
            static function (mixed $item): array {
                if (!is_array($item)) {
                    throw new RuntimeException('Invalid search result item format');
                }

                return [
                    'id' => $item['id'] ?? throw new RuntimeException('Missing id in search result'),
                    'score' => (float) ($item['score'] ?? throw new RuntimeException('Missing score in search result')),
                    'payload' => is_array($item['payload'] ?? null) ? $item['payload'] : [],
                ];
            },
            $response['result']
        );
    }

    /** Checks if a collection exists in Qdrant. */
    public function collectionExists(string $collection): bool
    {
        $url = sprintf('%s/collections/%s', $this->host, urlencode($collection));

        try {
            $ch = curl_init($url);

            if ($ch === false) {
                return false;
            }

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_CUSTOMREQUEST => 'GET',
            ]);

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

            curl_close($ch);

            return $result !== false && $httpCode === 200;
        } catch (\Throwable) {
            return false;
        }
    }

    /** Lists all available collections. Returns empty array on error. */
    public function listCollections(): array
    {
        $url = $this->host . '/collections';

        try {
            $ch = curl_init($url);

            if ($ch === false) {
                return [];
            }

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_CUSTOMREQUEST => 'GET',
            ]);

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

            curl_close($ch);

            if ($result === false || $httpCode !== 200) {
                return [];
            }

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

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

            $collections = [];
            foreach ($decoded['result']['collections'] as $collection) {
                if (is_array($collection) && isset($collection['name'])) {
                    $collections[] = (string) $collection['name'];
                }
            }

            sort($collections);

            return $collections;
        } catch (\Throwable) {
            return [];
        }
    }

    /**
     * Checks if the Qdrant API is available and responding.
     *
     * Performs a health check by querying the Qdrant root endpoint.
     * This is a quick operation with a short timeout.
     *
     * @return bool True if the API is available, false otherwise
     *
     * @example
     * $service = new QdrantService();
     * if ($service->isAvailable()) {
     *     echo "Qdrant is running";
     * } else {
     *     echo "Qdrant is not available";
     * }
     */
    public function isAvailable(): bool
    {
        $url = $this->host . '/';

        try {
            $ch = curl_init($url);

            if ($ch === false) {
                return false;
            }

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            ]);

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

            curl_close($ch);

            return $result !== false && $httpCode === 200;
        } catch (\Throwable) {
            return false;
        }
    }

    /**
     * Retrieves metadata and information about a collection.
     *
     * Fetches detailed information about a specific collection including
     * vector size, distance metric, number of points, and other metadata.
     *
     * @param string $collection The collection name to query
     *
     * @return array<string, mixed>|null The collection information array, or null if collection does not exist
     *
     * @throws RuntimeException If the API request fails (excluding 404)
     *
     * @example
     * $service = new QdrantService();
     * $info = $service->getCollectionInfo('documents');
     * // Returns: [
     * //   'status' => 'green',
     * //   'vectors_count' => 1234,
     * //   'points_count' => 1234,
     * //   'config' => ['params' => ['vectors' => ['size' => 1024, 'distance' => 'Cosine']]]
     * // ]
     */
    public function getCollectionInfo(string $collection): ?array
    {
        $url = sprintf('%s/collections/%s', $this->host, urlencode($collection));

        try {
            $ch = curl_init($url);

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

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::DEFAULT_TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_CUSTOMREQUEST => 'GET',
            ]);

            $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 !== '' ? $curlError : 'Unknown error')
                );
            }

            if ($httpCode === 404) {
                return null;
            }

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

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

            if (!is_array($decoded)) {
                throw new RuntimeException('Failed to decode JSON response from Qdrant API');
            }

            return is_array($decoded['result'] ?? null) ? $decoded['result'] : null;
        } catch (RuntimeException $e) {
            throw $e;
        } catch (\Throwable) {
            return null;
        }
    }

    /**
     * Makes an HTTP POST request to the Qdrant API.
     *
     * Internal helper method that handles cURL initialization, request execution,
     * error handling, and response parsing for POST requests.
     *
     * @param string               $url     The API endpoint URL
     * @param array<string, mixed> $payload The JSON payload to send
     * @param int                  $timeout The request timeout in seconds
     *
     * @return array<string, mixed> The decoded JSON response
     *
     * @throws RuntimeException If the request fails or returns an error
     */
    private function makeRequest(string $url, array $payload, int $timeout): array
    {
        $ch = curl_init($url);

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

        $jsonPayload = json_encode($payload);

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

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

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $jsonPayload,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Content-Length: ' . strlen($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 !== '' ? $curlError : 'Unknown error')
            );
        }

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

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

        if (!is_array($decoded)) {
            throw new RuntimeException('Failed to decode JSON response from Qdrant API');
        }

        return $decoded;
    }
}

Vollständig herunterladen

Aktionen

Herunterladen

Andere Versionen dieser Datei

ID Version Typ Größe Datum
1958 13 modified 9.2 KB 2025-12-28 02:27
1957 12 modified 9.7 KB 2025-12-28 02:27
1956 11 modified 10.3 KB 2025-12-28 02:27
1955 10 modified 10.7 KB 2025-12-28 02:26
1954 9 modified 11.1 KB 2025-12-28 02:26
1953 8 modified 11.6 KB 2025-12-28 02:26
1952 7 modified 12.3 KB 2025-12-28 02:26
749 6 modified 12.6 KB 2025-12-23 07:59
745 5 modified 12.8 KB 2025-12-23 07:58
378 4 modified 12.7 KB 2025-12-22 08:25
377 3 modified 12.7 KB 2025-12-22 08:25
376 2 modified 12.7 KB 2025-12-22 08:25
64 1 modified 10.8 KB 2025-12-20 18:48

← Zurück zur Übersicht