sineflow/elasticsearch-bundle
Symfony bundle for integrating Elasticsearch into PHP applications. It streamlines client configuration, service wiring, and index/query workflows so you can add search features using the official Elasticsearch client.
This bundle is a Symfony DI integration for the official elasticsearch/elasticsearch PHP client — not a full ODM like FOSElasticaBundle. Look at it as "wire up Elasticsearch\Client from Symfony config and get a few helpers," with the expectation that you write most query/domain logic yourself against the native client API.
Minimum steps:
composer require sineflow/elasticsearch-bundle
Register the bundle in config/bundles.php:
return [
// ...
Sineflow\ElasticsearchBundle\SineflowElasticsearchBundle::class => ['all' => true],
];
Configure a client (typical shape — verify exact keys against the bundle's README / DependencyInjection/Configuration.php on the pinned version):
# config/packages/sineflow_elasticsearch.yaml
sineflow_elasticsearch:
clients:
default:
hosts:
- '%env(ELASTICSEARCH_URL)%' # e.g. http://localhost:9200
Then inject the client service wherever you need it:
use Elasticsearch\Client;
final class ProductIndexer
{
public function __construct(private Client $es) {}
public function index(array $doc, string $id): void
{
$this->es->index(['index' => 'products', 'id' => $id, 'body' => $doc]);
}
}
Where to look first: the bundle's Resources/config/services.* files to learn the actual service IDs and aliases, then DependencyInjection/Configuration.php for the real config tree. Do not assume config keys from other Elasticsearch bundles.
First use case: index a handful of records on kernel boot via a command, then dump the index mapping with curl localhost:9200/products/_mapping to confirm the client is talking to the right cluster. Build from there.
Inject the native client, not a wrapper. The main value here is autowiring Elasticsearch\Client (or ClientInterface on newer client majors) from sineflow_elasticsearch.clients.default. Keep your own thin application service (e.g. ProductSearch) around it so you can mock search behavior in unit tests without hitting the cluster.
Multiple connections. Configure several clients (default, analytics, logs) and alias them by parameter, e.g. bind Client $analyticsClient in services.yaml:
services:
_defaults:
bind:
Elasticsearch\Client $analyticsEs: '@sineflow_elasticsearch.clients.analytics'
This is the cleanest way to avoid getService('sineflow_elasticsearch.clients.analytics') string lookups scattered through the code.
Index-per-entity with a mapping provider. Keep mappings as PHP files (config/elasticsearch/products.php returning a [settings, mappings] array) rather than inline arrays. A console command that creates the index if missing and applies mappings makes environments reproducible:
if (!$this->es->indices()->exists(['index' => $index])) {
$this->es->indices()->create([
'index' => $index,
'body' => require $mappingFile,
]);
}
Bulk indexing through helpers()->bulk(). For anything beyond a few hundred docs, batch writes:
$params = ['body' => []];
foreach ($products as $p) {
$params['body'][] = ['index' => ['_index' => 'products', '_id' => $p->getId()]];
$params['body'][] = $this->normalizer->normalize($p);
if (count($params['body']) >= 2000) { // ~1000 ops
$this->es->bulk($params);
$params = ['body' => []];
}
}
if ($params['body']) {
$this->es->bulk($params);
}
Search with search_after for deep pagination. Avoid from/size beyond 10k. Sort on a tiebreaker (_id or a unique field) and use search_after. Only reach for scroll on one-off export jobs, and always clear it:
// ... after consuming results
$this->es->clearScroll(['scroll_id' => $scrollId]);
Integration with Symfony Messenger for (re)indexing. Put writes on an async queue so a slow cluster never blocks a web request. A IndexProductMessage handler that does index() with refresh: 'wait_for' only in dev/test is a good compromise.
Testing. Run the same client against a throwaway cluster (Testcontainers, elasticsearch Docker service, or an in-memory stub). The bundle does not ship a fake; you'll want either a docker-compose test fixture or an adapter interface you can swap for an ArrayAdapter.
8.x vs 7.x) is not forward/backward compatible across ES server majors. Pin both and check the bundle's composer.json constraints before upgrading Symfony.refresh is the #1 source of "I indexed it but search returns nothing." Elasticsearch refreshes every ~1s by default. In tests, either wait, index with 'refresh' => true, or call indices()->refresh(['index' => $index]) before asserting.type or analyzer requires creating a new index, reindexing, then swapping via aliases. Design for aliases (products_live → products_v1) from day one, even if the bundle doesn't force it.hosts from an env var arrives as a string, not an array. %env(ELASTICSEARCH_HOSTS)% gives you "host1,host2". Split it in a config closure or use env(ELASTICSEARCH_HOSTS)% carefully; a malformed value surfaces as a confusing connection-pool exception, not a config error.Elasticsearch\Common\Exceptions\NoNodesAvailableException (cluster down) is operationally different from Missing404Exception / BadRequest400Exception (your fault). Wrap cluster-down cases in retries with backoff; don't retry 400s.bulk() returns HTTP 200 even when individual items fail. Always inspect $response['items'] and count error keys; otherwise you'll silently drop documents.bulk() returns the full response body including all item details — for 10k-doc batches that can be tens of MB. Keep batches ~500–1000 docs and discard responses.json_encode) sends DateTime as RFC3339 if you normalize correctly. If you send "2024-01-01 10:00:00", ES will reject or misparse depending on mapping. Normalize to DATE_ATOM explicitly.setLogger() from PSR-3) during development; Symfony's monolog at debug level will show the requests. Also learn the Kibana Dev Tools / _cat/indices?v — faster than re-reading stack traces.last release against your Symfony version. A 2026-07 release on a low-traffic bundle may not yet support a newly-released Symfony major; read the changelog before bumping symfony/framework-bundle.How can I help you explore Laravel packages today?