Presentación del nuevo cliente de PHP para Elasticsearch 8

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

El nuevo cliente de PHP para Elasticsearch 8 se volvió a escribir desde cero. Además de adoptar los estándares PSR, también rediseñamos la arquitectura y movimos hacia afuera la capa de transporte HTTP. Ahora también hay disponible un sistema conectable, gracias a la biblioteca HTTPlug.

Continúa leyendo para explorar lo siguiente:

  • La nueva arquitectura y características del cliente de PHP
  • Cómo consumir los endpoints y gestionar los errores usando el estándarPSR-7 para mensajes HTTP
  • Cómo interactuar con Elasticsearch usando un enfoque asíncrono

El cliente antiguo

La biblioteca elasticsearch-php es el cliente oficial para la programación de Elasticsearch con PHP. Esta biblioteca expone todos los más de 400 endpoints de Elasticsearch usando una clase Client principal. En la versión 7 de esta biblioteca, todos los endpoints se exponen usando funciones; por ejemplo, la API de índice está mapeada al método Client::index().

Estas funciones devuelven una matriz asociativa que es la deserialización de la respuesta HTTP de Elasticsearch. Por lo general, esta respuesta está representada por un mensaje de JSON. Este mensaje se convierte en una matriz usando la función json_decode() de PHP.

En caso de errores, el cliente arroja una excepción que depende del problema. Por ejemplo, si la respuesta HTTP es 404, el cliente arroja Missing404Exception. Si quieres recuperar la respuesta HTTP en sí, debes obtener la última respuesta del cliente usando el código siguiente:

$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

La solicitud y respuesta HTTP se recuperan de la capa de transporte (una propiedad del Cliente) con dos métodos: getLastConnection() y getLastRequestInfo().

Este código no ofrece una buena experiencia para desarrolladores porque las claves de la matriz asociativa $response son bastantes, y provienen del uso de extensiones cURL de PHP.

El cliente nuevo

Creamos el nuevo elasticsearch-php 8 desde cero por varios motivos: experiencia para desarrolladores, nuevos estándares de PHP, una arquitectura más abierta y rendimiento.

Con alrededor de 70 millones de instalaciones, no queríamos que hubiera muchas interrupciones de compatibilidad con versiones anteriores en la versión 8. En nuestro enfoque de compatibilidad con versiones anteriores, ofrecemos las mismas API de la versión 7. Eso significa que puedes conectarte a Elasticsearch usando el mismo código y ejecutar una llamada de endpoint como siempre. La diferencia está en la respuesta. En la versión 8, la respuesta es un objeto de la respuesta de Elasticsearch que implementa la interfaz de respuesta PSR-7 y la interfaz ArrayAccess de PHP.

Un momento, ¿eso no es una interrupción de compatibilidad con versiones anteriores? Afortunadamente, implementamos la interfaz ArrayAccess y puedes continuar consumiendo la respuesta como una matriz, de este modo:

Detecta las diferencias: el espacio de nombre se modificó. Presentamos el espacio de nombre raíz Elastic. El otro código parece ser igual, pero hay un cambio interno importante.

Como mencionamos, $response en la versión 8 es un objeto, mientras que en la versión 7 es una matriz asociativa. Si quieres exactamente el mismo comportamiento de la versión 7, puedes serializar la respuesta como una matriz usando la función $response->asArray().

También ofrecemos las funciones asObject(), asString() y asBool() para serializar el cuerpo como objeto de la clase estándar de PHP (stdClass), como cadena o booleano (verdadero en respuesta 2xx, falso de lo contrario).

Por ejemplo, puedes consumir el endpoint info() anterior de la siguiente manera:

$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 puede acceder al cuerpo de la respuesta como un objeto que implementa el método mágico _get() de PHP.

Si deseas leer la respuesta HTTP, no necesitas recuperar el último mensaje del objeto Client, puedes simplemente acceder al mensaje PSR-7 en $response en sí, de la siguiente manera:

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

Esta es una gran ventaja, en especial si estás trabajando con el enfoque asíncrono. De hecho, no puedes recuperar la última respuesta del cliente si estás usando la programación asíncrona. No hay garantías de que la última respuesta sea lo que estás buscando (veremos más detalles sobre las operaciones asíncronas más adelante en este artículo).

Parámetros de endpoint para autocompletado

Agregamos una capacidad de autocompletado en elasticsearch-php versión 8 usando las matrices de tipo objeto del proyecto de Psalm. Psalm es una herramienta de análisis estática que permite a los desarrolladores decorar el código usando el atributo phpDoc especial. Uno de estos atributos es @psalm-type, que posibilita especificar los tipos clave de una matriz asociativa. Aplicamos el tipo Psalm usando el @param de phpDoc estándar. Cada endpoint de cliente de PHP tiene un parámetro de entrada que es la matriz $params. Por ejemplo, aquí se está reportada la sección del endpoint 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 = [])

Todos los parámetros están especificados con un nombre (índice), incluido el tipo (cadena) y un comentario que describe el parámetro (el nombre del índice). Los parámetros requeridos se especifican con una nota REQUIRED.

Puedes tener autocompletado en tu IDE con la anotación anterior. Por ejemplo, con PhpStorm, puedes instalar el plugin gratuito deep-assoc-completiona fin de habilitar el autocompletado de la matriz asociativa de PHP con el atributo @psalm-type.

Video thumbnail

Deep-assoc-completion también está disponible para Visual Studio Code, incluso si esta versión aún está en desarrollo.

Arquitectura conectable

Otro cambio que hicimos en la versión 8 fue separar la capa de transporte HTTP de la biblioteca. Creamos la biblioteca elastic-transport-php que es un cliente PSR-18 para conectar los productos de Elastic en PHP. No solo elasticsearch-php consume esta biblioteca, sino que también se consume desde otros proyectos como enterprise-search-php.

Esta biblioteca se basa en una arquitectura conectable, lo que significa que puedes configurarla para usar una implementación específica de las interfaces siguientes:

La versión 8 de elasticsearch-php usa elastic-transport-php como dependencia. Esto significa que puedes conectarte a Elasticsearch usando una biblioteca HTTP personalizada, un grupo de nodos personalizado o un logger personalizado.

Usamos la biblioteca HTTPlug para realizar un autodescubrimiento de la biblioteca PSR-18 y PSR-7 disponible en una aplicación PHP. De forma predeterminada, si la aplicación no tiene instalada una biblioteca HTTP, usamos Guzzle.

Por ejemplo, puedes usar el cliente HTTP Symfony de la siguiente manera:

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

Como alternativa, puedes usar la biblioteca del logger Monolog de la siguiente manera:

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();

Para obtener más información sobre cómo personalizar Client, echa un vistazo a la página Configuration (Configuración).

Conectarse a Elastic Cloud

Elastic Cloud es la solución PaaS que ofrece Elastic. Para conectarte a Elastic Cloud, solo necesitas la Cloud ID y la clave de API.

La Cloud ID se puede recuperar en la página My deployment (Mi despliegue) de tu dashboard de Elastic Cloud. La clave de API se puede generar desde la sección Management (Gestión) en la configuración de la página Security (Seguridad).

Puedes leer la sección Connecting (Conexión) de la documentación del cliente PHP para obtener más información.

Una vez que hayas recopilado la Cloud ID y la clave de API, puedes usar elasticsearch-php para conectarte a tu instancia de Elastic Cloud, de la siguiente manera:

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

Seguridad predeterminada

Si instalaste Elasticsearch 8 en tu infraestructura, puedes usar el cliente PHP con TLS (seguridad de capa de transporte) habilitada. Elasticsearch 8 ofrece seguridad predeterminada, lo que significa que usa TLS para proteger la comunicación entre el cliente y el servidor.

A fin de configurar elasticsearch-php para que se conecte a Elasticsearch 8, debes tener el archivo de autoridad de certificados (CA).

Puedes instalar Elasticsearch de distintas formas. Si usas Docker, debes ejecutar el comando siguiente:

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

Una vez instalada la imagen de Docker, puedes ejecutar Elasticsearch usando una configuración de cluster de nodo único, de este modo:

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

Este comando crea una red de Docker elástica e inicia Elasticsearch con el puerto 9200 (predeterminado).

Cuando ejecutas la imagen de Docker, se genera una contraseña para el usuario de Elastic y se imprime en la terminal (es posible que debas desplazarte un poco hacia atrás en la terminal para verlo). Debes copiarlo para conectarte a Elasticsearch.

Ahora que Elasticsearch se está ejecutando, podemos obtener el certificado de archivo http_ca.crt. Cópialo desde la instancia de Docker usando el comando siguiente:

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

Una vez que tengamos el certificado http_ca.crt y la contraseña, que copiamos al iniciar Elasticsearch, podemos usarla para conectarnos de la siguiente manera:

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

Usar el cliente en modo asíncrono

El cliente PHP ofrecía la posibilidad de ejecutar llamadas asíncronas para cada endpoint. Con la versión 7, debes especificar un valor future => lazy especial en la clave client pasada como parámetro para el endpoint, de la siguiente manera:

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

En el ejemplo anterior se indexa el documento { "foo": "bar" } en Elasticsearch con una llamada HTTP asíncrona. $response es future, en lugar de la respuesta real.

Future representa un cálculo futuro y actúa como marcador de posición. Puedes pasar future en tu código como un objeto normal. Cuando necesites los valores de resultado, puedes resolver future. Si future ya se resolvió (debido a otra actividad), los valores estarán disponibles de forma inmediata. Si future no se resolvió aún, la resolución se bloquea hasta que estos valores estén disponibles (por ejemplo, luego de que la llamada de API se complete).

En la versión 7, future es en realidad un Promise del proyecto RingPHP. En la versión 8, si deseas usar el enfoque asíncrono, debes instalar el adaptador específico para tu cliente HTTP. Por ejemplo, si usas Guzzle 7 (la biblioteca HTTP predeterminada de elasticsearch-php), debes instalar php-http/guzzle7-adapter de la siguiente manera:

composer require php-http/guzzle7-adapter

Para ejecutar un endpoint usando una llamada asíncrona, debes habilitarlo con la función Client::setAsync(true), de la siguiente manera:

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

Si deseas deshabilitar el enfoque asíncrono en el endpoint siguiente, debes configurar setAsync como false nuevamente.

La respuesta de una llamada asíncrona es un objeto Promise de la biblioteca HTTPlug. Este Promise sigue el estándar Promises/A+. Promise representa el resultado eventual de una operación asíncrona.

Para obtener la respuesta, debes esperar a que dicha respuesta llegue. Esto bloqueará la ejecución que espera la respuesta, de la siguiente manera:

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

La forma principal de interactuar con promise es a través de su método then, el cual registra las devoluciones de llamada para recibir el valor eventual de promise o el motivo por el cual no puede cumplirse.

$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();

La última $response->wait() se necesita para resolver la llamada de ejecución en el ejemplo anterior.

Menos código y uso de memoria

El nuevo cliente PHP de Elasticsearch usa menos código en comparación con la versión 7. En particular, la versión 8 de elasticsearch-php se compone de 6522 líneas de código + 1025 líneas de código de elastic-transport-php, para sumar un total de 7547 líneas. En la versión 7, teníamos 20 715 líneas de código, por lo que la nueva versión 8 es de aproximadamente un tercio del tamaño de la anterior.

Respecto al uso de memoria, la versión 8 de elasticsearch-php implementa un mecanismo de carga lazy para optimizar la carga del espacio de nombre de la API. Esto significa que si estás usando un subconjunto de todos los más de 400 endpoints, no cargarás todas las especificaciones en la memoria.

Resumen

Elasticsearch 8 incluye algunas mejoras emocionantes. Desde la nueva arquitectura y la capacidad de preservar la compatibilidad con la versión 7 anterior, hasta la configuración de seguridad predeterminada y las ventajas del modo asíncrono, las posibilidades son más infinitas que nunca.

La mejor forma de dar los primeros pasos con Elastic Cloud. Comienza tu prueba gratuita de Elastic Cloud hoy.