153 lines
5.3 KiB
PHP
153 lines
5.3 KiB
PHP
<?php
|
||
/**
|
||
* @package Bicycle
|
||
* @author Egor Isaev
|
||
* @description Client.php
|
||
* @copyright (c) 07/08/2026
|
||
*/
|
||
|
||
namespace Services\Elasticsearch;
|
||
|
||
use RuntimeException;
|
||
use System\Classes\HTTP\Client\Curl;
|
||
use System\Classes\HTTP\Client\Request as HttpRequest;
|
||
use System\Classes\HTTP\Client\Response;
|
||
|
||
/**
|
||
* Тонкая обёртка над Elasticsearch REST API поверх System\Classes\HTTP\Client\Curl —
|
||
* без тяжёлой composer-зависимости elasticsearch/elasticsearch, только базовые операции
|
||
* (индексация/поиск/чтение/удаление документа, создание/удаление индекса).
|
||
*/
|
||
class Client
|
||
{
|
||
/**
|
||
* @param array $config base_url, timeout (сек, по умолчанию 10), username/password (Basic auth, опционально)
|
||
*/
|
||
public function __construct(protected array $config)
|
||
{
|
||
}
|
||
|
||
/**
|
||
* Индексирует документ. Без $id — Elasticsearch сгенерирует его сам (POST);
|
||
* с $id — документ по этому id создаётся/перезаписывается (PUT).
|
||
*
|
||
* @param string $index Имя индекса
|
||
* @param array $document Тело документа
|
||
* @param string|null $id ID документа
|
||
* @return array Ответ Elasticsearch (декодированный JSON)
|
||
* @throws \JsonException
|
||
*/
|
||
public function index(string $index, array $document, ?string $id = null): array
|
||
{
|
||
$path = $id !== null ? "/$index/_doc/" . rawurlencode($id) : "/$index/_doc";
|
||
$method = $id !== null ? 'PUT' : 'POST';
|
||
|
||
return $this->request($method, $path, $document)->json();
|
||
}
|
||
|
||
/**
|
||
* @param string $index Имя индекса
|
||
* @param string $id ID документа
|
||
* @return array|null null, если документ не найден (404)
|
||
* @throws \JsonException
|
||
*/
|
||
public function get(string $index, string $id): ?array
|
||
{
|
||
$response = $this->request('GET', "/$index/_doc/" . rawurlencode($id));
|
||
|
||
return $response->status() === 404 ? null : $response->json();
|
||
}
|
||
|
||
/**
|
||
* @param string $index Имя индекса
|
||
* @param string $id ID документа
|
||
* @return bool
|
||
*/
|
||
public function exists(string $index, string $id): bool
|
||
{
|
||
return $this->request('GET', "/$index/_doc/" . rawurlencode($id))->status() === 200;
|
||
}
|
||
|
||
/**
|
||
* @param string $index Имя индекса
|
||
* @param array $query Elasticsearch Query DSL, например ['query' => ['match' => ['title' => 'foo']]]
|
||
* @return array Ответ Elasticsearch (декодированный JSON)
|
||
* @throws \JsonException
|
||
*/
|
||
public function search(string $index, array $query): array
|
||
{
|
||
return $this->request('POST', "/$index/_search", $query)->json();
|
||
}
|
||
|
||
/**
|
||
* @param string $index Имя индекса
|
||
* @param string $id ID документа
|
||
* @return bool true, если удалён
|
||
*/
|
||
public function delete(string $index, string $id): bool
|
||
{
|
||
return $this->request('DELETE', "/$index/_doc/" . rawurlencode($id))->isSuccess();
|
||
}
|
||
|
||
/**
|
||
* @param string $index Имя индекса
|
||
* @param array $body settings/mappings (опционально)
|
||
* @return array Ответ Elasticsearch (декодированный JSON)
|
||
* @throws \JsonException
|
||
*/
|
||
public function createIndex(string $index, array $body = []): array
|
||
{
|
||
return $this->request('PUT', "/$index", $body ?: null)->json();
|
||
}
|
||
|
||
/**
|
||
* @param string $index Имя индекса
|
||
* @return bool true, если удалён
|
||
*/
|
||
public function deleteIndex(string $index): bool
|
||
{
|
||
return $this->request('DELETE', "/$index")->isSuccess();
|
||
}
|
||
|
||
/**
|
||
* Проверяет доступность кластера. Не бросает исключение при недоступности — просто false
|
||
* (удобно для graceful skip в тестах/коде, когда Elasticsearch может быть не поднят).
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function ping(): bool
|
||
{
|
||
try {
|
||
return $this->request('GET', '/')->isSuccess();
|
||
} catch (RuntimeException) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param string $method HTTP-метод
|
||
* @param string $path Путь относительно base_url
|
||
* @param array|null $body Тело запроса (JSON), null — без тела
|
||
* @return Response
|
||
*/
|
||
protected function request(string $method, string $path, ?array $body = null): Response
|
||
{
|
||
$http_request = HttpRequest::factory(rtrim($this->config['base_url'] ?? '', '/') . $path)
|
||
->method($method)
|
||
->timeout($this->config['timeout'] ?? 10);
|
||
|
||
if ($body !== null) {
|
||
$http_request->json($body);
|
||
}
|
||
|
||
if (!empty($this->config['username'])) {
|
||
$http_request->header(
|
||
'Authorization',
|
||
'Basic ' . base64_encode($this->config['username'] . ':' . ($this->config['password'] ?? ''))
|
||
);
|
||
}
|
||
|
||
return (new Curl())->execute($http_request);
|
||
}
|
||
}
|