Elasticsearch 8용 새로운 PHP 클라이언트 소개

library-branding-elastic-enterprise-search-midnight-1680x980-no-logo.png

Elasticsearch 8용 새로운 PHP 클라이언트는 아예 처음부터 다시 작성되었습니다. PSR 표준을 채택하면서, 아키텍처를 재설계하고 HTTP 전송 계층을 외부로 옮겼습니다. HTTPlug 라이브러리 덕분에, 이제 플러그형 시스템도 사용할 수 있습니다.

계속 살펴보려면 다음 항목을 읽어보세요.

  • PHP 클라이언트의 새로운 아키텍처 및 특징
  • HTTP 메시지에 대해 PSR-7 표준을 사용하여 엔드포인트를 사용하고 오류를 관리하는 방법
  • 비동기식 접근법을 사용하여 Elasticsearch와 상호작용하는 방법

예전 클라이언트

elasticsearch-php 라이브러리는 Elasticsearch를 PHP로 프로그래밍하기 위한 공식 클라이언트입니다. 이 라이브러리는 메인 클라이언트 클래스를 사용하여 Elasticsearch의 모든 400개 이상의 엔드포인트를 노출합니다. 이 라이브러리의 버전 7에서는, 모든 엔드포인트가 함수를 사용하여 노출됩니다. 예를 들어, 인덱스 API는 메서드 Client::index()에 매핑됩니다.

이 함수들은 Elasticsearch에서 HTTP 응답의 역직렬화인 연관 배열을 반환합니다. 보통, 이 응답은 JSON 메시지로 표현됩니다. 이 메시지는 PHP의 json_decode() 함수를 사용하여 배열로 변환됩니다.

오류의 경우, 클라이언트는 문제에 따라 예외를 발생시킵니다. 예를 들어, HTTP 응답이 404인 경우 클라이언트는 Missing404Exception을 발생시킵니다. HTTP 응답 자체를 검색하려면, 다음 코드를 사용하여 클라이언트의 마지막 응답을 받아야 합니다.

$response = $client->info();
$last = $client->transport->getLastConnection()->getLastRequestInfo();
 
$request = $last['request']; // associative array of the HTTP request
var_dump($request);
 
$response = $last['response']; // associative array of the HTTP response
echo $response['status']; // 200
echo $response['body'];   // the body as string

HTTP 요청 및 응답은 전송 계층(클라이언트의 속성)에서 getLastConnection()getLastRequestInfo() 등 몇 가지 방법으로 검색됩니다.

이 코드는 PHP의 cURL 확장자 사용으로 인해 연관 배열 $response의 키가 상당히 많기 때문에 좋은 개발자 경험을 제공하지 못합니다.

새 클라이언트

Elastic은 개발자 경험, 새로운 PHP 표준, 보다 개방적인 아키텍처, 성능 등 여러 가지 이유로 새로운 elasticsearch-php 8을 아예 처음부터 구축했습니다.

7천만 개의 설치가 있으며, Elastic은 버전 8에 대해 많은 BC 중단을 원하지 않았습니다. Elastic은 버전 7과 동일한 API를 제공하는 하위 호환성 접근 방식을 사용했습니다. 즉, 동일한 코드를 사용하여 Elasticsearch에 연결하고 평소와 동일한 엔드포인트 호출을 실행할 수 있습니다. 차이점은 응답에 있습니다. 버전 8에서, 응답은 PSR-7 응답 인터페이스와 PHP의 ArrayAccess 인터페이스를 구현하는 Elasticsearch 응답의 객체입니다.

잠시만요. BC가 크게 중단된 것 같지 않나요? 다행히, 우리는 ArrayAccess 인터페이스를 구현했으며 여러분은 다음과 같이 응답을 어레이로 계속 사용하실 수 있습니다.

차이점을 찾아보세요. 네임스페이스가 변경되었습니다! 우리는 Elastic root 네임스페이스를 도입했습니다. 다른 코드는 같지만, 잘 들여다보면 큰 변화가 있습니다.

앞서 언급한 것처럼, 버전 8의 $response는 객체이고 버전 7의 경우는 이것이 연관 배열입니다. 버전 7과 정확히 동일한 동작을 원하는 경우, $response->asArray() 함수를 사용하여 응답을 배열로 직렬화할 수 있습니다.

또한 스트링 또는 부울로(2xx 응답이면 true, 그렇지 않으면 false), PHP(stdClass)의 표준 클래스의 객체로 본문을 직렬화하기 위해 asObject(), asString(), asBool() 함수도 제공합니다.

예를 들어, 다음과 같이 이전의 info() 엔드포인트를 사용할 수 있습니다.

$client = ClientBuilder::create()
   ->setHosts(['localhost:9200'])
   ->build();
$response = $client->info();
 
echo $response['version']['number'];
echo $response->version->number; // 8.0.0
 
var_dump($response->asObject()); // response body content as stdClass object
var_dump($response->asString()); // response body as string (JSON)
var_dump($response->asBool());   // true if HTTP response code 2xx

$response는 PHP의 _get() 매직 메서드를 구현하는 객체로 응답 본문에 접근할 수 있습니다.

HTTP 응답을 읽으려는 경우, 클라이언트 객체에서 마지막 메시지를 복구할 필요가 없습니다. 다음과 같이 $response 자체에 있는 PSR-7 메시지에 액세스할 수 있습니다.

echo $response->getStatusCode();    // 200, since $response is PSR-7
echo (string) $response->getBody(); // Response body in JSON

이것은 특히 비동기식으로 작업하는 경우, 큰 이점입니다. 실제로, 비동기식 프로그래밍을 사용하는 경우, 클라이언트에서 마지막 응답을 가져올 수 없습니다. 마지막 응답이 여러분이 찾고 있는 응답이라는 것은 보장되지 않습니다(이 글의 뒷부분에서 비동기식 작업에 대해 좀더 다룹니다).

자동 완성을 위한 엔드포인트 매개 변수

우리는 Psalm 프로젝트의 유사 객체 배열을 사용하여 elastic search-php 버전 8에 자동 완성 기능을 추가했습니다. Psalm은 개발자들이 특별한 phpDoc 속성을 사용하여 코드를 꾸밀 수 있게 해주는 정적 분석 도구입니다. 이러한 속성 중 하나가 @psalm-type인데, 이는 연관 배열의 주요 유형을 지정할 수 있게 해줍니다. 우리는 표준 phpDoc @param을 사용하여 Psalm 유형을 적용했습니다. 각 PHP 클라이언트 엔드포인트는 $params 배열인 입력 매개 변수를 가지고 있습니다. 예를 들어, 여기에 보고된 것은 index() 엔드포인트 섹션입니다.

/**
    * Creates or updates a document in an index.
    *
    * @see https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-index_.html
    *
    * @param array{
    *     id: string, //  Document ID
    *     index: string, // (REQUIRED) The name of the index
    *     wait_for_active_shards: string, // Sets the number of shard copies …
    *     op_type: enum, // Explicit operation type. Defaults to `index` for requests…
    *     refresh: enum, // If `true` then refresh the affected shards to make this operation…
    *     routing: string, // Specific routing value
    *     timeout: time, // Explicit operation timeout
    *     version: number, // Explicit version number for concurrency control
    *     version_type: enum, // Specific version type
    *     if_seq_no: number, // only perform the index operation if the last operation…
    *     if_primary_term: number, // only perform the index operation if the last operation…
    *     pipeline: string, // The pipeline id to preprocess incoming documents with
    *     require_alias: boolean, // When true, requires destination to be an alias…
    *     pretty: boolean, // Pretty format the returned JSON response. (DEFAULT: false)
    *     human: boolean, // Return human readable values for statistics. (DEFAULT: true)
    *     error_trace: boolean, // Include the stack trace of returned errors. (DEFAULT: false)
    *     source: string, // The URL-encoded request definition. Useful for libraries…
    *     filter_path: list, // A comma-separated list of filters used to reduce the response.
    *     body: array, // (REQUIRED) The document
    * } $params
    */
   public function index(array $params = [])

유형(스트링)과 매개 변수(인덱스의 이름)를 설명하는 주석을 포함하여 모든 매개 변수는 이름(인덱스)으로 지정됩니다. 필수 매개 변수는 REQUIRED 노트를 사용하여 지정합니다.

이전 표기법을 사용하여 IDE에 자동 완성을 갖출 수 있습니다. 예를 들어, PhpStorm을 사용하여 deep-assoc-completion 무료 플러그인을 설치하여 @psalm-type 속성으로 PHP 연관 배열 자동 완성을 활성화할 수 있습니다.

videoImage

이 버전이 아직 개발 중인 경우에도 Visual Studio Code에서 deep-assoc-completion을 사용할 수 있습니다.

플러그형 아키텍처

버전 8에서 변경한 또 다른 사항은 라이브러리에서 HTTP 전송 레이어가 분할된 것입니다. 우리는 PHP에서 Elastic 제품에 연결하기 위한 PSR-18 클라이언트인 elastic-transport-php 라이브러리를 만들었습니다. 이 라이브러리는 elasticsearch-php 뿐만 아니라 enterprise-search-php와 같은 다른 프로젝트에서도 사용됩니다.

이 라이브러리는 플러그형 아키텍처를 기반으로 합니다. 즉, 다음 인터페이스의 특정 구현을 사용하도록 라이브러리를 구성할 수 있습니다.

elasticsearch-php 버전 8은 elastic-transport-php를 종속성으로 사용합니다. 즉, 사용자 정의 HTTP 라이브러리, 사용자 정의 노드 풀 또는 사용자 정의 로거를 사용하여 Elasticsearch에 연결할 수 있습니다.

PHP 애플리케이션에서 사용 가능한 PSR-18 및 PSR-7 라이브러리의 자동 검색을 수행하기 위해 HTTPlug 라이브러리를 사용하였습니다. 기본적으로, 애플리케이션에 HTTP 라이브러리가 설치되어 있지 않은 경우에는 Guzzle을 사용합니다.

예를 들어, 다음과 같이 Symfony HTTP 클라이언트를 사용할 수 있습니다.

use Symfony\Component\HttpClient\Psr18Client;
 
$client = ClientBuilder::create()
   ->setHttpClient(new Psr18Client)
   ->build();

또는 다음과 같이 Monolog 로거 라이브러리를 사용할 수 있습니다.

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
 
$logger = new Logger('name');
$logger->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));
$client = ClientBuilder::create()
   ->setLogger($logger)
   ->build();

클라이언트를 사용자 정의하는 방법에 대한 자세한 내용은 구성 페이지를 참조하세요.

Elastic Cloud에 연결

Elastic Cloud는 Elastic이 제공하는 PaaS 솔루션으로, Elastic Cloud에 연결하기 위해서는 Cloud IDAPI 키만 있으면 됩니다.

Cloud ID는 Elastic Cloud 대시보드의 내 배포 페이지에서 검색할 수 있으며, 보안 페이지 설정의 관리 섹션에서 API 키를 생성할 수 있습니다.

자세한 내용은 PHP 클라이언트 설명서의 연결 섹션을 읽어보시기 바랍니다.

Cloud IDAPI 키를 수집한 후, elasticsearch-php를 사용하여 다음과 같이 Elastic Cloud 인스턴스에 연결할 수 있습니다.

$client = ClientBuilder::create()
   ->setElasticCloudId('insert here the Cloud ID')
   ->setApiKey('insert here the API key')
   ->build();

보안 기본 제공

인프라에 Elasticsearch 8을 설치했다면, TLS(전송 계층 보안)이 활성화된 PHP 클라이언트를 사용할 수 있습니다. Elasticsearch 8은 기본적으로 보안을 제공하는데, 이는 클라이언트와 서버 간의 통신을 보호하기 위해 TLS를 사용한다는 것을 의미합니다.

Elasticsearch 8에 연결하도록 elasticsearch-php를 구성하려면, 인증 기관 파일(CA)이 있어야 합니다.

Elasticsearch는 다양한 방법으로 설치할 수 있으며, Docker를 사용하는 경우 다음 명령을 실행해야 합니다.

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.1.0

Docker 이미지가 설치되면, 다음과 같이 단일 노드 클러스터 구성을 사용하여 Elasticsearch를 실행할 수 있습니다.

docker network create elastic
docker run --name es01 --net elastic -p 9200:9200 -p 9300:9300 -it docker.elastic.co/elasticsearch/elasticsearch:8.1.0

이 명령은 elastic Docker 네트워크를 생성하고 포트 9200(기본값)을 사용하여 Elasticsearch를 시작합니다.

Docker 이미지를 실행하면, Elastic 사용자를 위한 비밀번호가 생성되어 단말기에 인쇄됩니다(단말기에서 스크롤을 조금 뒤로 돌려야 볼 수 있습니다). Elasticsearch에 연결하려면 이를 복사해야 합니다.

이제 Elasticsearch가 실행 중이므로, http_ca.crt 파일 인증서를 얻을 수 있습니다. 다음 명령을 사용하여 이를 Docker 인스턴스에서 복사합니다.

docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .

Elasticsearch를 시작하는 동안 복사된 http_ca.crt 인증서와 비밀번호가 있으면, 이를 사용해 다음과 같이 연결할 수 있습니다.

$client = ClientBuilder::create()
   ->setHosts(['https://localhost:9200'])
   ->setBasicAuthentication('elastic', 'password copied during ES start')
   ->setCABundle('path/to/http_ca.crt')
   ->build();

비동기 모드에서 클라이언트 사용

PHP 클라이언트는 각 엔드포인트에 대해 비동기 호출을 실행할 수 있는 가능성을 제공했습니다. 버전 7에서는, 다음과 같이 엔드포인트에 대한 매개 변수로 전달된 클라이언트 키에 특별한 future => lazy 값을 지정해야 했습니다.

$params = [
   'index' => 'my-index',
   'client' => [
      'future' => 'lazy'
   ],
   'body' => [
       'foo' => 'bar'
   ]
];
$response = $client->index($params);

이전 예제는 비동기 HTTP 호출을 사용하여 Elasticsearch의 {"foo": "bar"} 문서를 색인합니다. $response는 실제 응답이 아닌 future입니다.

future는 미래 계산을 나타내며 자리 표시자와 같은 역할을 합니다. 코드 주변에 future를 일반 객체처럼 전달할 수 있습니다. 결과 값이 필요할 때, future를 해결할 수 있습니다. future가 이미 해결된 경우(다른 활동으로 인해), 값을 즉시 사용할 수 있습니다. future가 아직 해결되지 않은 경우, 해당 값을 사용할 수 있을 때까지 해결이 차단됩니다(예: API 호출이 완료된 후).

버전 7에서는 future가 실제로 RingPHP 프로젝트의 Promise입니다. 버전 8에서는, 비동기식을 사용하려면 HTTP 클라이언트를 위한 특정 어댑터를 설치해야 합니다. 예를 들어, Guzzle 7(elasticsearch-php의 기본 HTTP 라이브러리)을 사용하는 경우 다음과 같이 php-http/guzzle7-adapter를 설치해야 합니다.

composer require php-http/guzzle7-adapter

비동기 호출을 사용하여 엔드포인트를 실행하려면, 다음과 같이 Client::setAsync(true) 함수를 사용하여 이를 활성화해야 합니다.

$client->setAsync(true);
$params = [
   'index' => 'my-index',
   'body' => [
       'foo' => 'bar'
   ]
];
$response = $client->index($params);

다음 엔드포인트에 대해 비동기를 사용하지 않도록 설정하려면, 비동기를 다시 false로 setAsync해야 합니다.

비동기 호출의 응답은 HTTPlug 라이브러리의 Promise 객체입니다. 이 Promise는 Promises/A+ 표준을 따릅니다. Promise는 비동기 작업의 최종 결과를 나타냅니다.

응답을 받으려면, 해당 응답이 도착할 때까지 기다려야 합니다. 이렇게 하면 다음과 같이 응답을 기다리는 실행이 차단됩니다.

$response = $client->index($params);
$response = $response->wait();
printf("Body response:\n%s\n", $response->asString());

Promise와 상호 작용하는 주된 방법은 Promise의 최종 가치나 Promise를 이행할 수 없는 이유를 받기 위해 콜백을 등록하는 그 당시의 방법을 통해서입니다.

$response = $client->index($params);
$response->then(
   // The success callback
   function (ResponseInterface $response) {
       // $response is Elastic\Elasticsearch\Response\Elasticsearch
       printf("Body response:\n%s\n", $response->asString());
   },
   // The failure callback
   function (\Exception $exception) {
       echo 'Houston, we have a problem';
       throw $exception;
   }
);
$response = $response->wait();

위 예제에서 실행 호출을 해결하려면 마지막 $response->wait()가 필요합니다.

코드 및 메모리 사용량 감소

Elasticsearch의 새로운 PHP 클라이언트는 버전 7에 비해 코드 사용량이 적습니다. 특히, elasticsearch-php 버전 8은 코드 6,522줄 + elastic-transport-php의 코드 1,025줄, 이렇게 총 7,547줄로 구성되어 있으며, 버전 7의 경우, 코드 20,715줄로 구성되어 있어 새로운 버전 8은 이전의 약 3분의 1 크기입니다.

메모리 사용량과 관련하여, elasticsearch-php 버전 8은 API 네임스페이스 로딩을 최적화하기 위해 lazy loading 메커니즘을 구현합니다. 이것은 400개 이상의 엔드포인트 중 일부만 사용하는 경우, 모든 사양을 메모리에 로드하지 않는다는 것을 의미합니다.

결론

Elasticsearch 8은 몇 가지 흥미로운 개선점을 제공합니다. 새로운 아키텍처 및 버전 7과의 하위 호환성을 유지할 수 있는 기능에서부터 기본 보안 설정과 비동기 모드의 장점에 이르기까지, 가능성은 그 어느 때보다도무궁무진합니다.

Elastic Cloud를 시작하는 가장 좋은 방법. 지금 Elastic Cloud 무료 체험판을 시작해 보세요!