Vorstellung des neuen PHP-Clients für Elasticsearch 8

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

Der neue PHP-Client für Elasticsearch 8 wurde komplett neu entwickelt. Wir haben die PSR-Standards übernommen, die Architektur umgestaltet und die HTTP-Transportschicht nach außen verlagert. Dank der Bibliothek HTTPlug ist das System jetzt auch modular aufgebaut.

Im Folgenden gehen wir auf folgende Themen und Fragen ein:

  • Die neue Architektur und die neuen Features des PHP-Clients
  • Wie hilft der Standard PSR-7 für HTTP-Nachrichten dabei, die Endpoints zu nutzen und Fehler zu verwalten?
  • Wie lässt sich der asynchrone Modus für die Interaktion mit Elasticsearch nutzen?

Der alte Client

Der offizielle Client für die Programmierung von Elastic mit PHP ist die Bibliothek elasticsearch-php. Diese Bibliothek ermöglicht über eine Client-Hauptklasse den Zugriff auf die mehr als 400 Endpoints von Elasticsearch. In Version 7 dieser Bibliothek wird der Zugriff auf alle Endpoints durch Funktionen ermöglicht. So wird beispielsweise die index-API über die Methode Client::index() abgebildet.

Diese Funktionen geben ein assoziatives Array zurück, das die Deserialisierung der HTTP-Antwort von Elasticsearch darstellt. In der Regel wird diese Antwort in Form einer JSON-Nachricht dargestellt. Diese Nachricht wird mit der PHP-Funktion json_decode() in ein Array umgewandelt.

Bei Fehlern gibt der Client eine Ausnahme aus. Um welche es sich dabei handelt, richtet sich nach dem Problem. Wenn z. B. die HTTP-Antwort „404“ lautet, gibt der Client Missing404Exception aus. Wenn Sie die eigentliche HTTP-Antwort abrufen möchten, müssen Sie die letzte Antwort vom Client abrufen:

$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

Die HTTP-Anfrage und -Antwort werden aus der Transportschicht abgerufen (ein Client-Property). Dabei kommen die folgenden Methoden zum Einsatz: getLastConnection() und getLastRequestInfo().

Dieser Code ist nicht sehr entwicklerfreundlich, da die Schlüssel des assoziativen Arrays $response aufgrund der Nutzung der cURL-Erweiterungen von PHP ziemlich zahlreich sind.

Der neue Client

Wir haben den neuen PHP-Client für Elasticsearch 8 von Grund auf neu entwickelt und dafür gab es viele Gründe: die Developer Experience, neue PHP-Standards, eine offenere Architektur und die Performance.

Angesichts von etwa 70 Millionen Installationen wollten wir bei Version 8 Probleme aufgrund fehlender Abwärtskompatibilität weitestgehend vermeiden. Zu diesem Zweck bieten wir dieselben APIs wie bei Version 7. Das bedeutet, dass Sie für die Verbindung zu Elasticsearch denselben Code verwenden und Endpoints auf die gleiche Art und Weise wie bisher aufrufen können. Was anders geworden ist, ist die Art der Antwort. In Version 8 ist die Antwort ein Objekt der Elasticsearch-Antwort, die die PSR‑7-Antwort-Schnittstelle und die ArrayAccess-Schnittstelle von PHP implementiert.

Moment mal – heißt das nicht, dass man die Abwärtskompatibilität vergessen kann? Nun, wir haben ja zum Glück die ArrayAccess-Schnittstelle implementiert, sodass Sie die Antwort weiterhin als Array nutzen können, wie in den folgenden Vergleichs-Screenshots zu sehen:

Haben Sie den Unterschied bemerkt? Der Namespace hat sich geändert! Wir haben den Root-Namespace Elastic eingeführt. Der restliche Code sieht nach außen hin gleich aus, aber unter der Motorhaube hat sich einiges getan.

Wie bereits erwähnt ist $response in Version 8 ein Objekt, während es in Version 7 ein assoziatives Array ist. Wenn Sie möchten, dass das Verhalten in Version 8 identisch mit dem Verhalten in Version 7 ist, können Sie die Antwort mit der Funktion $response->asArray() als Array serialisieren.

Wir bieten auch die Funktionen asObject(), asString() und asBool(), um den Nachrichtentext als ein Objekt der PHP-Standardklasse (stdClass), als String oder als Boolean (bei 2xx-Antwort „true“, sonst „false“) zu serialisieren.

So können Sie beispielsweise den oben genannten Endpointinfo() wie folgt nutzen:

$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 kann auf den Nachrichtentext der Antwort als ein Objekt zugreifen, das die magische PHP-Methode _get() implementiert.

Wenn Sie die HTTP-Antwort lesen möchten, müssen Sie nicht die letzte Nachricht des Client-Objekts abrufen, sondern können einfach wie folgt auf die PSR‑7-Nachricht in $response selbst zugreifen:

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

Das ist ein großer Vorteil, vor allem wenn Sie mit Asynchronität arbeiten. Tatsache ist: Wenn Sie mit asynchroner Programmierung arbeiten, können Sie die letzte Antwort vom Client nicht abrufen. Es ist nicht unbedingt gesagt, dass die letzte Antwort die ist, nach der Sie suchen (auf asynchrone Operationen gehen wir weiter unten noch einmal ein).

Endpoint-Parameter für die automatische Vervollständigung

Im PHP-Client für Elasticsearch 8 gibt es neu eine Funktion zur automatischen Vervollständigung („Autocompletion“). Dabei kommen die objektartigen Arrays des Psalm-Projekts zum Einsatz. Psalm ist ein Tool für die statische Analyse, mit dem Entwickler:innen den Code mit dem speziellen Attribut „phpDoc“ dekorieren können. Eines dieser Attribute ist @psalm-type, das die Angabe der Schlüsseltypen eines assoziativen Arrays ermöglicht. Wir haben den Psalm-Typ mit dem phpDoc-Standard-Tag @param angewendet. Jeder PHP-Client-Endpoint hat das Array $params als Eingabeparameter. Das folgende Beispiel zeigt den Endpoint-Abschnitt 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 = [])

Alle Parameter werden mit einem Namen („index“) angegeben, der aus dem Typ („string“) und einem Kommentar besteht, der den Parameter beschreibt („The name of the index“). Die Pflichtparameter sind durch den Hinweis „REQUIRED“ gekennzeichnet.

Mit der oben dargestellten Notation können Sie die automatische Vervollständigung in Ihrer IDE einrichten. So können Sie z. B. mit „PhpStorm“ das kostenlose Plugin „deep-assoc-completion“ installieren, um mit dem Attribut „@psalm-type“ die automatische Vervollständigung für das assoziative PHP-Array möglich zu machen.

Video thumbnail

Das Plugin „deep-assoc-completion“ ist auch für Visual Studio Code verfügbar, auch wenn diese Version sich noch in der Entwicklung befindet.

Modulare Architektur

Eine weitere Änderung, die wir in Version 8 vorgenommen haben, war die Abspaltung der HTTP-Transportschicht von der Bibliothek. Wir haben mit der Bibliothek „elastic-transport-php“ einen PSR‑18-Client für die Verbindung mit Elastic-Produkten in PHP geschaffen. Diese Bibliothek wird nicht nur von „elasticsearch-php“, sondern auch von anderen Projekten wie „enterprise-search-php“ genutzt.

Sie basiert auf einer modularen Architektur, das heißt, Sie können sie so konfigurieren, dass sie eine bestimmte Implementierung der folgenden Schnittstellen verwendet:

Der PHP-Client für Elasticsearch 8 verwendet elastic-transport-php als Abhängigkeit. Das bedeutet, dass Sie für die Verbindung zu Elasticsearch eine benutzerdefinierte HTTP-Bibliothek, einen benutzerdefinierten Knoten-Pool oder einen benutzerdefinierten Logger verwenden können.

Wir haben die Bibliothek „HTTPlug“ verwendet, um die PSR‑18- und die PSR‑7-Bibliothek in einer PHP-Anwendung automatisch erkennen zu lassen. Wenn in der Anwendung keine HTTP-Bibliothek installiert ist, verwenden wir standardmäßig Guzzle.

So kann z. B. der Symfony-HTTP-Client wie folgt verwendet werden:

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

Alternativ können Sie auch die Monolog-Logger-Bibliothek wie folgt verwenden:

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

Weitere Informationen, wie Sie den Client anpassen können, finden Sie auf der Konfigurationsseite.

Verbindung zu Elastic Cloud

Elastic Cloud ist die Elastic-eigene PaaS-Lösung. Zum Herstellen einer Verbindung zu Elastic Cloud benötigen Sie lediglich die Cloud-ID und den API-Schlüssel.

Die Cloud-ID finden Sie in Ihrem Elastic Cloud-Dashboard auf der Seite My deployment. Den API-Schlüssel können Sie auf der Seite Management im Bereich Security generieren.

Weitere Informationen dazu finden Sie in der PHP-Client-Dokumentation im Abschnitt „Connecting“.

Wenn Sie die Cloud-ID und den API-Schlüssel zur Hand haben, können Sie „elasticsearch-php“ wie folgt nutzen, um eine Verbindung zu Ihrer Elastic Cloud-Instanz herzustellen:

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

„Secure by default“

Wenn Sie in Ihrer Infrastruktur Elasticsearch 8 installiert haben, können Sie den PHP-Client mit aktivierter TLS (Transport Layer Security) verwenden. Elasticsearch 8 ist „Secure-by-default“, das heißt, die Kommunikation zwischen Client und Server wird per TLS geschützt.

Um „elasticsearch-php“ so zu konfigurieren, dass eine Verbindung zu Elasticsearch 8 hergestellt wird, benötigen Sie die Zertifikatsdatei der Zertifizierungsstelle (CA).

Es gibt verschiedene Möglichkeiten, Elasticsearch zu installieren. Wenn Sie Docker verwenden, müssen Sie den folgenden Befehl ausführen:

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

Nachdem Sie das Docker-Image installiert haben, können Sie Elasticsearch mit einer Cluster-Konfiguration mit nur einem Knoten ausführen:

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

Dieser Befehl erstellt ein Docker-Netzwerk namens „elastic“ und startet Elasticsearch über den Port 9200 (Standard).

Wenn Sie das Docker-Image ausführen, wird für die Elastic-Nutzerin bzw. den Elastic-Nutzer ein Passwort erstellt und an das Terminal ausgegeben (möglicherweise müssen Sie im Terminal etwas zurückscrollen, um es zu sehen). Dieses Passwort müssen Sie kopieren, um eine Verbindung zu Elasticsearch herstellen zu können.

Elasticsearch wird jetzt ausgeführt und die Zertifikatsdatei http_ca.crt kann abgerufen werden. Diese können Sie mit dem folgenden Befehl aus der Docker-Instanz kopieren:

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

Wenn Sie das Zertifikat http_ca.crt und das Passwort haben, das Sie beim Starten von Elasticsearch kopiert haben, können Sie wie folgt eine Verbindung herstellen:

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

Verwenden des Clients im asynchronen Modus

Der PHP-Client bot die Möglichkeit, für jeden Endpoint asynchrone Aufrufe auszuführen. Bei Version 7 mussten Sie dazu im client-Schlüssel, der als Parameter für den Endpoint übergeben wurde, wie folgt einen speziellen future => lazy-Wert angeben:

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

Das Beispiel oben indexiert das „{ "foo": "bar" }“-Dokument in Elasticsearch mithilfe eines asynchronen HTTP-Aufrufs. $response ist ein Future und nicht die eigentliche Antwort.

Ein Future steht für eine zukünftige Berechnung und fungiert als Platzhalter. Sie können ein Future wie ein normales Objekt in Ihrem Code weitergeben. Wenn Sie die Ergebniswerte benötigen, können Sie das Future auflösen. Wenn das Future (aufgrund irgendeiner Aktivität) bereits aufgelöst wurde, sind die Werte sofort verfügbar. Wenn das Future noch nicht aufgelöst wurde, wird die Auflösung so lange blockiert, bis diese Werte verfügbar werden (z. B. nach Abschluss des API-Aufrufs).

In Version 7 ist das Future eigentlich ein Promise des RingPHP-Projekts. In Version 8 müssen Sie, wenn Sie den asynchronen Ansatz verwenden möchten, den speziellen Adapter für Ihren HTTP-Client installieren. Wenn Sie z. B. Guzzle 7 verwenden (die standardmäßige HTTP-Bibliothek für „elasticsearch-php“), müssen Sie den php-http/guzzle7-Adapter wie folgt installieren:

composer require php-http/guzzle7-adapter

Zum Ausführen eines Endpoints mithilfe eines asynchronen Aufrufs müssen Sie diesen mithilfe der Funktion Client::setAsync(true) wie folgt aktivieren:

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

Wenn Sie den asynchronen Ansatz für den nächsten Endpoint wieder deaktivieren möchten, müssen Sie setAsync auf „false“ zurücksetzen.

Die Antwort eines asynchronen Aufrufs ist ein Promise-Objekt der HTTPlug-Bibliothek. Dieses Promise entspricht dem Promises/A+-Standard. Ein Promise stellt das Endergebnis eines asynchronen Vorgangs dar.

Um diese Antwort zu erhalten, müssen Sie warten, bis sie eintrifft. Dadurch wird das Warten auf die Antwort wie folgt blockiert:

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

Der primäre Weg der Interaktion mit einem Promise ist über dessen then-Methode. Diese registriert Callbacks, um entweder den letztendlichen Wert eines Promise oder den Grund zu erhalten, warum das Promise nicht erfüllt werden kann.

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

Der letzte $response->wait()-Eintrag im Beispiel oben wird benötigt, um den Ausführungsaufruf aufzulösen.

Weniger Code und Arbeitsspeicherbelastung

Der neue PHP-Client für Elasticsearch nutzt im Vergleich zu Version 7 weniger Code. Konkret besteht Version 8 dieses PHP-Clients aus 6.522 Zeilen Code + 1.025 Zeilen „elastic-transport-php“-Code, was zusammen 7.547 Zeilen ergibt. In Version 7 waren es 20.715 Zeilen Code. Version 8 ist also nur etwa ein Drittel so groß wie die Vorgängerversion.

In Bezug auf den Arbeitsspeicher ist zu erwähnen, dass der PHP-Client für Elasticsearch 8 einen „Lazy Loading“-Mechanismus implementiert, um das Laden des API-Namespace zu optimieren. Auf diese Weise wird sichergestellt, dass nicht alle Spezifikationen für alle der mehr als 400 verfügbaren Endpoints in den Arbeitsspeicher geladen werden, wenn Sie nur einen Teil dieser Gesamtmenge verwenden.

Fazit

Elasticsearch 8 bietet einige wirklich spannende Verbesserungen. Von der neuen Architektur und der Abwärtskompatibilität mit Version 7 zu standardmäßigen Security-Einstellungen und den Vorteilen des asynchronen Modus – die Zahl der Möglichkeiten war nie so groß wie heute.

Am besten lässt sich das alles in Elastic Cloud nutzen. Probieren Sie Elastic Cloud kostenlos aus!