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 编程的官方客户端。这个库使用一个主客户端类公开了 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()

这段代码不能提供良好的开发人员体验,因为关联数组 $response 的键非常多,它们源于对 PHP 的 cURL 扩展的使用。

新客户端

促使我们从头开始构建新 elasticsearch-php 8 的原因有很多:开发人员体验、新的 PHP 标准、更开放的架构和性能。

鉴于安装数量大约有 7000 万,我们不希望在版本 8 中出现太多 BC 中断。我们使用了向后兼容的方法,提供了与版本 7 相同的 API。这样您就可以使用相同的代码连接到 Elasticsearch,像往常一样执行终端调用。不同之处在于响应。在版本 8 中,响应是 Elasticsearch 响应的一个对象,它实现了 PSR-7 响应接口和 PHP 的 ArrayAccess 接口。

等等,这不是重大 BC 中断吗?幸运的是,我们实现了 ArrayAccess 接口,您可以继续以数组的形式使用响应,如下所示:

看图找不同:命名空间发生了更改!我们推出了 Elastic 根命名空间。其他代码看起来是一样的,但仔细看,会发现有一个重大变化。

我们刚刚也提到了,版本 8 中的 $response 是一个对象,而在版本 7 中,它是一个关联数组。如果想要与版本 7 完全相同的行为,可以使用函数 $response->asArray() 将响应序列化为数组。

我们还提供 asObject()asString()asBool() 函数,用于将正文序列化为 PHP 标准类 (stdClass) 的对象、字符串或布尔值(如果是 2xx 响应,则为 true,否则为 false)。

例如,您可以使用前面的 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

这是一个很大的优势,特别是在使用异步操作时。实际上,如果使用异步编程,是无法从客户端检索最后一个响应的,因为最后一个响应并不一定就是您要寻找的响应(本文后面将详细介绍异步操作)。

自动补全的终端参数

我们在 elasticsearch-php 版本 8 中添加了一个自动补全功能,该功能用到了 Psalm项目的类对象数组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 关联数组自动补全功能。

Video thumbnail

即使这个版本仍在开发中,也可以在 Visual Studio Code 中使用 deep-assoc-completion。

可插拔架构

我们在版本 8 中做的另一个更改是将 HTTP 传输层从库中分离出来。我们创建了 elastic-transport-php 库,它是一个 PSR-18 客户端,用于在 PHP 中连接到 Elastic 产品。这个库不仅用于 elasticsearch-php,还用于 enterprise-search-php 等其他项目。

这个库基于一个可插拔架构,这意味着您可以将其配置为使用以下接口的特定实现:

elasticsearch-php 版本 8 使用 elastic-transport-php 作为依赖项。这意味着您可以使用定制 HTTP 库、定制节点池或定制记录器连接到 Elasticsearch。

我们使用 HTTPlug 库自动发现 PHP 应用程序中可用的 PSR-18 和 PSR-7 库。默认情况下,如果应用程序没有安装 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-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

该命令将创建一个 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);

如果要为下一个终端禁用异步,则需要再次将 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()

代码更少,内存占用率更低

与版本 7 相比,Elasticsearch 的新 PHP 客户端使用的代码更少。特别是,elasticsearch-php 版本 8 由 6,522 行代码 + 1,025 行 elastic-transport-php 代码组成,总共 7,547 行。在版本 7 中,我们有 20,715 行代码,因此新版本 8 的大小大约是前一个版本的三分之一。

关于内存占用,elasticsearch-php 版本 8 实现了一种延迟加载机制来优化 API 命名空间加载。这意味着,如果您只使用所有 400 多个终端中的一个子集,则不会在内存中加载所有规范。

总结

Elasticsearch 8 带来了一些激动人心的改进。从全新架构和保持与版本 7 向后兼容的能力,到默认提供安全保护的设置和异步模式的优势,从未像现在这样,拥有无限可能。

使用 Elastic Cloud 是最好的入门方式。立即开始免费试用 Elastic Cloud