Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Elasticsearch Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Client version must match the cluster's major version. The official PHP client (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.
  • Mapping changes are not applied in place. Changing a field's 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.
  • Catch the right exceptions. 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 responses lie by omission. bulk() returns HTTP 200 even when individual items fail. Always inspect $response['items'] and count error keys; otherwise you'll silently drop documents.
  • Memory during bulk. 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.
  • Dates and mappings. The client (with 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.
  • Debugging: enable the client's logger (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.
  • Extension points are thin. Because this is a lightweight bundle, there is no repository base class to override. If you need pagination, hydration, or document mapping, build your own layer; don't expect the bundle to grow with you.
  • Low bus factor (8 stars, single maintainer). Wrap the client behind your own small interface in the app so a fork or replacement bundle (or FOSElastica) is a swap of one adapter, not a rewrite.
  • Check 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr