munkie/elasticsearch-thrift-php
A PHP Elasticsearch client that talks to the cluster over the Thrift protocol instead of HTTP. It lets PHP applications index, search, and manage data through the Thrift-generated API, making it useful for legacy Elasticsearch setups that still expose Thrift endpoints.
munkie/elasticsearch-thrift-php is not an Elasticsearch client in the conventional sense — it is a collection of Thrift-generated PHP classes for the long-deprecated elasticsearch-transport-thrift plugin. You don't get a query DSL builder, connection pooling, retries, or a Laravel facade. You open a raw Thrift socket (default :9500), send a RestRequest that mimics an HTTP call, and receive a RestResponse. That's the whole API.
Verify the server side before writing a line of code. The Thrift transport has not shipped with Elasticsearch since the 2.0 era. This package is only viable if you are pinned to an ES 1.x cluster that already has the elasticsearch-transport-thrift plugin installed and listening. If you're on ES 2.x+, OpenSearch, Elastic Cloud, or AWS OpenSearch, stop — the plugin is gone and this package cannot help you. Use elasticsearch/elasticsearch instead.
Assuming you genuinely have an ES 1.x node with the plugin:
composer require munkie/elasticsearch-thrift-php
composer require apache/thrift # or enable ext-thrift in php.ini
Where to look first, in this order:
src/Elasticsearch/Thrift/RestClient.php — the class you instantiate. Every operation funnels through execute().src/Elasticsearch/Thrift/RestRequest.php and RestResponse.php — read the property lists. Those DTOs are the API surface.src/Elasticsearch/Thrift/Method.php — verb constants (Method::GET, Method::POST, Method::PUT, Method::DELETE).*_types.php / *_args.php files if you hit a TProtocolException — that almost always signals a version mismatch between the generated code and the installed plugin.First use case — open a socket and ping the cluster:
use Elasticsearch\Thrift\RestClient;
use Elasticsearch\Thrift\RestRequest;
use Elasticsearch\Thrift\Method;
use Thrift\Transport\TSocket;
use Thrift\Transport\TBufferedTransport;
use Thrift\Protocol\TBinaryProtocol;
$socket = new TSocket(config('es-thrift.host'), config('es-thrift.port', 9500));
$socket->setRecvTimeout(5000);
$socket->setSendTimeout(5000);
$transport = new TBufferedTransport($socket);
$transport->open();
$client = new RestClient(new TBinaryProtocol($transport));
$req = new RestRequest();
$req->method = Method::GET;
$req->uri = '/';
$req->params = [];
$res = $client->execute($req);
// $res->status, $res->body, $res->headers
A status of 200 and a JSON body means you have a working baseline. Everything else from here is building uri/params/body payloads by hand.
The transport is stateful and expensive to open per request. Open it once and reuse it — this mirrors how you'd wrap any low-level socket client.
// app/Providers/ElasticsearchThriftServiceProvider.php
public function register(): void
{
$this->app->singleton(RestClient::class, function ($app) {
$cfg = $app['config']->get('es-thrift');
$socket = new TSocket($cfg['host'], $cfg['port']);
$socket->setRecvTimeout($cfg['timeout']);
$socket->setSendTimeout($cfg['timeout']);
$transport = new TBufferedTransport($socket);
$transport->open();
return new RestClient(new TBinaryProtocol($transport));
});
}
Pair it with a publishable config/es-thrift.php holding only host, port, and timeout. There is no scheme, no auth, no TLS — don't invent config keys the transport cannot honor.
RestRequest is a dumb DTO, so it's dangerously easy to leak Thrift vocabulary into controllers, jobs, and models. Isolate it behind one adapter class and treat that adapter as the only place in your codebase allowed to use Elasticsearch\Thrift\....
class ThriftSearchGateway
{
public function __construct(private RestClient $client) {}
public function search(string $index, array $query, int $size = 10): array
{
$req = new RestRequest();
$req->method = Method::POST;
$req->uri = "/{$index}/_search";
$req->params = [];
$req->body = json_encode([
'query' => $query,
'size' => $size,
]);
$res = $this->client->execute($req);
if ((int) $res->status >= 400) {
throw new ThriftSearchException("ES {$res->status}: {$res->body}");
}
return json_decode($res->body, true, 512, JSON_THROW_ON_ERROR);
}
}
The key fact your whole design hinges on: execute() does not throw on an ES 4xx/5xx. It returns a RestResponse with a non-200 status and you must inspect it. Centralizing that check in the gateway is the single highest-value thing you can do here.
The Thrift transport sends an arbitrary body per call, so the NDJSON _bulk endpoint works — but the client gives you zero help. You must build the NDJSON yourself, enforce a trailing newline (ES 1.x is strict about it), and inspect per-item results inside the returned JSON.
public function bulk(string $index, array $docs): array
{
$body = '';
foreach ($docs as $doc) {
$body .= json_encode(['index' => ['_index' => $index, '_type' => 'doc', '_id' => $doc['id'] ?? null]]) . "\n";
$body .= json_encode($doc['source']) . "\n";
}
// trailing newline is already present from the last append
$req = new RestRequest();
$req->method = Method::POST;
$req->uri = '/_bulk';
$req->params = [];
$req->body = $body;
$res = $this->client->execute($req);
$decoded = json_decode($res->body, true);
if (!empty($decoded['errors'])) {
// individual items failed; do not assume a 200 means success
throw new ThriftBulkException($decoded['items']);
}
return $decoded;
}
Because each execute() is a synchronous socket round-trip with no pipelining, per-document indexing in a loop is slow. Batch inside a queued job and flush every N docs (a few hundred is typical for ES 1.x). Wrap the flush in a try/finally that closes and reopens the transport on socket errors, since a crashed Thrift connection will not recover on its own.
Add a /health proxy that performs a GET / via the gateway and returns 503 if it fails or status >= 400. This is what lets you fail over or alert when the legacy plugin node goes away. Combine with a circuit breaker around the gateway so a dead Thrift port doesn't cascade into timeouts across the app.
:9500. Verify the plugin is actually loaded on every target node — it's often left out of newer AMIs or container images.execute() never throws for ES-level errors. A 404/500 comes back as a normal RestResponse with status: 404/500. Check $res->status every single call. Missing this makes bugs invisible.execute() in your own retry helper with backoff, or accept the failure loudly — do not silently swallow.RestClient bound as a singleton will deadlock or throw if two code paths use it concurrently (e.g., queue workers sharing a process). Either bind it scoped or ensure each worker opens its own.TProtocolException / TApplicationException during execute() almost always means the generated Thrift classes and the installed plugin are on incompatible versions. Pin apache/thrift in composer.json and match it to the plugin version on the server — do not float.TSocket will happily block forever if you forget setRecvTimeout / setSendTimeout. Always set both; a hung Thrift call ties up a PHP-FPM worker.\n. Mappings and analyzers differ from ES 7+/OpenSearch; queries copied from modern docs will behave differently or be rejected.elasticsearch/elasticsearch (HTTP/JSON) as soon as the cluster can move.RestRequest fields before execute() and compare against what curl -XGET 'http://node:9200/index/_search?pretty' returns. The Thrift transport is just an HTTP shim over a binary socket, so the wire semantics are the same.RestClient. The generated code is meant to be treated as a black box.How can I help you explore Laravel packages today?