Présentation du nouveau client PHP pour Elasticsearch 8

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

Le nouveau client PHP pour Elasticsearch 8 est a été totalement réécrit. En plus des normes PSR que nous avons adoptées, nous avons aussi repensé l'architecture et déplacé la couche de transport HTTP à l'extérieur. Un système modulaire est également disponible, grâce à la bibliothèque HTTPlug.

Dans cet article, nous verrons :

  • la nouvelle architecture et les fonctionnalités du client PHP ;
  • comment utiliser les points de terminaison et gérer les erreurs avec la norme PSR-7 pour les messages HTTP ;
  • comment interagir avec Elasticsearch à l'aide d'une approche asynchrone.

L'ancien client

La bibliothèque elasticsearch-php est le client officiel pour la programmation d'Elasticsearch avec PHP. Cette bibliothèque présente l'ensemble des points de terminaison d'Elasticsearch, qui s'élèvent à plus de 400 et dont la classe principale est Client. Dans la version 7 de cette bibliothèque, tous les points de terminaison sont présentés à l'aide de fonctions. Par exemple, l'API des index est mappée à la méthode Client::index().

Ces fonctions renvoient un tableau associatif, qui correspond à la désérialisation de la réponse HTTP d'Elasticsearch. En général, cette réponse est représentée par un message JSON. Ce message est converti en tableau à l'aide de la fonction json_decode() de PHP.

En cas d'erreurs, le client émet une exception qui dépend du problème. Par exemple, si la réponse HTTP est 404, le client émet une exception Missing404Exception. Si vous souhaitez récupérer la réponse HTTP en tant que telle, vous devez obtenir la dernière réponse du client à l'aide du code suivant :

$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 requête HTTP et la réponse sont récupérées à partir de la couche de transport (propriété du client) grâce à deux méthodes : getLastConnection() and getLastRequestInfo().

Ce code n'offre pas une expérience optimale aux développeurs, car les clés du tableau associatif $response sont nombreuses, du fait de l'utilisation des extensions cURL de PHP.

Le nouveau client

Nous avons conçu le nouveau client elasticsearch-php 8 de toutes pièces pour de nombreuses raisons : l'expérience des développeurs, les nouvelles normes PHP, une architecture plus ouverte et les performances.

Avec près de 70 millions d'installations, nous ne voulions pas avoir des problèmes de rétrocompatibilité pour la version 8. Nous avons donc opté pour une approche offrant les mêmes API que la version 7 pour garantir la rétrocompatibilité. Cela signifie que vous pouvez vous connecter à Elasticsearch avec le même code et appeler un point de terminaison comme vous le faites habituellement. La seule différence, c'est la réponse. Dans la version 8, la réponse est un objet de la réponse d'Elasticsearch qui met en œuvre l'interface de réponse PSR-7 et l'interface ArrayAccess de PHP.

Attendez... est-ce qu'il ne s'agit pas là d'une modification radicale ? Heureusement, nous avons mis en œuvre l'interface ArrayAccess et vous pouvez continuer à utiliser la réponse sous forme de tableau, comme suit :

Si l'on joue au jeu des différences, l'espace de nom a changé ! Nous avons introduit l'espace de nom racine Elastic. L'autre code semble identique, mais en coulisses, il y a un grand changement.

Comme nous l'avons dit, dans la version 8, $response est un objet, alors que dans la version 7, il s'agit d'un tableau associatif. Si vous souhaitez obtenir exactement le même comportement qu'avec la version 7, vous pouvez sérialiser la réponse sous forme de tableau à l'aide de la fonction $response->asArray().

Nous proposons également les fonctions asObject(), asString() et asBool() pour sérialiser le corps sous forme d'objet dans la classe standard de PHP (stdClass), sous forme de chaîne ou sous forme booléenne (vrai pour la réponse 2xx, faux dans les autres cas).

Par exemple, vous pouvez utiliser le point de terminaison info() précédent comme suit :

$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 est capable d'accéder au corps de la réponse sous forme d'objet en appliquant la méthode magique _get() de PHP.

Si vous souhaitez lire la réponse HTTP, vous n'avez pas besoin de récupérer le dernier message de l'objet Client. Il vous suffit d'accéder au message PSR-7 directement dans $response, comme suit :

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

C'est un véritable avantage, en particulier si vous travaillez avec une approche asynchrone. Pour tout dire, si vous utilisez une programmation asynchrone, vous ne pouvez pas récupérer la dernière réponse du client. D'ailleurs, la dernière réponse n'est pas nécessairement celle que vous recherchez (nous verrons les opérations asynchrones plus loin dans cet article).

Paramètres des points de terminaison pour le remplissage automatique

Nous avons ajouté la possibilité d'utiliser le remplissage automatique dans elasticsearch-php version 8 à l'aide des tableaux de type Objet du projet Psalm. Psalm est un outil d'analyse statique qui permet aux développeurs de décorer le code à l'aide des attributs spéciaux phpDoc. L'un de ces attributs est @psalm-type, qui permet de spécifier les types de clés d'un tableau associatif. Nous avons appliqué le type Psalm à l'aide de l'attribut phpDoc standard @param. Chaque point de terminaison du client PHP a un paramètre d'entrée qui correspond au tableau $params. L'exemple ci-dessous montre la section du point de terminaison 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 = [])

Chaque paramètre est indiqué avec un nom (index), son type (chaîne) et un commentaire pour le décrire (nom de l'index). Les paramètres requis sont indiqués par la mention REQUIRED.

Vous pouvez bénéficier du remplissage automatique dans votre environnement de développement en utilisant la notation précédente. Par exemple, avec PhpStorm, vous pouvez installer le plug-in gratuit deep-assoc-completion pour permettre le remplissage automatique du tableau associatif PHP avec l'attribut @psalm-type.

Video thumbnail

Le plug-in deep-assoc-completion est également disponible pour Visual Studio Code, même si cette version est en cours de développement.

Architecture modulable

Un autre changement que nous avons apporté dans la version 8 est la séparation entre la couche de transport HTTP et la bibliothèque. Nous avons créé la bibliothèque elastic-transport-php, qui est un client PSR-18 pour la connexion aux produits Elastic en PHP. Cette bibliothèque est utilisée non seulement par elasticsearch-php, mais aussi par d'autres projets comme enterprise-search-php.

Cette bibliothèque se base sur une architecture modulable, ce qui signifie que vous pouvez l'utiliser pour mettre en œuvre les interfaces suivantes :

elasticsearch-php version 8 utilise elastic-transport-php comme dépendance. Cela signifie que vous pouvez vous connecter à Elasticsearch à l'aide d'une bibliothèque HTTP personnalisée, un pool de nœuds ou un logger personnalisé.

Nous avons utilisé la bibliothèque HTTPlug pour procéder à la découverte automatique de la bibliothèque PSR-18 et PSR-7 disponible dans une application PHP. Par défaut, si l'application n'a aucune bibliothèque installée, nous utilisons Guzzle.

Par exemple, vous pouvez utiliser le client HTTP Symfony comme suit :

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

Si vous préférez, vous pouvez aussi utiliser la bibliothèque de logger Monolog comme suit :

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

Pour en savoir plus sur les possibilités de personnalisation du client, consultez la page de configuration.

Connexion à Elastic Cloud

Elastic Cloud est la solution PaaS que propose Elastic. Pour vous connecter à Elastic Cloud, vous avez besoin uniquement de l'ID du cloud et de la clé d'API.

L'ID du cloud peut être récupéré sur la page My deployment (Mon déploiement) de votre tableau de bord Elastic Cloud. La clé d'API peut être générée à partir de la section Management (Gestion) dans les paramètres de la page Security (Sécurité).

Pour en savoir plus, n'hésitez pas à consulter la section relative à la connexion de la documentation sur le client PHP.

Une fois que vous avez récupéré l'ID du cloud et la clé d'API, vous pouvez utiliser elasticsearch-php pour vous connecter à votre instance Elastic Cloud, comme suit :

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

La sécurité par défaut

Si vous avez installé Elasticsearch 8 dans votre infrastructure, vous pouvez utiliser le client PHP avec le protocole TLS (sécurité de la couche de transport) activé. Elasticsearch 8 applique la sécurité par défaut, ce qui signifie que le protocole TLS est utilisé pour protéger la communication entre le client et le serveur.

Pour configurer la connexion d'elasticsearch-php à Elasticsearch 8, vous devez disposer du fichier de l'autorité de certification (AC).

Pour installer Elasticsearch, plusieurs possibilités s'offrent à vous. Si vous utilisez Docker, vous devez exécuter la commande suivante :

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

Une fois l'image Docker installée, vous pouvez exécuter Elasticsearch à l'aide d'une configuration de cluster à un seul nœud, comme suit :

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

Cette commande entraîne la création d'un réseau Docker et exécute Elasticsearch sur le port 9200 (par défaut).

Lorsque vous exécutez l'image Docker, un mot de passe est généré pour l'utilisateur Elastic et est indiqué sur le terminal (vous devrez peut-être remonter un peu dans le terminal pour le voir). Copiez ce mot de passe pour vous connecter à Elasticsearch.

Maintenant qu'Elasticsearch est en cours d'exécution, nous pouvons obtenir le certificat http_ca.crt. Copiez-le à partir de l'instance Docker à l'aide de la commande suivante :

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

Une fois que nous avons le certificat http_ca.crt et le mot de passe (copié lors du démarrage d'Elasticsearch) en notre possession, nous pouvons nous en servir pour nous connecter, comme suit :

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

Utilisation du client en mode asynchrone

Le client PHP offre la possibilité d'exécuter des appels asynchrones pour chaque point de terminaison. Avec la version 7, vous aviez besoin d'indiquer une valeur future => lazy spéciale dans la clé client transmise en tant que paramètre pour le point de terminaison, comme suit :

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

Dans l'exemple précédent, le document { "foo": "bar" } est indexé dans Elasticsearch à l'aide d'un appel HTTP asynchrone. $response renvoie une valeur future, plutôt que la réponse réelle.

Quand on parle de valeur "future", on parle d'un calcul à venir qui agit comme un espace réservé. Vous pouvez faire passer une valeur future dans votre code comme un objet normal. Lorsque vous souhaitez obtenir les valeurs réelles des résultats, vous pouvez résoudre cette valeur future. Si la valeur future a déjà été résolue (en raison d'une autre activité), les valeurs réelles sont immédiatement disponibles. Dans le cas contraire, la résolution est bloquée jusqu'à ce que ces valeurs deviennent disponibles (par exemple, une fois l'appel de l'API terminé).

Dans la version 7, la valeur future correspond à un objet "Promise" (ou promesse) du projet RingPHP. Dans la version 8, si vous souhaitez utiliser une approche asynchrone, vous devez installer un adaptateur spécifique pour votre client HTTP. Par exemple, si vous utilisez Guzzle 7 (bibliothèque HTTP par défaut pour elasticsearch-php), vous devez installer php-http/guzzle7-adapter, comme suit :

composer require php-http/guzzle7-adapter

Pour exécuter un point de terminaison à l'aide d'un appel asynchrone, vous devez l'activer avec la fonction Client::setAsync(true), comme suit :

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

Si vous souhaitez désactiver l'appel asynchrone pour le point de terminaison suivant, vous devez définir setAsync de nouveau sur "false".

La réponse d'un appel asynchrone est un objet Promise (ou promesse) de la bibliothèque HTTPlug. Cet objet se conforme à la norme Promises/A+. Une promesse représente le résultat final d'une opération asynchrone.

Pour obtenir la réponse, vous devez attendre qu'elle arrive. Cela bloquera l'exécution en attendant la réponse, comme suit :

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

Pour interagir avec une promesse, la première possibilité est de passer par la méthode then, qui enregistre les rappels pour recevoir soit la valeur finale d'une promesse, soit la raison pour laquelle cette promesse ne peut pas être satisfaite.

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

Dans l'exemple ci-dessus, l'élément $response->wait() est nécessaire pour résoudre l'appel de l'exécution.

Moins de code et moins de mémoire utilisée

Le nouveau client PHP d'Elasticsearch utilise moins de code par rapport à la version 7. En effet, elasticsearch-php version 8 se compose de 6 522 lignes de code + 1 025 lignes d'elastic-transport-php, pour un total de 7 547 lignes. Dans la version 7, nous avions 20 715 lignes de code. La version 8 fait donc un tiers de la taille de la version précédente.

En ce qui concerne la mémoire utilisée, elasticsearch-php version 8 met en œuvre un mécanisme de charge différée (lazy) pour optimiser le chargement des espaces de noms d'API. Cela signifie que, sur les quelque 400 points de terminaison, si vous n'en utilisez uniquement qu'un sous-ensemble, les spécifications ne seront pas toutes chargées dans la mémoire.

Conclusion

Elasticsearch 8 propose des améliorations vraiment intéressantes. Depuis la nouvelle architecture et la possibilité de préserver la rétrocompatibilité avec la version 7 jusqu'aux paramètres de sécurité par défaut et les avantages du mode asynchrone, les possibilités sont infinies.

Le mieux pour vous lancer, c'est d'utiliser Elastic Cloud. Commencez un essai gratuit d'Elastic Cloud dès aujourd'hui !