host = $host ?? CredentialService::getQdrantHost(); } /** * Searches for similar vectors in a Qdrant collection. * * @param array $vector Query embedding vector * @param string $collection Collection to search (default: documents) * @param int $limit Max results (default: 5) * @return array}> * @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. * * Verifies the existence of a specific collection by attempting to retrieve * its information. This is useful before performing searches or operations. * * @param string $collection The collection name to check * * @return bool True if the collection exists, false otherwise * * @example * $service = new QdrantService(); * if ($service->collectionExists('documents')) { * echo "Collection exists"; * } else { * echo "Collection not found"; * } */ 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 in Qdrant. * * Retrieves a list of all collection names from the Qdrant database. * Returns an empty array if Qdrant is not available or on error. * * @return array Array of collection names * * @example * $service = new QdrantService(); * $collections = $service->listCollections(); * // Returns: ['documents', 'mail', 'entities'] */ 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|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 $payload The JSON payload to send * @param int $timeout The request timeout in seconds * * @return array 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; } }