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

ongr/elasticsearch-bundle

The Ongr Elasticsearch Bundle integrates Elasticsearch with Symfony applications, offering configuration-driven document object mapping, repositories, indexing, bulk operations, aggregations, and flexible query building for scalable full-text search and analytics in PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the bundle and the DSL library it builds on:

    composer require ongr/elasticsearch-bundle ongr/elasticsearch-dsl
    

    Register ONGR\ElasticsearchBundle\ONGRElasticsearchBundle in config/bundles.php. Check the bundle's version against your Elasticsearch server and Symfony version first — the bundle is tightly coupled to elasticsearch-php 7.x, so ES 8 is out of scope. As of v6.3.0 the bundle runs on PHP 8.x; on a PHP 8 codebase pin ^6.3 explicitly ("ongr/elasticsearch-bundle": "^6.3") so you don't resolve back to a PHP 7-only tag, and be ready to run composer update --with-all-dependencies because the PHP constraint bump can reshuffle doctrine/annotations and the ES client.

  2. Configure a manager (the manager owns one index and one connection; the config shape changed between majors, so copy the example from the README matching your installed tag):

    # config/packages/ongr_elasticsearch.yaml
    ongr_elasticsearch:
      managers:
        default:
          index:
            hosts: ['%env(ELASTICSEARCH_HOST)%']
            index_name: app_products
            settings:
              number_of_replicas: 0
    
  3. Define a document — a plain PHP class annotated with the bundle's mapping annotations. This is the equivalent of a Doctrine entity and is what the bundle uses to build the index mapping:

    use ONGR\ElasticsearchBundle\Annotation as ES;
    
    /**
     * @ES\Document(repositoryClass="App\Repository\ProductRepository")
     */
    class Product
    {
        /** @ES\Id() */
        public ?string $id = null;
    
        /** @ES\Property(type="text") */
        public string $name = '';
    
        /** @ES\Property(type="keyword") */
        public string $sku = '';
    }
    

    PHP 8 typed properties are fine — the default hydrator writes to public properties directly — but initialise them (= null, = '', = []) so a field missing from a document doesn't leave a typed property uninitialised.

  4. Create the index from the mapping and confirm it exists:

    php bin/console ongr:es:index:create
    
  5. First use case — index a document, then search it. Inject the manager and a repository, persist() each document, commit() once, then query:

    $manager = $this->get('ongr_elasticsearch.manager.default');
    
    $manager->persist(new Product());  // queue into bulk
    $manager->commit();                // flush the bulk request
    
    $repo = $manager->getRepository(Product::class);
    $search = $repo->createSearch();
    $search->addQuery(new \ONGR\ElasticsearchDSL\Query\FullText\MatchQuery('name', 'widget'));
    $results = $repo->findDocuments($search); // hydrated Product objects
    

That loop (document → index create → persist/commit → repository search) is 90% of day-to-day work with this package.

Implementation Patterns

Manager-per-Index. Treat the manager as the unit of index access. Most apps have one manager for the primary index, and optionally a second for logs / autocomplete / a read alias. Inject the manager (or a thin service wrapping it) rather than the client, so the bundle's hydration and mapping stay authoritative.

Custom repositories for query composition. Put @ES\Document(repositoryClass=...) on every document and centralize query building in the repository. The repository extends DocumentRepository and gives you createSearch() (returns an ongr/elasticsearch-dsl Search object), find(), findBy(), and findDocuments($search). Repositories are the natural seam for pagination, default filters (e.g. published = true), and sort order — keep controllers/services free of DSL syntax.

CRUD flow. Reads go through the repository. Writes go through the manager:

$manager->persist($product);   // add to bulk queue
$manager->remove($product);    // add delete to bulk queue
$manager->commit();             // one bulk request for everything since last commit

Batch imports (CSV/API sync) follow the same pattern: loop persist() thousands of documents, commit() in chunks (a few hundred to a few thousand per commit) to keep bulk payloads bounded.

Index lifecycle as a deploy step. Mapping only takes effect at index creation. The deploy workflow is: run your import/migration script into a new index name, then flip an alias. For zero-downtime this means configuring the manager to use an alias name as index_name, creating app_products_v2 with the new mapping, backfilling, and atomically repointing the alias. For small apps, dropping and recreating the index in a deploy task is acceptable — but treat ongr:es:index:create / ongr:es:index:drop as deploy-time commands, never per-request.

Testing. Set refresh: true on the manager in the test environment so writes are searchable immediately after commit(); leave it off in prod. Use a dedicated *_test index name and reset it in a setUp/tearDown.

Relations and nested data. Embedded/nested documents (@ES\Embedded, @ES\Object, parent/child) are supported but the annotations are fussy — prefer flattening denormalized fields for search, and only reach for nested when you genuinely query arrays of sub-objects with nested queries. Remember that changing a nested mapping is an index rebuild.

Search DSL integration. The bundle leans on ongr/elasticsearch-dsl for query building. Use the DSL's BoolQuery/FunctionScore/aggregation classes rather than hand-written arrays; you get autocompletion and the objects are serializable into the search request the repository sends. Aggregations come back off the raw result set, so use the raw result when you care about buckets rather than hydrated documents.

Service wiring. The bundle exposes one service per manager (ongr_elasticsearch.manager.<name>), useful for legacy get() code, but modern Symfony code should prefer constructor injection via an alias or a small wrapper service. The manager is stateful because of the bulk queue, so avoid injecting it into long-running workers unless you commit and re-fetch carefully.

PHP 8 ergonomics. Now that v6.3 supports PHP 8, you can write documents with typed/readonly-adjacent public properties, enums, and constructor promotion — but keep the surface the hydrator needs: public properties (or well-formed setters) and constructors the hydrator can call. Don't put hydration-critical defaults inside a promoted constructor and assume the hydrator runs it; test hydration after refactoring a document to promoted properties.

Gotchas and Tips

  • persist() writes nothing until commit(). This trips up nearly everyone once. There is no flush(); commit() sends a single bulk request and clears the queue. If documents are "missing," check for a missing commit() first.
  • Refreshes are not immediate. Even after commit(), documents are not searchable until the index refreshes (~1s by default). Set refresh: true on the manager for tests and for read-after-write flows, but not in production hot paths — it is expensive.
  • Mapping changes require an index rebuild. You cannot update field types or analyzers on an existing index. ongr:es:index:create will fail or be skipped if the index already exists; you need ongr:es:index:drop plus re-import, or the alias-swap pattern. Budget for this on every schema change.
  • Bulk errors can be silent. The ES bulk response contains per-document errors; a commit() that "succeeds" can still have rejected documents (mapping conflict, wrong analyzer, etc.). Log/inspect the bulk response in import scripts, or you will discover data loss days later.
  • Annotations are still the configuration mechanism. v6.3.0 adds PHP 8.x runtime support, not PHP 8 attribute support — the release notes don't claim attributes. Documents remain docblock-annotated and still need doctrine/annotations (and doctrine/annotations' own PHP 8 constraint must resolve against your platform). If you were hoping to delete the annotation reader, check the tag's docs/changelog before rewriting documents as #[ES\Property].
  • Typed-property hydration failures. With PHP 8 typed properties, a document field absent from the ES source leaves the property uninitialised; reading it throws Typed property ... must not be accessed before initialization, and assigning a wrong scalar type throws a TypeError inside the hydrator. Initialise every property and keep types loose enough for what ES actually returns (e.g. ?string for fields that may be missing).
  • Version matrix is still the main risk — but PHP is no longer the blocker. elasticsearch-php 7.x only (no ES 8), pre-attributes annotation config, and the config key layout moved between 4.x and 5.x, so copy the README section for the exact tag you install. Check the changelog for the last release: v6.3.0 is a narrow PHP 8 compatibility bump, not a feature or deprecation release, so nothing else in your integration changes.
  • ID handling. The @ES\Id property maps to _id. If you leave it null, ES generates an ID. For idempotent upserts/imports, set the ID yourself so re-runs overwrite rather than duplicate.
  • Date and object serialization is opinionated. \DateTime fields are serialized to the mapping's expected format; mismatches between the annotation's format and the actual PHP value surface as bulk errors. Similarly, embedded objects need constructors consistent with the hydrator — public properties or well-formed setters.
  • Query the raw result for aggregations. Hydration targets documents; aggregations, _score, and highlights live on the raw result set. Don't try to read buckets off hydrated entities.
  • Profiler/web debug toolbar has an Elasticsearch panel that shows the actual DSL sent to the server — the fastest way to debug a query that returns nothing. Use it before adding var_dump inside the DSL builder.
  • Extension points are mainly repositories (custom query logic), custom hydrators (if the default hydration doesn't fit your domain model), and the manager's event/commit lifecycle. Wrapping the manager in your own service is a lightweight way to add logging, metrics, or a decorator without subclassing internals.
  • Don't hold the manager across long processes. Workers and messenger consumers should construct/search per message or explicitly commit() and discard state; a stale bulk queue will silently batch unrelated writes, and a stale connection will fail after an ES restart.
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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings