Backup #1958

ID1958
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Infrastructure/AI/QdrantService.php
Version13
Typ modified
Größe9.2 KB
Hashdab6c7f6b540c2a218cb1df43995a8f712331f1470b09d9b53f51ebe139d3f96
Datum2025-12-28 02:27:40
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 Qdrant API is available (health check). */
    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 collection metadata (vector size, point count, etc).
     * @return array<string, mixed>|null Null if collection doesn't exist
     * @throws RuntimeException If API request fails (excluding 404)
     */
    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 Qdrant API. @throws RuntimeException */
    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