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

Elastic Bridge Laravel Package

lacasera/elastic-bridge

lacasera/elastic-bridge is a Laravel package that connects Eloquent models with Elasticsearch. It lets you index and sync records automatically, then search them through a clean, familiar query builder—an easy way to add fast, scalable full-text search to any Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require lacasera/elastic-bridge
php artisan vendor:publish --tag="elastic-bridge-config"

Set your driver and connection in .env — note the SEARCH_* prefix (v1's ELASTICSEARCH_* names no longer resolve):

SEARCH_DRIVER=elasticsearch        # or: opensearch
SEARCH_HOST=https://es1:9200,https://es2:9200
SEARCH_USERNAME=
SEARCH_PASSWORD=
# or SEARCH_API_KEY=... / SEARCH_VERIFY_SSL=true / SEARCH_SSL_CERT=...
# AWS SigV4 (requires `aws/aws-sdk-php`): SEARCH_AWS_REGION, SEARCH_AWS_SERVICE

Where to look first

  1. config/elastic-bridge.php — connection, bulk.max / bulk.chunk_size, and any driver defaults.
  2. The v1 → v2 upgrade guide if you're migrating — the env rename has no fallback, so this is the first thing to fix.
  3. llms.txt and the shipped Boost skill (resources/boost/…) — genuinely useful for generating correct v2 query builder code instead of v1 patterns.

First use case — define a model, index a document, query it:

class Hotel extends \Lacasera\ElasticBridge\Model
{
    protected $index = 'hotels';

    protected $casts = [
        'stars'            => 'integer',
        'rating'           => 'decimal:2',
        'location.lat'     => 'float',
        'status'           => HotelStatus::class,
        'opened_at'        => 'datetime',
    ];
}

Hotel::create(['name' => 'Ritz', 'stars' => 5, 'location' => ['lat' => 48.85]]);

Hotel::query()->where('stars', '>=', 4)->orderBy('rating', 'desc')->get();

The same fluent API works against Elasticsearch and OpenSearch — swapping SEARCH_DRIVER should not require code changes.

Implementation Patterns

Model-centric writes. Treat the model as the unit of work: $casts, accessors/mutators via Attribute::make, and $appends behave like Eloquent, including dot-notation nested paths ('hotel.location.lat' => 'float'). Nested values assign/retrieve by dotted key and serialize in place in toArray()/toJson(), preserving siblings — so you can cast a nested field without clobbering the rest of the object.

protected $appends = ['display_name'];

protected function displayName(): Attribute
{
    return Attribute::make(get: fn () => "{$this->name} ({$this->stars}★)");
}

Bulk writes for ingestion. Use Model::bulk([...]) / Model::upsert([...]) for imports and syncs. Chunking is automatic; tune bulk.max and bulk.chunk_size to your cluster's payload limits. Inspect the BulkResult DTO rather than assuming success:

$result = Hotel::upsert($rows);

if ($result->hasErrors()) {
    Log::warning('Partial bulk failure', [
        'count'   => $result->count(),
        'total'   => $result->total(),
        'failed'  => $result->failed(), // per-document errors
    ]);
}

Multi-index reads, concrete writes. This is the headline pattern for logs/time-series: read from a wildcard or comma list, write to a rolled index.

class LogEntry extends Model
{
    protected $index      = 'logs-*';      // read target
    protected $writeIndex = 'logs-2025.01'; // write target
}

Existing documents write back to their origin _index, so updates don't silently migrate data. Use from() / into() to override read/write index per call (e.g. for backfills or cross-index migrations).

Query builder discipline. Boolean clauses (must/should/must_not/filter) render as arrays of clause objects; multiMatch() and matchPhrase() nest under bool.must. Filters used outside an explicit asBoolean() are promoted into a bool rather than dropped — so chaining filters and boolean clauses produces valid DSL. Lean on validation: invalid orderBy values throw InvalidQuery against OrderOperator, and range comparisons are checked against RangeOperator.

Aggregations. Results are stored per collection instance (no global Collection::macro()), so multiple aggregations coexist and don't leak between requests in Octane/queues. Prefer stats(), histogram(), and query-scoped aggregates over hand-rolled _search bodies.

Testing. Use ::fake() backed by Lacasera\ElasticBridge\Testing\FakeConnection — driver-agnostic, so the same test suite passes with either backend and without a live cluster.

Gotchas and Tips

The breaking changes will bite in this order:

  1. Env vars. ELASTICSEARCH_* → SEARCH_*, no fallback. Also re-publish config with --force after upgrading.
  2. BridgeBuilder::all() is now all(int $perPage = 15, array $columns = ['*']) and returns a bounded first page. Any call site that assumed "everything" must switch to ->get() with explicit pagination or bulk() for full dumps.
  3. ConnectionInterface now requires search(), count(), index(), update(), bulk() — getClient() is gone. Custom connections need reimplementation.
  4. Range operators. RangeOperator::FROM / ::TO are removed; only gt/gte/lt/lte are valid in a range query. Don't confuse this with the from() / into() index-override methods, which still exist and mean something entirely different.
  5. TermsSetValidator was renamed (was TermSetValidator). If you resolved or extended it by name, update references.

Count semantics. count() now sends only query to the _count API (the full search body was rejected) and honors filter-only clauses. If you relied on count being derived from a search response, expect a separate request and slightly different behavior with size/from set.

Bulk tuning. bulk.max is a record cap, distinct from chunk_size (per-request batch). Setting max below your dataset size will silently truncate — check BulkResult::total() against your input count.

Empty/aggregation-only hits. getAttribute() is now safe when a hit has no _source (aggregation-only or _source: false), and PaginatedCollection::links() handles empty results (total: 0, empty cursors). Still, guard against null model attributes when a query explicitly disables _source.

Serialization. The decimal cast crash on toArray()/toJson() is fixed — if you worked around it with accessors returning floats, you can remove those shims.

Long-lived workers. If you previously saw aggregation results bleeding between jobs/requests under Octane, that's fixed by per-instance storage — but it also means you must read aggregations from the same collection instance you ran them on, not a global.

Driver-specific edges. Multi-host load-balancing applies to Elasticsearch only; OpenSearch uses the given endpoint. AWS SigV4 auth requires the optional aws/aws-sdk-php package — install it before setting SEARCH_AWS_*, or auth fails at connect time.

Version floor. PHP 8.2/8.3, Laravel 10–12. Laravel 13 is not yet supported — plan upgrades accordingly if you're on a pre-release.

Debugging InvalidQuery. These are thrown at build time, not on the wire — catch them in tests to validate query construction cheaply. For everything else, ::fake() plus asserting the generated DSL beats round-tripping against a real cluster.

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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource