zenstruck/elastica-bundle
Zenstruck ElasticaBundle integrates the Elastica Elasticsearch client with Symfony, providing bundle configuration and service wiring for connecting Symfony applications to Elasticsearch with less boilerplate.
This is a thin Symfony DI wrapper around the ruflin/elastica client — it does not provide entity mapping, automatic index population, or a repository layer. Start by understanding that scope: the bundle's only job is registering a configured Elastica\Client as a service.
Install and register
composer require zenstruck/elastica-bundle
// app/AppKernel.php (Symfony 2.x era)
public function registerBundles()
{
$bundles = [
// ...
new Zenstruck\ElasticaBundle\ZenstruckElasticaBundle(),
];
}
Configure the client in app/config/config.yml:
zenstruck_elastica:
clients:
default:
host: "%elasticsearch_host%"
port: "%elasticsearch_port%"
Where to look first: the bundle's DependencyInjection/ folder — the Extension and Configuration classes define every config key and the service aliases (typically zenstruck_elastica.client.default, and a zenstruck_elastica.client alias pointing at the first client). Read these before guessing at config shapes.
First use case — inject the client and index a document:
use Elastica\Client;
use Elastica\Document;
class ProductIndexer
{
public function __construct(private Client $client) {}
public function index(array $row): void
{
$index = $this->client->getIndex('products');
$index->getType('product')->addDocument(
new Document($row['id'], $row)
);
}
}
Wire Elastica\Client to the bundle's service in your container (or use the explicit service id if autowiring isn't available).
Inject the client, not a wrapper. Since there is no repository abstraction, the idiomatic pattern is to inject Elastica\Client (or the explicit zenstruck_elastica.client.<name> service) into your own small indexer/searcher services:
services:
app.product_indexer:
class: AppBundle\Search\ProductIndexer
arguments: ['@zenstruck_elastica.client.default']
Multiple clients / clusters. Define more than one under clients: and address them by service id — useful for a writable cluster plus a read replica, or for separating analytics from search:
zenstruck_elastica:
clients:
default: { host: es-write.internal, port: 9200 }
read: { host: es-read.internal, port: 9200 }
Index and mapping bootstrap. The bundle does not create indexes for you. Add an idempotent console command or migration step that checks/creates the index and sets mappings:
if (!$client->getIndex('products')->exists()) {
$client->getIndex('products')->create();
}
$type = $client->getIndex('products')->getType('product');
$type->setMapping([
'name' => ['type' => 'string'],
'price' => ['type' => 'float'],
]);
Bulk for backfills. For populating an existing dataset, use Elastica's bulk API rather than per-document indexing — the difference is an order of magnitude at scale:
$bulk = new \Elastica\Bulk($client);
foreach ($rows as $row) {
$bulk->addData(new Document($row['id'], $row), 'index', ['index' => 'products', 'type' => 'product']);
}
$bulk->send();
Searching stays entirely in Elastica's query DSL — build a Query, wrap in Query\Match/Query\BoolQuery, and call $index->search($query):
$query = new \Elastica\Query(new \Elastica\Query\Match('name', $term));
$results = $client->getIndex('products')->search($query);
Sync-on-write. Since there's no event subscriber, tie indexing to your domain events (Doctrine postPersist/postUpdate or a Symfony event listener) and keep the indexer as a discrete, testable service.
Testing. Point the client at a disposable ES instance in config_test.yml, drop index prefixes per test run, or swap Elastica\Client for a mock in unit tests — the thinness of the bundle makes this easy.
Version compatibility is the big one. This bundle last shipped in 2015 and pins to Elastica versions from that era (1.x/2.x), targeting Elasticsearch 1.x/2.x. Modern Elasticsearch (7.x/8.x) requires a much newer ruflin/elastica — you will not be able to use this bundle against a current cluster without significant patching. Verify the constraint in composer.json before adopting.
It is not FOSElasticaBundle. There is no automatic Doctrine integration, no index population, no listeners, no serializer. If you need those, this is the wrong tool — the whole package is a config-to-service bridge.
Very low adoption signal. With ~1 star, zero dependents, and no release since 2015, treat this as an abandoned personal package. For a new project, register the Elastica\Client yourself in three lines of YAML — you'd get identical behavior with no dead dependency.
Service id guessing. If autowiring isn't in play, log the container services (debug:container) to confirm the exact client ids the extension registers — naming conventions in older Zenstruck bundles occasionally differ from the obvious zenstruck_elastica.client.<name>.
Multiple-client default resolution. When you define several clients, check whether a zenstruck_elastica.client alias exists and which one it points at; injecting @zenstruck_elastica.client in a multi-client setup can silently give you the wrong cluster.
No config validation depth. Older extensions often accept sparse config and pass it straight to Elastica\Client. A missing host/port will fail at connection time, not at container compile time — set sensible defaults in parameters and reference them.
Extension points are minimal. There is no Elastica\Client factory hook or compiler pass for customization beyond config; if you need connection options the bundle doesn't expose, define your own Elastica\Client service and skip the bundle entirely.
Debugging search results. Elastica\Response exposes getData() and getQuery() — dump the raw response when queries return nothing unexpected; silent mapping mismatches (e.g., string vs text/keyword) are the most common cause and are indistinguishable until you inspect the raw JSON.
How can I help you explore Laravel packages today?