QdrantService.php
- Pfad:
src/Infrastructure/AI/QdrantService.php - Namespace: Infrastructure\AI
- Zeilen: 300 | Größe: 9,345 Bytes
- Geändert: 2025-12-28 02:27:40 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 85
- Dependencies: 100 (25%)
- LOC: 25 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 2
- use Infrastructure\Config\CredentialService
- use RuntimeException
Klassen 1
-
QdrantServiceclass Zeile 12
Funktionen 7
-
__construct()public Zeile 27 -
search()public Zeile 41 -
collectionExists()public Zeile 74 -
listCollections()public Zeile 105 -
isAvailable()public Zeile 155 -
getCollectionInfo()public Zeile 189 -
makeRequest()private Zeile 245
Verwendet von 7
- ChatController.php constructor
- ChatController.php use
- ChatService.php constructor
- ChatServiceProvider.php use
- StreamingChatMessageUseCase.php constructor
- StreamingChatMessageUseCase.php use
- VectorSearchService.php constructor
Versionen 13
-
v13
2025-12-28 02:27 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v12
2025-12-28 02:27 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v11
2025-12-28 02:27 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v10
2025-12-28 02:26 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v9
2025-12-28 02:26 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v8
2025-12-28 02:26 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v7
2025-12-28 02:26 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v6
2025-12-23 07:59 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v5
2025-12-23 07:58 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v4
2025-12-22 08:25 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v3
2025-12-22 08:25 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v2
2025-12-22 08:25 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v1
2025-12-20 18:48 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation
Code
<?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;
/** @param string|null $host Qdrant API host (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;
}
}