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 Laravel Package

basemkhirat/elasticsearch

A Laravel package for working with Elasticsearch through a fluent query builder. It supports multiple connections, indices and documents, CRUD operations, bulk indexing, aggregations, and Eloquent-style models, simplifying search integration.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install (pin to a version compatible with your Elasticsearch major):
    composer require basemkhirat/elasticsearch
    
  2. Register the provider/alias if you are not on Laravel package auto-discovery (older Laravel or Lumen):
    • Provider: Basemkhirat\Elasticsearch\ElasticsearchServiceProvider
    • Alias: Elasticsearch → Basemkhirat\Elasticsearch\Facades\Elasticsearch
    • Lumen: copy config/elasticsearch.php into config/ and enable $app->withFacades().
  3. Publish the config:
    php artisan vendor:publish --provider="Basemkhirat\Elasticsearch\ElasticsearchServiceProvider"
    
  4. Point it at a cluster in .env (the package reads these in config/elasticsearch.php):
    ELASTICSEARCH_HOST=localhost
    ELASTICSEARCH_PORT=9200
    ELASTICSEARCH_SCHEME=http
    ELASTICSEARCH_USER=
    ELASTICSEARCH_PASS=
    
  5. Verify connectivity before writing any queries:
    use Elasticsearch;
    Elasticsearch::index('posts')->search()->get(); // or ->count()
    
  6. First real use case — an Eloquent-style model:
    use Basemkhirat\Elasticsearch\Model;
    
    class Post extends Model
    {
        protected $index = 'posts';
        protected $type  = 'post'; // leave '' on ES 7+
    }
    
    $posts = Post::where('title', 'elasticsearch')
        ->orderBy('created_at', 'desc')
        ->take(20)
        ->get();
    
    This where/orderBy/take/get triad is the 80% case — the rest of the package is variations on it.

Where to look first: config/elasticsearch.php (hosts + default connection), then the Model base class for $index, $type, $connection, and $timestamps. The Query builder is the surface you will actually be poking at.

Implementation Patterns

Model-per-index (recommended). One model per index keeps $index, $type, and casting logic in one place:

class Product extends Model
{
    protected $index     = 'products';
    protected $type      = 'product';
    protected $connection = 'analytics';
    protected $timestamps = true;
    protected $attributes = ['stock' => 0];
}

Reads: build, then execute. get(), first(), paginate(), count(), exists(), chunk(), and scroll() all hydrate Model instances (except aggregations — see below):

Product::whereTerm('status', 'active')
    ->whereRange('price', ['gte' => 10, 'lte' => 100])
    ->whereIn('category_id', [1,2,3])
    ->select(['id','name','price'])
    ->orderBy('_score', 'desc')
    ->paginate(25);

Writes: single and bulk. create(), save(), update(), delete() mirror Eloquent; bulk() is what you use for ingestion:

$model->fill([...])->save();
$model->update(['stock' => 5]);
$model->delete();

Product::bulk(function ($q) use ($rows) {
    foreach ($rows as $row) {
        $q->index('products')->type('product')->id($row['id'])->insert($row);
    }
});

Bulk is the only sane path for anything past a few hundred docs — do not loop ->save().

Raw DSL escape hatch. When the fluent builder cannot express it, use search() and pass a body:

$result = Product::search()
    ->body([
        'query' => ['function_score' => [/* ... */]],
        'aggs'  => [/* ... */],
    ])
    ->get();

Inspect what the builder is actually sending with ->body() / ->toSql() before calling get(). This is the single most useful debugging habit with this package.

Aggregations are nested builders hanging off aggregate()/aggs():

$stats = Order::whereTerm('status', 'paid')
    ->aggregate('terms', 'category_id')
        ->bucket('revenue')->sum('total')
        ->bucket('avg_order')->avg('total')
    ->get();   // returns a raw array, NOT hydrated models

Index and mapping lifecycle. Keep schema in migrations or an Artisan command, not in model classes:

Elasticsearch::index('products')->create(['settings' => [...], 'mappings' => [...]]);
Elasticsearch::index('products')->putMapping([...]);
Elasticsearch::index('products')->exists();
Elasticsearch::index('products')->delete();

Aliases + reindex() for zero-downtime rebuilds are the standard prod workflow.

Multi-cluster / multi-tenant. Two patterns: set $connection per model, or flip the facade per tenant:

Elasticsearch::connection('tenant_42')->index('products')->search()->get();

Prefer model-level for read/write separation; facade-level for per-tenant indexes.

Repository/service layer. Wrap each model in a small repository exposing typed methods (searchProducts(array $filters), reindexProduct($id)). Do not let controllers touch Elasticsearch:: directly — the builder API is too easy to misuse under time pressure, and it gives you one place to swap in search_after or raw DSL later.

Gotchas and Tips

  • This package is effectively frozen (last release 2019-11-28). It targets ES 5.x/6.x. ES 7+ removed mapping types and ES 8 dropped much of the wire compatibility; $type is ignored or rejected. Budget for a rewrite to elasticsearch/elasticsearch or laravel/scout + a driver if you are on anything modern.
  • Do not use $type on ES 7+. Either omit it or set ''; leaving a legacy type in will cause errors only at query time, not boot.
  • where() is a match query, not a term filter. For exact/keyword lookups use whereTerm(). This is the most common source of "empty results in dev, works in prod" bugs.
  • Writes are not immediately searchable. There is no refresh magic in save()/insert(). In tests call Elasticsearch::index('x')->refresh('true') (or wait_for) between write and read, and consider a per-test unique index suffix to avoid cross-test bleed.
  • Hydration is inconsistent by method. get(), first(), paginate(), chunk() → Model instances. Aggregations, raw(), and search()->get() on raw bodies → arrays. Pick a return shape per repository method and stick to it.
  • from/size is bounded by index.max_result_window (default 10k). paginate() beyond that silently fails or errors. For deep pagination use scroll() (snapshots, exports) or search_after (infinite scroll) via raw body().
  • Default is no refresh interval tuning. For bulk reindex/backfill, set refresh_interval: -1 + number_of_replicas: 0 during the load and restore after — otherwise you will melt the cluster.
  • Timestamp fields need a date mapping. $timestamps = true writes created_at/updated_at; if the mapping is text/keyword you will get rejected docs and only find out via bulk error responses.
  • Check bulk responses. bulk() returns the raw response with a errors flag; silently ignoring it means half your data is missing. Loop over items[] and log error entries.
  • Nested fields require a nested mapping and whereNested(). where('comments.author', 'x') on a nested-mapped field returns nothing sensible — always wrap with a nested query.
  • _source filtering is not automatic. Use select() or ->source([...]); otherwise you drag full documents around and hydrate models you throw away.
  • Debugging workflow: enableQueryLog() / getQueryLog() (if present in your version), and ->body() to capture the exact JSON DSL. Reproduce it in Kibana Dev Tools — the builder hides mistakes well.
  • Result sorting must reference mapped fields. orderBy('price', 'desc') on an unmapped/text field fails at query time; for numeric sorts ensure the field is integer/float/keyword.
  • Config quirks: the hosts array form (['https://user:pass@host:9200']) bypasses the host/port/user/pass keys; do not mix them. Set ELASTICSEARCH_SCHEME explicitly if you terminate TLS at a proxy, or you get a confusing connection reset.
  • Extension points: subclass Basemkhirat\Elasticsearch\Model for shared scopes; subclass Query if you need new DSL verbs; a custom Collection is possible but rarely worth it — hydrate into your own value objects instead.
  • Laravel version drift: on Laravel 9+ verify the provider still registers (some releases relied on helpers that moved). If auto-discovery skips it, register manually and confirm Elasticsearch::connection() works before debugging anything else.
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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect