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ライブラリは、PHPでElasticsearchをプログラミングするためのオフィシャルクライアントです。このライブラリは、メインのClientクラスを使用してElasticsearchの400以上のエンドポイントをすべて公開しています。このライブラリのバージョン7では、すべてのエンドポイントが関数を使用して公開されています。たとえば、インデックスAPIClient::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()という2つのメソッドでトランスポートレイヤー(Clientのプロパティ)から取得されます。

このコードは、開発者にとって快適なものではありません。PHPの拡張機能であるcURLを使用することにより、連想配列$responseのキー数が膨大になるからです。

新しいクライアント

Elasticが新しいelasticsearch-php 8をゼロから構築したのには、開発者エクスペリエンスの向上、新しいPHP標準、よりオープンなアーキテクチャー、パフォーマンスの強化といった、複数の理由があります。

7,000万のインストール数があるので、バージョン8ではBCブレークが多数ある事態を避けたいという思いもありました。そこで、バージョン7と同じAPIを提供する、後方互換性重視のアプローチを採用しました。つまり、同じコードを使用してElasticsearchに接続し、いつものようにエンドポイント呼び出しを実行できます。違いは応答にあります。バージョン8では、応答は、PHPのPSR-7応答インターフェースとArrayAccessインターフェースを実装したElasticsearch応答のオブジェクトです。

ちょっと待ってください。これは大きなBCブレークではないのでしょうか。幸いなことに、ArrayAccessインターフェースを実装したことで、以下のように引き続き応答を配列として使用できます。

異なる点は、名前空間です。Elasticルート名前空間が導入されています。他のコードは同じように見えますが、内側では大きな変化が起きています。

すでに述べたように、バージョン7では連想配列である$responseが、バージョン8ではオブジェクトになっています。バージョン7とまったく同じ挙動にするには、$response->asArray()関数を使用して応答を配列としてシリアル化します。

また、本文をPHPの標準クラス(stdClass)のオブジェクトとしてシリアル化したり、文字列またはブール型(応答が2xx場合はtrue、その他の場合はfalse)としてシリアル化したりするための関数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応答を読むには、Clientオブジェクトから最後のメッセージを復元する必要はなく、以下のように$response自体に含まれるPSR-7メッセージにアクセスするだけです。

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

これは非同期での作業では特に、大きなメリットになります。実際、非同期プログラミングを使用している場合は、クライアントから最後の応答を取得することはできません。最後の応答が探しているものである保証はありません(非同期での作業についてはこの記事で後ほど詳しく取り上げます)。

オートコンプリートのためのエンドポイントパラメーター

elasticsearch-phpのバージョン8では、PsalmプロジェクトのObject-like arraysを使用するオートコンプリート機能が追加されています。Psalmとは、開発者が特殊なphpDoc属性を使用してコードの品質を向上できる、静的分析ツールです。その属性の1つに、連想配列のキータイプを指定できるようにする、@psalm-typeがあります。Psalmタイプは、標準的なphpDoc @paramを使用して適用されています。それぞれの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連想配列オートコンプリートを有効にできます。

Video thumbnail

deep-assoc-completionは、このバージョンがまだ開発段階であっても、Visual Studio Codeでも使用できます。

プラグイン型アーキテクチャー

バージョン8でのもう1つの変更点が、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();

クライアントをカスタマイズする方法について詳しくは、「Configuration」のページをご覧ください。

Elastic Cloudへの接続

Elastic Cloudは、Elasticが提供するPaaSソリューションです。Elastic Cloudに接続するために必要なのは、Cloud IDAPIキーだけです。

Cloud IDは、Elastic Cloudダッシュボードの[My deployment](マイデプロイ)ページで取得できます。APIキーは、[Security](セキュリティ)ページ設定の[Management](管理)セクションで生成できます。

詳細については、PHPクライアントドキュメントの「Connecting」セクションを参照してください。

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-phpをElasticsearch 8に接続するように構成するには、認証局(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

このコマンドにより、ポート9200(デフォルト)を使用してElastic Dockerネットワークが作成され、Elasticsearchが起動します。

Dockerイメージを実行すると、Elasticユーザー用のパスワードが生成され、ターミナルに出力されます(少し上にスクロールしないとパスワードが見えない場合があります)。Elasticsearchに接続するには、これをコピーする必要があります。

Elasticsearchが起動したら、http_ca.crtファイル証明書を入手できます。以下のコマンドを使用して、これをDockerインスタンスからコピーします。

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

http_ca.crt証明書とElasticsearch起動時にコピーしたパスワードが揃ったら、これを使用して以下の要領で接続します。

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

非同期モードでクライアントを使用する

PHPクライアントでは、それぞれのエンドポイントを非同期的に呼び出せます。バージョン7では、以下のようにエンドポイントにパラメーターとして渡すclientキー内で、特殊な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);

次のエンドポイントで非同期を無効にする場合には、改めてsetAsyncをfalseにする必要があります。

非同期呼び出しの応答は、HTTPlugライブラリのPromiseオブジェクトです。このPromiseは、Promises/A+標準に準拠しています。Promiseは非同期操作の最終的な結果を表します。

応答を取得するには、応答が到達するまで待つ必要があります。これにより、以下のように応答待ちの実行がブロックされます。

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

Promiseは、主にthenメソッドを通じて操作します。これは、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のサイズは、前バージョンの1/3ほどになっています。

メモリー使用量については、elasticsearch-phpバージョン8には遅延読み込みメカニズムが実装されていて、API名前空間の読み込みが最適化されます。そのため、全部で400を超えるエンドポイントのうち、ほんの一部しか使用していない場合には、すべての仕様がメモリーに読み込まれることはありません。

まとめ

Elasticsearch 8は、いくつかの魅力的な改善が施されています。新しいアーキテクチャーとバージョン7との後方互換性の維持から、デフォルトのセキュリティ設定と非同期モードがもたらすメリットまで、かつてないほどの可能性の広がりを感じさせます。

Elastic Cloudと組み合わせて利用を開始することを、お勧めします。今すぐ、Elastic Cloudの無料トライアルを始めましょう。