m6web/elasticsearch-bundle
A Symfony bundle that integrates Elasticsearch into your application, providing services, configuration, and tooling to index and query documents through the official Elasticsearch PHP client. It simplifies client setup, connection management, and exposing search capabilities to your Symfony service...
Install and register
composer require m6web/elasticsearch-bundle
There is no Flex recipe worth relying on — register the bundle manually in config/bundles.php:
M6Web\Bundle\ElasticsearchBundle\M6WebElasticsearchBundle::class => ['all' => true],
Minimal config
# config/packages/m6_web_elasticsearch.yaml
m6_web_elasticsearch:
clients:
default:
hosts:
- '%env(ELASTICSEARCH_DSN)%'
logger: 'logger' # PSR-3 service id
profiling: '%kernel.debug%'
Verify the real root key and every available option before writing anything else:
bin/console config:dump-reference m6_web_elasticsearch
The bundle's DependencyInjection/Configuration.php is the single source of truth here — the README has drifted across versions and the config root has been spelled both m6_web_elasticsearch and m6web_elasticsearch in different eras. Dump the reference, don't guess.
First use case
The bundle's whole job is to give you a ready-to-inject, lazily-connected instance of the official Elasticsearch\Client (plus logging and a profiler panel). So the first thing to do is inject it and run one round-trip:
final class ProductIndexer
{
public function __construct(private \Elasticsearch\Client $client) {}
public function index(array $doc): array
{
return $this->client->index([
'index' => 'products',
'id' => $doc['id'],
'body' => $doc,
]);
}
}
If autowiring the interface fails, fall back to the named service (m6_web_elasticsearch.client.default — again, confirm with bin/console debug:container --show-private | grep elasticsearch). Then open the Symfony profiler in dev and confirm the request shows up in the Elasticsearch panel. That panel appearing is your proof the bundle is wired correctly; if it's absent, the problem is registration or profiling: false, not your query.
One client per cluster, not per feature. The bundle's differentiator over a hand-written service is named clients. Use them for real topology boundaries: a write cluster and a read/replica cluster, a per-tenant cluster, an "analytics" cluster distinct from "search". Configure each with its own hosts and logger, then alias the one you want as the default for autowiring. Do not create clients per index or per module — that's what a repository layer is for.
Wrap the client, never leak it. Standard pattern: a SearchRepository/IndexRepository per bounded context that takes the client and exposes intention-revealing methods (findBySku, bulkReindex, createIndexWithAlias). Keeps the query DSL out of controllers and makes it mockable in unit tests. The bundle is an infrastructure concern; treat it that way.
Alias-swap for reindexing. The most common day-to-day operational task is "change the mapping without downtime." The bundle gives you nothing for this, so codify it as a console command: create products_v2, bulk-index into it, then indices.updateAliases to atomically point products at v2 and drop v1. Run it as a Symfony command so it composes with Messenger/CRON and shows up in your deploy runbook.
Let the profiler drive the dev loop. The data collector is the highest-value part of this bundle for day-to-day work: request body, response, timing, and the client/connection used. When a query is unexpectedly slow, check the panel before reaching for curl. In tests, $client->getProfiler()->getProfiles() style assertions (check the collector's actual API in DataCollector/) let you assert on query shape without hitting a live cluster.
Route cross-cutting concerns through the logger, not subclasses. Configure logger: 'monolog.logger.elasticsearch' and give that channel its own handler. You get slow-query and error visibility for free, per client, without decorating anything.
Split your test strategy. Unit tests: bind a mock of Elasticsearch\Client to the repository's constructor — the bundle isn't involved. Integration tests: boot the kernel with test config pointing at a container (docker-compose or Testcontainers), and exercise the real bundle wiring including the connection pool. Don't try to fake the transport layer; you'll spend more time on the fake than on a throwaway container.
Version coupling is the real adoption risk. This bundle wraps Elasticsearch\Client / Elasticsearch\ClientBuilder — the v7-and-earlier namespace. The v8 client moved to Elastic\Elasticsearch\Client with a different builder and options. Check the bundle's composer.json constraints against your elasticsearch/elasticsearch and symfony/* versions before committing. If you're on ES 8, this bundle likely isn't for you, and the honest alternative is ~20 lines of service definition plus ClientBuilder::fromConfig().
Maintenance posture. Last release 2023-08, 22 stars, no dependents. Treat it as vendored code you own: pin the version exactly, and be prepared to fork. The value proposition (lazy clients, profiler, logging) is genuinely nice, but it is not load-bearing enough to justify a painful Symfony or ES major upgrade later. Decide deliberately.
Lazy connection hides misconfiguration until runtime. Nothing connects at kernel boot, so a wrong host or credential fails on the first real query — potentially in a worker at 3am. Add a smoke-test command (ping() against each configured client) and run it in your deploy healthcheck or CI.
Profiling is not free and captures payloads. The collector stores request bodies. In dev with large bulk requests that's memory pressure; in long-running Messenger workers it's a leak. Gate it on %kernel.debug%, and if you run workers in dev, disable profiling for the worker process specifically. Also check whether indexed documents contain PII before leaving the profiler enabled anywhere shared.
Config key drift between README and code. Historically m6web_elasticsearch vs m6_web_elasticsearch, and option names like connectionParams/connection_params have shifted. Always resolve against config:dump-reference and debug:container, never against a blog post or the README of an old tag.
Debugging the transport. When you need wire-level detail (TLS failures, proxy issues, connection pool exhaustion), the bundle won't show it. Drop to the underlying client's handler/connectionParams options in the client config, or set the ES logger channel to debug. The bundle's profiler panel shows application-level requests only — it won't tell you why the pool refused a connection.
Extension points, in order of preference. (1) A PSR-3 logger service for observability; (2) a decorator around your own repository — the cleanest seam; (3) replacing the client service definition in your app config if you need a custom handler stack. Avoid subclassing the bundle's client wrapper: it's an internal class and the surface isn't stable across the sparse releases.
It does not manage mappings, templates, or ILM. There is no schema DSL, no migration system, no index lifecycle management. If you were hoping for an ORM-like experience, you'll be disappointed — this is DI plumbing, logging, and a profiler panel for the official client, nothing more. Budget accordingly.
How can I help you explore Laravel packages today?