gbourgeat/elasticsearch-bundle
A Symfony bundle for integrating Elasticsearch into your PHP applications, providing configuration, service wiring, and convenient tooling for indexing and querying Elasticsearch from Symfony projects.
This is a Symfony bundle, not a Laravel package — there is no ServiceProvider, no config/search.php, no Artisan commands. If you're coming from Laravel, the mental model to hold is: the bundle registers the official elasticsearch/elasticsearch client as a DI service, adds annotation-driven document mapping, and exposes bin/console index commands. The underlying client is what actually talks to Elasticsearch — the bundle is glue.
Install and verify the pin first:
composer require gbourgeat/elasticsearch-bundle
Before writing a single annotation, open composer.json and DependencyInjection/Configuration.php in the installed vendor directory. You need to confirm three things:
elasticsearch/elasticsearch major it pins (must match your server major, not just your PHP version).ongr_elasticsearch shape in most forks — do not assume the key is gbourgeat_elasticsearch).symfony/* components cleanly.Register the bundle (Symfony 3.4/4.x era — no bundles.php autoload convenience in the oldest versions):
// app/AppKernel.php (or config/bundles.php on 4.x)
$bundles = [
new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(),
];
Minimal config — define a connection and a document manager. The manager is what you inject; the raw client is an implementation detail:
# config/packages/ongr_elasticsearch.yaml
ongr_elasticsearch:
managers:
default:
index:
index_name: app
hosts: ['%env(ELASTICSEARCH_HOST)%']
mappings:
- App\Document\Product
First use case — get something indexing and querying in 30 minutes:
@Document, @Field).bin/console ongr:es:index:create (command names may differ slightly in the fork — run bin/console list | grep es to see what you actually have).ONGR\ElasticsearchBundle\Service\Manager into a service, call $manager->persist($doc); $manager->commit();.$manager->getRepository(Product::class)->findBy(...) or via the raw client for anything the repository doesn't cover.If any of those four steps fails and the source doesn't explain why within an hour, stop — the cost of fighting an unmaintained fork without documentation exceeds the cost of wiring elasticsearch/elasticsearch directly (~100 lines of Symfony service config).
Inject the Manager, not the raw client. The manager owns the connection lifecycle and the index/alias state. Reaching past it to $client->search() works but defeats the purpose:
final class ProductSearch
{ public function __construct(private Manager $manager) {}
public function index(Product $product): void
{
$this->manager->persist($product);
$this->manager->commit(); // batch this — see Gotchas
}
}
Always commit in batches. persist() only stages documents; commit() flushes them as a bulk request. A per-document commit is the single most common performance mistake with this bundle. Collect N documents (500–1000 as a starting point), commit once, and parse items[].status from the response — a 200 HTTP status with per-item failures is not success.
Use repositories for the common case, the raw client for everything else. The repository abstraction handles hydration back into your document classes, which is genuinely useful. For aggregations, custom analyzers, or _search bodies you can't express through the annotation DSL, drop to $manager->getClient()->search() — just keep it behind a SearchGateway interface so the bundle's types never leak into your domain code.
Abstract before you integrate. The highest-value pattern here is wrapping the manager in an internal interface on day one:
interface SearchGateway
{
public function index(string $id, array $doc): void;
public function bulkIndex(iterable $docs): void;
public function delete(string $id): void;
public function search(array $query): array;
public function reindexByAlias(string $from, string $to): void;
}
This is load-bearing. It's the difference between "replace the bundle in a sprint" and "rewrite the search feature." When the bundle inevitably stops working on PHP 8 or ES 8, you swap the adapter and nothing else changes.
Index via alias, always. Never point application code at a physical index name. Write to products_write, read from products_read, and swap atomically after validating document counts. The bundle supports alias management; use it. This is what makes reindexing safe and rollback possible.
Backfill/reindex workflow:
products_v2 with the new mapping.count on old and new indexes.products_read to v2.Test against a real Elasticsearch container. Mocked ES clients pass tests that fail in production on mapping conflicts, alias behavior, and analyzer differences. Use Testcontainers or a Docker service in CI — this is non-negotiable if you're adopting the bundle.
Multi-connection config. The bundle supports multiple managers; define one per cluster and inject by name. Useful for separating a staging index from production within a single app, but don't over-engineer this — most apps need exactly one.
The PHP 7.2 pin is a wall, not a speed bump. PHP 7.2 hit EOL in November 2019. If your app is on PHP 8.x, this bundle will not install without forking Composer constraints and patching the source — work you own forever. Confirm the runtime pin before anything else; it decides whether this whole assessment is academic.
Client/server major mismatch produces opaque failures. An ES 7 client against an ES 8 server gets Content-Type header rejections that surface as cryptic 406s, not helpful errors. Check elasticsearch/elasticsearch version against GET / on your server before debugging anything else.
Annotation scanning happens at container compile time. With many document classes, bin/console cache:clear gets slow. More importantly, a malformed annotation fails the entire container build — that is a deploy blocker, not a search bug. Keep document classes small and validate in CI.
commit() returning 200 does not mean documents indexed. Bulk responses carry per-item statuses. Always verify:
$response = $this->manager->commit();
foreach (\json_decode($response, true)['items'] ?? [] as $item) {
if (($item['index']['status'] ?? 0) >= 400) {
throw new \RuntimeException($item['index']['error']['reason']);
}
}
Skipping this ships empty indexes silently — the most common production search incident.
Reindexing can hit mapping conflicts on real data. Unit tests won't catch it; only indexing production-shaped documents will. Dry-run the reindex against a snapshot of production data before any alias swap.
Naming and config are inherited, not documented. Command names, root config keys, and annotation namespaces carry over from ONGR with inconsistent renaming across forks. When something doesn't work, run bin/console list, dump the resolved container, and read DependencyInjection/Configuration.php in vendor — the source is small and readable, and it's more reliable than any docs you'll find.
Extension points are limited and undocumented. You can decorate services and override command classes in your app, but the bundle has no formal plugin system. If you need behavior it doesn't provide, fork it — with eyes open about inheriting maintenance.
No security response path. Zero stars, zero dependents, single maintainer, upstream discontinued. If Elasticsearch announces a CVE, you will not get a patched bundle. Budget ~10–15% of one engineer for steady-state fork maintenance and name a specific owner — not a team.
Instrument these explicitly:
| Failure mode | Signal to watch |
|---|---|
| Alias swap to half-populated index | Count validation before repoint + manual gate |
| Mapping conflict on reindex | Index real-shaped data in staging |
| Client/server version drift | Log GET / server version on boot |
| Empty index after bulk | Parse items[].status, alert on non-zero error ratio |
| Container compile failure on PHP bump | Run cache:clear in CI against target PHP version |
The escape hatch is cheap. elasticsearch/elasticsearch wired as a Symfony service is a few dozen lines you own and that survives the PHP 8 migration. If the bundle's annotation mapping isn't saving you real time within the first day, take the hatch. The glue code was never the product.
How can I help you explore Laravel packages today?