PHPackages                             huscakmak/elasticsearch - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. huscakmak/elasticsearch

ActivePackage[Database &amp; ORM](/categories/database)

huscakmak/elasticsearch
=======================

The missing elasticsearch ORM for Laravel!

v1.7(4y ago)1591MITPHPPHP &gt;=7.3|^8.0

Since Sep 9Pushed 4y ago1 watchersCompare

[ Source](https://github.com/huscakmak/elasticsearch)[ Packagist](https://packagist.org/packages/huscakmak/elasticsearch)[ RSS](/packages/huscakmak-elasticsearch/feed)WikiDiscussions main Synced today

READMEChangelog (7)Dependencies (11)Versions (9)Used By (0)

[![Latest Stable Version](https://camo.githubusercontent.com/322e4feb000cd96b50e8fd8abad7b02e5ac8321a7644399cee82480422827be2/68747470733a2f2f706f7365722e707567782e6f72672f68757363616b6d616b2f656c61737469637365617263682f76)](https://packagist.org/packages/huscakmak/elasticsearch) [![Total Downloads](https://camo.githubusercontent.com/922af5a874c5901e0aaa761824920053f85c0dea880e73871b41f33deb8722d8/68747470733a2f2f706f7365722e707567782e6f72672f68757363616b6d616b2f656c61737469637365617263682f646f776e6c6f616473)](https://packagist.org/packages/huscakmak/elasticsearch) [![Latest Unstable Version](https://camo.githubusercontent.com/6c4de81563d49391ad3cc5b4d6439533fa5dc368216c3858b8ab95c736833851/68747470733a2f2f706f7365722e707567782e6f72672f68757363616b6d616b2f656c61737469637365617263682f762f756e737461626c65)](https://packagist.org/packages/huscakmak/elasticsearch) [![License](https://camo.githubusercontent.com/5cacc837677a2995cc34adb43c8c27d02e8b70830e33f10528807cb9d3ed00a7/68747470733a2f2f706f7365722e707567782e6f72672f68757363616b6d616b2f656c61737469637365617263682f6c6963656e7365)](https://packagist.org/packages/huscakmak/elasticsearch)

Laravel Elasticsearch Integration
=================================

[](#laravel-elasticsearch-integration)

This is a fork of the excellent library by [@basemkhirat](https://github.com/basemkhirat), who sadly seems to have abandoned it by now.
As we rely on this library quite heavily, we will attempt to keep it up to date and compatible with newer Laravel and Elasticsearch versions.

**Changes in this fork:**

- Support for Elasticsearch 7.10 and newer
- Support for PHP 7.3 and newer (PHP 8 included!)
- Broadened support for Laravel libraries, allowing you to use it with almost all versions of Laravel
- Type hints in all supported places, giving confidence in all parameters
- Docblock annotations for advanced autocompletion, extensive inline documentation
- Clean separation of connection management into a [`ConnectionManager` class](./src/ConnectionManager.php), while preserving backwards compatibility
- Support for *most* Eloquent model behaviour ([see below](#elasticsearch-models))
- Removed dependencies on Laravel internals

If you're interested in contributing, please submit a PR or open an issue!

**Features:**

- Fluent Elasticsearch query builder with an elegant syntax
- Elasticsearch models inspired by Laravel's Eloquent
- Index management using simple artisan commands
- Limited support for the [Lumen framework](https://lumen.laravel.com/)
- Can be used as a [Laravel Scout](https://laravel.com/docs/8.x/scout) driver
- Parallel usage of multiple Elasticsearch connections
- Built-in pagination based on [Laravel Pagination](https://laravel.com/docs/8.x/pagination)
- Caching queries using a caching layer based on [laravel cache](https://laravel.com/docs/8.x/cache).

**Table of Contents**

- [Requirements](#requirements)
- [Installation](#installation)
    - [Install package using composer](#install-package-using-composer)
        - [Laravel Installation](#laravel-installation)
        - [Lumen Installation](#lumen-installation)
    - [Generic app installation](#generic-app-installation)
- [Configuration (Laravel &amp; Lumen)](#configuration-laravel-lumen)
- [Artisan commands (Laravel &amp; Lumen)](#artisan-commands-laravel-lumen)
    - [`es:indices:list`: List all indices on server](#es-indices-list-list-all-indices-on-server)
    - [`es:indices:create`: Create indices defined in `config/es.php`](#es-indices-create-create-indices-defined-in-config-esphp)
    - [`es:indices:update`: Update indices defined in `config/es.php`](#es-indices-update-update-indices-defined-in--config-esphp)
    - [`es:indices:drop`: Drop index](#es-indices-drop-drop-index)
    - [Reindexing data (with zero downtime)](#reindexing-data-with-zero-downtime)
- [Usage as a Laravel Scout driver](#usage-as-a-laravel-scout-driver)
- [Elasticsearch models](#elasticsearch-models)
    - [Index Names](#index-names)
    - [Connection Names](#connection-names)
    - [Mapping type](#mapping-type)
    - [Default Attribute Values](#default-attribute-values)
    - [Retrieving Models](#retrieving-models)
    - [Adding additional constraints](#adding-additional-constraints)
    - [Collections](#collections)
    - [Chunking Results](#chunking-results)
    - [Retrieving individual Models](#retrieving-individual-models)
    - [Not Found Exceptions](#not-found-exceptions)
    - [Inserting and Updating Models](#inserting-and-updating-models)
        - [Inserts](#inserts)
        - [Updates](#updates)
        - [Examining Attribute Changes](#examining-attribute-changes)
        - [Mass Assignment](#mass-assignment)
        - [Allowing Mass Assignment](#allowing-mass-assignment)
        - [Upserts](#upserts)
        - [Deleting An Existing Model By Its ID](#deleting-an-existing-model-by-its-id)
    - [Query Scopes](#query-scopes)
        - [Global Scopes](#global-scopes)
        - [Local Scopes](#local-scopes)
        - [Dynamic Scopes](#dynamic-scopes)
    - [Comparing Models](#comparing-models)
    - [Events](#events)
    - [Replicating Models](#replicating-models)
    - [Mutators and Casting](#mutators-and-casting)
- [Usage as a query builder](#usage-as-a-query-builder)
- [Releases](#releases)
- [Authors](#authors)
- [Bugs, Suggestions and Contributions](#bugs-suggestions-and-contributions)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP &gt;= `7.3`
    See [Travis CI Builds](https://travis-ci.org/huscakmak/elasticsearch).
- `laravel/laravel` &gt;= 5.\* or `laravel/lumen` &gt;= 5.\* or any other application using composer

Installation
------------

[](#installation)

This section describes the installation process for all supported application types.

### Install package using composer

[](#install-package-using-composer)

Whether you're using Laravel, Lumen or another framework, start by installing the package using composer:

```
composer require Huslab/elasticsearch
```

#### Laravel Installation

[](#laravel-installation)

If you have package autodiscovery disabled, add the service provider and facade to your `config/app.php`:

```
    'providers' => [
        // ...

        Huslab\Elasticsearch\ElasticsearchServiceProvider::class,

        // ...
    ],

    // ...

    'aliases' => [
        // ...

        'ES' => Huslab\Elasticsearch\Facades\ES::class,

        // ...
    ],
```

Lastly, publish the service provider to your configuration directory:

```
php artisan vendor:publish --provider="Huslab\Elasticsearch\ElasticsearchServiceProvider"
```

#### Lumen Installation

[](#lumen-installation)

After installing the package from composer, add package service provider in `bootstrap/app.php`:

```
$app->register(Huslab\Elasticsearch\ElasticsearchServiceProvider::class);
```

Copy the package config directory at `vendor/Huslab/elasticsearch/src/config/` to your project root folder alongside with your `app/` directory:

```
cp -r ./vendor/Huslab/elasticsearch/src/config ./config
```

If you haven't already, make Lumen work with facades by uncommenting this line in `bootstrap/app.php`:

```
$app->withFacades();
```

If you don't want to enable facades in Lumen, you can access the query builder using `app("es")`:

```
app("es")->index("my_index")->type("my_type")->get();

# This is similar to:
ES::index("my_index")->type("my_type")->get();
```

### Generic app installation

[](#generic-app-installation)

You can install package with any composer-based application. While we can't provide general instructions, the following example should give you an idea of how it works:

```
require "vendor/autoload.php";

use Huslab\Elasticsearch\ConnectionManager;
use Huslab\Elasticsearch\Factories\ClientFactory;

$connectionManager = new ConnectionManager([
 'servers' => [
        [
            "host" => '127.0.0.1',
            "port" => 9200,
            'user' => '',
            'pass' => '',
            'scheme' => 'http',
        ],
    ],

	// Custom handlers
	// 'handler' => new MyCustomHandler(),

    'index' => 'my_index',
], new ClientFactory());

$connection = $connectionManager->connection();

// Access the query builder using created connection
$documents = $connection->search("hello")->get();
```

Configuration (Laravel &amp; Lumen)
-----------------------------------

[](#configuration-laravel--lumen)

After publishing the service provider, a configuration file has been created at `config/es.php`. Here, you can add one or more Elasticsearch connections, with multiple servers each. Take a look at the following example:

```
# Here you can define the default connection name.
'default' => env('ELASTIC_CONNECTION', 'default'),

# Here you can define your connections.
'connections' => [
	'default' => [
	    'servers' => [
	        [
	            "host" => env("ELASTIC_HOST", "127.0.0.1"),
	            "port" => env("ELASTIC_PORT", 9200),
	            'user' => env('ELASTIC_USER', ''),
	            'pass' => env('ELASTIC_PASS', ''),
	            'scheme' => env('ELASTIC_SCHEME', 'http'),
	        ]
	    ],

		// Custom handlers
		// 'handler' => new MyCustomHandler(),
		'index' => env('ELASTIC_INDEX', 'my_index')
	]
],

# Here you can define your indices.
'indices' => [
	'my_index_1' => [
	    "aliases" => [
	        "my_index"
	    ],
	    'settings' => [
	        "number_of_shards" => 1,
	        "number_of_replicas" => 0,
	    ],
	    'mappings' => [
	        'posts' => [
                'properties' => [
                    'title' => [
                        'type' => 'string'
                    ]
                ]
	        ]
	    ]
	]
]
```

If you'd like to use Elasticsearch with [Laravel Scout](https://laravel.com/docs/8.x/scout#introduction), you can find the scout specific settings in `config/scout.php`.

Artisan commands (Laravel &amp; Lumen)
--------------------------------------

[](#artisan-commands-laravel--lumen)

With the artisan commands included with this package, you can create or update settings, mappings and aliases. Note that all commands use the default connection by default. You can change this by passing the `--connection ` option.

The following commands are available:

### `es:indices:list`: List all indices on server

[](#esindiceslist-list-all-indices-on-server)

```
$ php artisan es:indices:list
+----------------------+--------+--------+----------+------------------------+-----+-----+------------+--------------+------------+----------------+
| configured (es.php)  | health | status | index    | uuid                   | pri | rep | docs.count | docs.deleted | store.size | pri.store.size |
+----------------------+--------+--------+----------+------------------------+-----+-----+------------+--------------+------------+----------------+
| yes                  | green  | open   | my_index | 5URW60KJQNionAJgL6Q2TQ | 1   | 0   | 0          | 0            | 260b       | 260b           |
+----------------------+--------+--------+----------+------------------------+-----+-----+------------+--------------+------------+----------------+

```

### `es:indices:create`: Create indices defined in `config/es.php`

[](#esindicescreate-create-indices-defined-in-configesphp)

Note that creating operation skips the index if exists.

```
# Create all indices in config file.
php artisan es:indices:create

# Create only 'my_index' index in config file
php artisan es:indices:create my_index
```

### `es:indices:update`: Update indices defined in `config/es.php`

[](#esindicesupdate-update-indices-defined-in-configesphp)

Note that updating operation updates indices setting, aliases and mapping and doesn't delete the indexed data.

```
# Update all indices in config file.
php artisan es:indices:update

# Update only 'my_index' index in config file
php artisan es:indices:update my_index
```

### `es:indices:drop`: Drop index

[](#esindicesdrop-drop-index)

**Be careful when using this command, as you will lose your index data!**
Running drop command with `--force` option will skip all confirmation messages.

```
# Drop all indices in config file.
php artisan es:indices:drop

# Drop specific index on sever. Not matter for index to be exist in config file or not.
php artisan es:indices:drop my_index
```

### Reindexing data (with zero downtime)

[](#reindexing-data-with-zero-downtime)

**First, why reindexing?**
Changing index mapping doesn't reflect without data reindexing, otherwise your search results will not work on the right way.
To avoid down time, your application should work with index `alias` not index `name`.
The index `alias` is a constant name that application should work with to avoid change index names.

**Assume that we want to change mapping for `my_index`, this is how to do that:**

1. Add `alias` as example `my_index_alias` to `my_index` configuration and make sure your application is working with it.

    ```
    "aliases" => [
        "my_index_alias"
    ]
    ```
2. Update index with command:

    ```
    php artisan es:indices:update my_index
    ```
3. Create a new index as example `my_new_index` with your new mapping in configuration file.

    ```
    $ php artisan es:indices:create my_new_index
    ```
4. Reindex data from `my_index` into `my_new_index` with command:

    ```
    php artisan es:indices:reindex my_index my_new_index

    # Control bulk size. Adjust it with your server.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000

    # Control query scroll value.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000 --scroll=2m

    # Skip reindexing errors such as mapper parsing exceptions.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000 --skip-errors

    # Hide all reindexing errors and show the progres bar only.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000 --skip-errors --hide-errors
    ```
5. Remove `my_index_alias` alias from `my_index` and add it to `my_new_index` in configuration file and update with command:

    ```
    php artisan es:indices:update
    ```

Usage as a Laravel Scout driver
-------------------------------

[](#usage-as-a-laravel-scout-driver)

First, follow [Laravel Scout installation](https://laravel.com/docs/8.0/scout#installation).
All you have to do is updating the following lines in `config/scout.php`:

```
# change the default driver to 'es'
'driver' => env('SCOUT_DRIVER', 'es'),

# link `es` driver with default elasticsearch connection in config/es.php
'es' => [
    'connection' => env('ELASTIC_CONNECTION', 'default'),
],
```

Have a look at [Laravel Scout documentation](https://laravel.com/docs/8.0/scout#configuration), too!

Elasticsearch models
--------------------

[](#elasticsearch-models)

Each index type has a corresponding *"Model"* which is used to interact with that type. Models allow you to query for data in your types or indices, as well as insert new documents into the type. Elasticsearch Models mimic Eloquent models as closely as possible: You can use model events, route bindings, advanced attribute methods and more. **If there is any Eloquent functionality you're missing, open an issue, and we'll be happy to add it!**.

> **Supported features:**
>
> - Attributes
> - Events
> - Route bindings
> - Global and Local Query Scopes
> - Replicating models

A minimal model might look like this:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    // ...
}
```

### Index Names

[](#index-names)

This model is not specifically bound to any index and will simply use the index configured for the given Elasticsearch connection. To specifically target an index, you may define an `index` property on the model:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    protected $index = 'posts';
}
```

### Connection Names

[](#connection-names)

By default, all Elasticsearch models will use the default connection that's configured for your application. If you would like to specify a different connection that should be used when interacting with a particular model, you should define a $connection property on the model:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    protected $connection = 'blag';
}
```

### Mapping type

[](#mapping-type)

If you're still using mapping types, you may add a `type` property to your model to indicate the mapping `_type` to be used for queries.

> **Mapping Types are deprecated:**
> Please note that Elastic has [deprecated mapping types](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html) and will remove them in the next major release. You should not rely on them to continue working.

```
namespace App;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    protected $type = 'posts';
}
```

### Default Attribute Values

[](#default-attribute-values)

By default, a newly instantiated model instance will not contain any attribute values. If you would like to define the default values for some of your model's attributes, you may define an `attributes` property on your model:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    protected $attributes = [
        'published' => false,
    ];
}
```

### Retrieving Models

[](#retrieving-models)

Once you have created a model and its associated index type, you are ready to start retrieving data from your index. You can think of your Elasticsearch model as a powerful query builder allowing you to fluently query the index associated with the model. The model's `all` method will retrieve all the documents from the model's associated Elasticsearch index:

```
use App\Models\Post;

foreach (Post::all() as $post) {
    echo $post->title;
}
```

### Adding additional constraints

[](#adding-additional-constraints)

The `all` method will return all the results in the model's index. However, since each Elasticsearch model serves as a query builder, you may add additional constraints to queries, and then invoke the `get()` method to retrieve the results:

```
use App\Models\Post;

$posts = Post::where('status', 1)
             ->orderBy('created_at', 'desc')
             ->take(10)
             ->get();
```

### Collections

[](#collections)

As we have seen, Elasticsearch methods like `all` and `get` retrieve multiple documents from the index. However, these methods don't return a plain PHP array. Instead, an instance of [`Huslab\Elasticsearch\Collection`](./src/Collection.php) is returned.

The Elasticsearch `Collection` class extends Laravel's base `Illuminate\Support\Collection` class, which provides a [variety of helpful methods](https://laravel.com/docs/master/collections#available-methods) for interacting with data collections. For example, the `reject`method may be used to remove models from a collection based on the results of an invoked closure:

```
use App\Models\Post;

$posts = Post::where('sponsored', true)->get();
$posts = $posts->reject($post => $post->in_review);
```

In addition to the methods provided by Laravel's base collection class, the Elasticsearch collection class provides a few extra methods that are specifically intended for interacting with collections of Elasticsearch models:

#### Result Meta data

[](#result-meta-data)

Elasticsearch provides a few additional fields in addition to the hits of a query, like the total result amount, or the query execution time. The Elasticsearch collection provides getters for these properties:

```
use App\Models\Post;

$posts = Post::all();
$total = $posts->getTotal();
$maxScore = $posts->getMaxScore();
$duration = $posts->getDuration();
$isTimedOut = $posts->isTimedOut();
$scrollId = $posts->getScrollId();
$shards = $posts->getShards();
```

#### Iterating

[](#iterating)

Since all of Laravel's collections implement PHP's `iterable` interfaces, you may loop over collections as if they were an array:

```
foreach ($title as $title) {
    echo $post->title;
}
```

### Chunking Results

[](#chunking-results)

Elasticsearch indices can grow quite huge. Your application may run out of memory if you would attempt to load tens of thousands of Elasticsearch documents via the `all` or `get` methods without an upper bound. Therefore, the default amount of documents fetched is set to `10`. To change this, use the `take` method:

```
use App\Models\Post;

$posts = Post::take(500)->get();
```

### Retrieving individual Models

[](#retrieving-individual-models)

In addition to retrieving all the documents matching a given query, you may also retrieve single documents using the `find`, `first`, or `firstWhere` methods. Instead of returning a collection of models, these methods return a single model instance:

```
use App\Models\Post;

// Retrieve a model by its ID...
$posts = Post::find('AVp_tCaAoV7YQD3Esfmp');

// Retrieve the first model matching the query constraints...
$post = Post::where('published', 1)->first();

// Alternative to retrieving the first model matching the query constraints...
$post = Post::firstWhere('published', 1);```
```

Sometimes you may wish to retrieve the first result of a query or perform some other action if no results are found. The `firstOr` method will return the first result matching the query or, if no results are found, execute the given closure. The value returned by the closure will be considered the result of the `firstOr` method:

```
use App\Models\Post;

$model = Post::where('tags', '>', 3)->firstOr(function () {
    // ...
});
```

### Not Found Exceptions

[](#not-found-exceptions)

Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The `findOrFail` and `firstOrFail`methods will retrieve the first result of the query; however, if no result is found, a [`Huslab\Elasticsearch\Exceptions\DocumentNotFoundException`](./src/Exceptions/DocumentNotFoundException.php) will be thrown:

```
$post = Post::findOrFail('AVp_tCaAoV7YQD3Esfmp');

$post = Post::where('published', true)->firstOrFail();
```

If the `DocumentNotFoundException` is not caught, a 404 HTTP response is automatically sent back to the client:

```
use App\Models\Post;

Route::get('/api/posts/{id}', function ($id) {
    return Post::findOrFail($id);
});
```

### Inserting and Updating Models

[](#inserting-and-updating-models)

#### Inserts

[](#inserts)

To insert a new document into the index, you should instantiate a new model instance and set attributes on the model. Then, call the `save` method on the model instance:

```
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * Create a new post instance.
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request): Response
    {
        // Validate the request...

        $post = new Post;
        $post->title = $request->title;

        $post->save();
    }
}
```

In this example, we assign the `name` field from the incoming HTTP request to the `name` attribute of the `App\Models\Post` model instance. When we call the `save` method, a document will be inserted into the index.

Alternatively, you may use the `create` method to "save" a new model using a single PHP statement. The inserted model instance will be returned to you by the `create` method:

```
use App\Models\Post;

$post = Post::create([
'title' => 'Searching efficiently',
]);
```

However, before using the create method, you will need to specify either a `fillable` or `guarded` property on your model class. These properties are required because all Elasticsearch models are protected against mass assignment vulnerabilities by default. To learn more about mass assignment, please consult the [mass assignment documentation](https://laravel.com/docs/8.x/eloquent#mass-assignment).

#### Updates

[](#updates)

The `save` method may also be used to update models that already exist in the index. To update a model, you should retrieve it and set any attributes you wish to update. Then, you should call the model's `save` method.

The `save()` method may also be used to update models that already exist. To update a model, you should retrieve it, set any attributes you wish to update, and then call the save method.

```
use App\Models\Post;

$post = Post::find('AVp_tCaAoV7YQD3Esfmp');

$post->title = 'Modified Post Title';

$post->save();
```

#### Examining Attribute Changes

[](#examining-attribute-changes)

Elasticsearch provides the `isDirty`, `isClean`, and `wasChanged` methods to examine the internal state of your model and determine how its attributes have changed from when the model was originally retrieved.

The `isDirty` method determines if any of the model's attributes have been changed since the model was retrieved. You may pass a specific attribute name to the `isDirty` method to determine if a particular attribute is *dirty*. The `isClean` will determine if an attribute has remained unchanged since the model was retrieved. This method also accepts an optional attribute argument:

```
use App\Models\Author;

$author = Author::create([
'first_name' => 'Moritz',
'last_name' => 'Friedrich',
'title' => 'Developer',
]);

$author->title = 'Painter';

$author->isDirty(); // true
$author->isDirty('title'); // true
$author->isDirty('first_name'); // false

$author->isClean(); // false
$author->isClean('title'); // false
$author->isClean('first_name'); // true

$author->save();

$author->isDirty(); // false
$author->isClean(); // true
```

The `wasChanged` method determines if any attributes were changed when the model was last saved within the current request cycle. If needed, you may pass an attribute name to see if a particular attribute was changed:

```
use App\Models\Author;

$author = Author::create([
'first_name' => 'Taylor',
'last_name' => 'Otwell',
'title' => 'Developer',
]);

$author->title = 'Painter';

$author->save();

$author->wasChanged(); // true
$author->wasChanged('title'); // true
$author->wasChanged('first_name'); // false
```

The `getOriginal` method returns an array containing the original attributes of the model regardless of any changes to the model since it was retrieved. If needed, you may pass a specific attribute name to get the original value of a particular attribute:

```
use App\Models\Author;

$author = Author::find(1);

$author->name; // John
$author->email; // john@example.com

$author->name = "Jack";
$author->name; // Jack

$author->getOriginal('name'); // John
$author->getOriginal(); // Array of original attributes...
```

#### Mass Assignment

[](#mass-assignment)

You may use the `create` method to "save" a new model using a single PHP statement. The inserted model instance will be returned to you by the method:

```
use App\Models\Post;

$post = Post::create([
    'title' => 'Searching effectively',
]);
```

However, before using the `create` method, you will need to specify either a `fillable` or `guarded` property on your model class. These properties are required because all Elasticsearch models are protected against mass assignment vulnerabilities by default.

A mass assignment vulnerability occurs when a user passes an unexpected HTTP request field and that field changes a field in your index that you did not expect.

So, to get started, you should define which model attributes you want to make mass assignable. You may do this using the `fillable` property on the model. For example, let's make the `title` attribute of our `Post` model mass assignable:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['title'];
}
```

Once you have specified which attributes are mass assignable, you may use the `create` method to insert a new document in the index. The `create` method returns the newly created model instance:

```
$post = Post::create(['title' => 'Searching effectively']);
```

If you already have a model instance, you may use the `fill` method to populate it with an array of attributes:

```
$post->fill(['title' => 'Searching more effectively']);
```

#### Allowing Mass Assignment

[](#allowing-mass-assignment)

If you would like to make all of your attributes mass assignable, you may define your model's `guarded` property as an empty array. If you choose to un-guard your model, you should take special care to always hand-craft the arrays passed to Elasticsearch's `fill`, `create`, and `update` methods:

```
/**
 * The attributes that aren't mass assignable.
 *
 * @var array
 */
protected $guarded = [];
```

#### Upserts

[](#upserts)

There is currently no convenience wrapper for upserting documents (inserting or updating depending on whether models exist). If you're interested in such a capability, please open an issue.

##### Deleting Models

[](#deleting-models)

To delete a model, call the `delete` method on a model instance:

```
use App\Models\Post;

$post = Post::find('AVp_tCaAoV7YQD3Esfmp');

$post->delete();
```

#### Deleting An Existing Model By Its ID

[](#deleting-an-existing-model-by-its-id)

In the example above, we are retrieving the model from the index before calling the `delete` method. However, if you know the ID of the model, you may delete the model without explicitly retrieving it by calling the `destroy` method. In addition to accepting the single ID, the `destroy` method will accept multiple IDs, an array of IDs, or a collection of IDs:

```
use App\Models\Post;

Post::destroy(1);

Post::destroy(1, 2, 3);

Post::destroy([1, 2, 3]);

Post::destroy(collect([1, 2, 3]));
```

> **Important:**
> The `destroy` method loads each model individually and calls the `delete` method so that the `deleting` and `deleted` events are properly dispatched for each model.

### Query Scopes

[](#query-scopes)

Query scopes are implemented exactly the way as they are in Eloquent.

#### Global Scopes

[](#global-scopes)

Global scopes allow you to add constraints to all queries for a given model. Writing your own global scopes can provide a convenient, easy way to make sure every query for a given model receives certain constraints.

##### Writing Global Scopes

[](#writing-global-scopes)

Writing a global scope is simple. First, define a class that implements the [`Huslab\Elasticsearch\Interfaces\ScopeInterface`](./src/Interfaces/ScopeInterface.php) interface. Laravel does not have a conventional location that you should place scope classes, so you are free to place this class in any directory that you wish.

The `ScopeInterface` requires you to implement one method: `apply`. The `apply` method may add constraints or other types of clauses to the query as needed:

```
namespace App\Scopes;

use Huslab\Elasticsearch\Query;
use Huslab\Elasticsearch\Model;
use Huslab\Elasticsearch\Interfaces\ScopeInterface;

class AncientScope implements ScopeInterface
{
    /**
     * Apply the scope to a given Elasticsearch query builder.
     *
     * @param  \Huslab\Elasticsearch\Query  $query
     * @param  \Huslab\Elasticsearch\Model  $model
     * @return void
     */
    public function apply(Query $query, Model $model)
    {
        $query->where('created_at', 'get();
```

```
// Remove some of the global scopes...
Post::withoutGlobalScopes([
    FirstScope::class,
    SecondScope::class
])->get();
```

#### Local Scopes

[](#local-scopes)

Local scopes allow you to define common sets of query constraints that you may easily re-use throughout your application. For example, you may need to frequently retrieve all posts that are considered "popular".

##### Writing local scopes

[](#writing-local-scopes)

To define a scope, prefix an Elasticsearch model method with scope. Scopes should always return a query builder instance:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    /**
     * Scope a query to only include popular posts.
     *
     * @param  \Huslab\Elasticsearch\Query  $query
     * @return \Huslab\Elasticsearch\Query
     */
    public function scopePopular(Query $query): Query
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include published posts.
     *
     * @param  \Huslab\Elasticsearch\Query  $query
     * @return \Huslab\Elasticsearch\Query
     */
    public function scopePublished(Query $query): Query
    {
        return $query->where('published', 1);
    }
}
```

##### Utilizing local scopes

[](#utilizing-local-scopes)

Once the scope has been defined, you may call the scope methods when querying the model. However, you should not include the scope prefix when calling the method. You can even chain calls to various scopes:

```
use App\Models\Post;

$posts = Post::popular()->published()->orderBy('created_at')->get();
```

#### Dynamic Scopes

[](#dynamic-scopes)

Sometimes you may wish to define a scope that accepts parameters. To get started, just add your additional parameters to your scope method's signature. Scope parameters should be defined after the `$query` parameter:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    /**
     * Scope a query to only include posts of a given type.
     *
     * @param  \Huslab\Elasticsearch\Query  $query
     * @param  mixed  $type
     * @return \Huslab\Elasticsearch\Query
     */
    public function scopeOfType(Query $query, $type): Query
    {
        return $query->where('type', $type);
    }
}
```

Once the expected arguments have been added to your scope method's signature, you may pass the arguments when calling the scope:

```
$posts = Post::ofType('news')->get();

```

### Comparing Models

[](#comparing-models)

Sometimes you may need to determine if two models are the "same". The is method may be used to quickly verify two models have the same ID, index, type, and connection:

```
if ($post->is($anotherPost)) {
    //
}
```

### Events

[](#events)

Elasticsearch models dispatch several events, allowing you to hook into the following moments in a model's lifecycle: `retrieved`, `creating`, `created`, `updating`, `updated`, `saving`, `saved`, `deleting`, `deleted`, `restoring`, `restored`, and `replicating`.

The `retrieved` event will dispatch when an existing model is retrieved from the index. When a new model is saved for the first time, the `creating` and `created` events will dispatch. The `updating` / `updated` events will dispatch when an existing model is modified, and the `save` method is called. The `saving` / `saved` events will dispatch when a model is created or updated - even if the model's attributes have not been changed.

To start listening to model events, define a `dispatchesEvents` property on your Elasticsearch model. This property maps various points of the Elasticsearch model's lifecycle to your own [event classes](https://laravel.com/docs/8.x/events). Each model event class should expect to receive an instance of the affected model via its constructor:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;
use App\Events\UserDeleted;
use App\Events\UserSaved;

class Post extends Model
{
    /**
     * The event map for the model.
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => PostSaved::class,
        'deleted' => PostDeleted::class,
    ];
}
```

After defining and mapping your events, you may use [event listeners](https://laravel.com/docs/8.x/events#defining-listeners) to handle the events.

#### Using Closures

[](#using-closures)

Instead of using custom event classes, you may register closures that execute when various model events are dispatched. Typically, you should register these closures in the `booted` method of your model:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class Post extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted(): void
    {
        static::created(function ($post) {
            //
        });
    }
}
```

If needed, you may utilize [queueable anonymous event listeners](https://laravel.com/docs/8.x/events#queuable-anonymous-event-listeners) when registering model events. This will instruct Laravel to execute the model event listener in the background using your application's [queue](https://laravel.com/docs/8.x/queues):

```
use function Illuminate\Events\queueable;

static::created(queueable(function ($post): void {
    //
}));
```

##### Accessors &amp; Mutators

[](#accessors--mutators)

###### Defining An Accessor

[](#defining-an-accessor)

To define an `accessor`, create a `getFooAttribute` method on your model where `Foo` is the "studly" cased name of the field you wish to access. In this example, we'll define an accessor for the `title` attribute. The accessor will automatically be called by model when attempting to retrieve the value of the `title` attribute:

```
namespace App;

use Huslab\Elasticsearch\Model;

class post extends Model
{
    /**
     * Get the post title.
     *
     * @param  string  $value
     * @return string
     */
    public function getTitleAttribute(string $value): string
    {
        return ucfirst($value);
    }
}
```

As you can see, the original value of the field is passed to the accessor, allowing you to manipulate and return the value. To access the value of the accessor, you may simply access the `title` attribute on a model instance:

```
$post = App\Post::find(1);

$title = $post->title;
```

Occasionally, you may need to add array attributes that do not have a corresponding field in your index. To do so, simply define an accessor for the value:

```
public function getIsPublishedAttribute(): bool
{
    return $this->attributes['status'] === 1;
}
```

Once you have created the accessor, just add the value to the `appends` property on the model:

```
protected $appends = ['is_published'];
```

Once the attribute has been added to the appends list, it will be included in model's array.

###### Defining A Mutator

[](#defining-a-mutator)

To define a mutator, define a `setFooAttribute` method on your model where `Foo` is the "studly" cased name of the field you wish to access. So, again, let's define a mutator for the `title` attribute. This mutator will be automatically called when we attempt to set the value of the `title`attribute on the model:

```
namespace App;

use Huslab\Elasticsearch\Model;

class post extends Model
{
    /**
     * Set the post title.
     *
     * @param  string  $value
     * @return string
     */
    public function setTitleAttribute(string $value): string
    {
        return strtolower($value);
    }
}
```

The mutator will receive the value that is being set on the attribute, allowing you to manipulate the value and set the manipulated value on the model's internal `$attributes` property. So, for example, if we attempt to set the title attribute to `Awesome post to read`:

```
$post = App\Post::find(1);

$post->title = 'Awesome post to read';
```

In this example, the setTitleAttribute function will be called with the value `Awesome post to read`. The mutator will then apply the strtolower function to the name and set its resulting value in the internal $attributes array.

#### Muting Events

[](#muting-events)

You may occasionally need to temporarily "mute" all events fired by a model. You may achieve this using the `withoutEvents` method. The `withoutEvents` method accepts a closure as its only argument. Any code executed within this closure will not dispatch model events. For example, the following example will fetch and delete an `App\Models\Post` instance without dispatching any model events. Any value returned by the closure will be returned by the `withoutEvents` method:

```
use App\Models\Post;

$post = Post::withoutEvents(function () use () {
Post::findOrFail(1)->delete();

    return Post::find(2);
});
```

#### Saving A Single Model Without Events

[](#saving-a-single-model-without-events)

Sometimes you may wish to "save" a given model without dispatching any events. You may accomplish this using the `saveQuietly` method:

```
$post = Post::findOrFail(1);

$post->title = 'Other search strategies';

$post->saveQuietly();
```

### Replicating Models

[](#replicating-models)

You may create an unsaved copy of an existing model instance using the replicate method. This method is particularly useful when you have model instances that share many of the same attributes:

```
use App\Models\Address;

$shipping = Address::create([
    'type' => 'shipping',
    'line_1' => '123 Example Street',
    'city' => 'Victorville',
    'state' => 'CA',
    'postcode' => '90001',
]);

$billing = $shipping->replicate()->fill([
    'type' => 'billing'
]);

$billing->save();
```

### Mutators and Casting

[](#mutators-and-casting)

Accessors, mutators, and attribute casting allow you to transform Elasticsearch attribute values when you retrieve or set them on model instances. For example, you may want to use the [Laravel encrypter](https://laravel.com/docs/8.x/encryption) to encrypt a value while it is stored in the index, and then automatically decrypt the attribute when you access it on an Elasticsearch model. Or, you may want to convert a JSON string that is stored in your index to an array when it is accessed via your Elasticsearch model.

#### Accessors &amp; Mutators

[](#accessors--mutators-1)

##### Defining An Accessor

[](#defining-an-accessor-1)

An accessor transforms an Elasticsearch attribute value when it is accessed. To define an accessor, create a `get{Attribute}Attribute` method on your model where `{Attribute}` is the "studly" cased name of the field you wish to access.

In this example, we'll define an accessor for the `first_name` attribute. The accessor will automatically be called by Elasticsearch when attempting to retrieve the value of the `first_name` attribute:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class User extends Model
{
    /**
     * Get the user's first name.
     *
     * @param  string  $value
     * @return string
     */
    public function getFirstNameAttribute(string $value): string
    {
        return ucfirst($value);
    }
}
```

As you can see, the original value of the field is passed to the accessor, allowing you to manipulate and return the value. To access the value of the accessor, you may simply access the `first_name` attribute on a model instance:

```
use App\Models\User;

$user = User::find(1);

$firstName = $user->first_name;
```

You are not limited to interacting with a single attribute within your accessor. You may also use accessors to return new, computed values from existing attributes:

```
/**
 * Get the user's full name.
 *
 * @return string
 */
public function getFullNameAttribute(): string
{
    return "{$this->first_name} {$this->last_name}";
}
```

##### Defining A Mutator

[](#defining-a-mutator-1)

A mutator transforms an Elasticsearch attribute value when it is set. To define a mutator, define a `set{Attribute}Attribute` method on your model where `{Attribute}` is the "studly" cased name of the field you wish to access.

Let's define a mutator for the `first_name` attribute. This mutator will be automatically called when we attempt to set the value of the `first_name` attribute on the model:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class User extends Model
{
    /**
     * Set the user's first name.
     *
     * @param  string  $value
     * @return void
     */
    public function setFirstNameAttribute(string $value): void
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}
```

The mutator will receive the value that is being set on the attribute, allowing you to manipulate the value and set the manipulated value on the Elasticsearch model's internal `$attributes` property. To use our mutator, we only need to set the `first_name` attribute on an Elasticsearch model:

```
use App\Models\User;

$user = User::find(1);

$user->first_name = 'Sally';
```

In this example, the `setFirstNameAttribute` function will be called with the value `Sally`. The mutator will then apply the `strtolower` function to the name and set its resulting value in the internal `$attributes` array.

#### Attribute Casting

[](#attribute-casting)

Attribute casting provides functionality similar to accessors and mutators without requiring you to define any additional methods on your model. Instead, your model's `$casts` property provides a convenient method of converting attributes to common data types.

The `$casts` property should be an array where the key is the name of the attribute being cast, and the value is the type you wish to cast the field to. The supported cast types are:

- `array`
- `boolean`
- `collection`
- `date`
- `datetime`
- `decimal:`
- `double`
- `encrypted`
- `encrypted:array`
- `encrypted:collection`
- `encrypted:object`
- `float`
- `integer`
- `object`
- `real`
- `string`
- `timestamp`

To demonstrate attribute casting, let's cast the `is_admin` attribute, which is stored in our index as an integer (`0` or `1`) to a boolean value:

```
namespace App\Models;

use Huslab\Elasticsearch\Model;

class User extends Model
{
    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'is_admin' => 'boolean',
    ];
}
```

After defining the cast, the `is_admin` attribute will always be cast to a boolean when you access it, even if the underlying value is stored in the index as an integer:

```
$user = App\Models\User::find(1);

if ($user->is_admin) {
    //
}
```

> **Note:** Attributes that are `null` will not be cast.

##### Date Casting

[](#date-casting)

You may cast date attributes by defining them within your model's `$cast` property array. Typically, dates should be cast using the `datetime` cast.

When defining a `date` or `datetime` cast, you may also specify the date's format. This format will be used when the [model is serialized to an array or JSON](https://laravel.com/docs/8.x/eloquent-serialization):

```
/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'created_at' => 'datetime:Y-m-d',
];
```

When a field is cast as a date, you may set its value to a UNIX timestamp, date string (`Y-m-d`), date-time string, or a `DateTime` / `Carbon` instance. The date's value will be correctly converted and stored in your index:

You may customize the default serialization format for all of your model's dates by defining a `serializeDate` method on your model. This method does not affect how your dates are formatted for storage in the index:

```
/**
 * Prepare a date for array / JSON serialization.
 *
 * @param  \DateTimeInterface  $date
 * @return string
 */
protected function serializeDate(DateTimeInterface $date)
{
    return $date->format('Y-m-d');
}
```

To specify the format that should be used when actually storing a model's dates within your index, you should define a `$dateFormat` property on your model:

```
/**
 * The storage format of the model's date fields.
 *
 * @var string
 */
protected $dateFormat = 'U';
```

#### Custom Casts

[](#custom-casts)

Laravel has a variety of built-in, helpful cast types; however, you may occasionally need to define your own cast types. You may accomplish this by defining a class that implements the `CastsAttributes` interface.

Classes that implement this interface must define a `get` and `set` method. The `get` method is responsible for transforming a raw value from the index into a cast value, while the `set` method should transform a cast value into a raw value that can be stored in the index. As an example, we will re-implement the built-in `json` cast type as a custom cast type:

> **Note:** Due to type incompatibility, you will need to use different casts for Eloquent and Elasticsearch models, or omit the parameter type.

```
namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Json implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Huslab\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return array
     */
    public function get($model, $key, $value, $attributes)
    {
        return json_decode($value, true);
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Huslab\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return json_encode($value);
    }
}
```

Once you have defined a custom cast type, you may attach it to a model attribute using its class name:

```
    namespace App\Models;

    use App\Casts\Json;
    use Huslab\Elasticsearch\Model;

    class User extends Model
    {
        /**
         * The attributes that should be cast.
         *
         * @var array
         */
        protected $casts = [
            'options' => Json::class,
        ];
    }
```

##### Value Object Casting

[](#value-object-casting)

You are not limited to casting values to primitive types. You may also cast values to objects. Defining custom casts that cast values to objects is very similar to casting to primitive types; however, the `set` method should return an array of key / value pairs that will be used to set raw, storable values on the model.

As an example, we will define a custom cast class that casts multiple model values into a single `Address` value object. We will assume the `Address` value has two public properties: `lineOne` and `lineTwo`:

```
namespace App\Casts;

use App\Models\Address as AddressModel;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;

class Address implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Huslab\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return \App\Models\Address
     */
    public function get($model, $key, $value, $attributes)
    {
        return new AddressModel(
            $attributes['address_line_one'],
            $attributes['address_line_two']
        );
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Huslab\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  \App\Models\Address  $value
     * @param  array  $attributes
     * @return array
     */
    public function set($model, $key, $value, $attributes)
    {
        if (! $value instanceof AddressModel) {
            throw new InvalidArgumentException('The given value is not an Address instance.');
        }

        return [
            'address_line_one' => $value->lineOne,
            'address_line_two' => $value->lineTwo,
        ];
    }
}
```

When casting to value objects, any changes made to the value object will automatically be synced back to the model before the model is saved:

```
use App\Models\User;

$user = User::find(1);

$user->address->lineOne = 'Updated Address Value';

$user->save();
```

> **Tip:** If you plan to serialize your Elasticsearch models containing value objects to JSON or arrays, you should implement the `Illuminate\Contracts\Support\Arrayable` and `JsonSerializable` interfaces on the value object.

##### Array / JSON Serialization

[](#array--json-serialization)

When an Elasticsearch model is converted to an array or JSON using the `toArray` and `toJson` methods, your custom cast value objects will typically be serialized as well as long as they implement the `Illuminate\Contracts\Support\Arrayable` and `JsonSerializable` interfaces. However, when using value objects provided by third-party libraries, you may not have the ability to add these interfaces to the object.

Therefore, you may specify that your custom cast class will be responsible for serializing the value object. To do so, your custom class cast should implement the `Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes` interface. This interface states that your class should contain a `serialize` method which should return the serialized form of your value object:

```
/**
 * Get the serialized representation of the value.
 *
 * @param  \Illuminate\Database\Eloquent\Model|\Huslab\Elasticsearch\Model  $model
 * @param  string  $key
 * @param  mixed  $value
 * @param  array  $attributes
 * @return mixed
 */
public function serialize($model, string $key, $value, array $attributes)
{
    return (string) $value;
}
```

##### Inbound Casting

[](#inbound-casting)

Occasionally, you may need to write a custom cast that only transforms values that are being set on the model and does not perform any operations when attributes are being retrieved from the model. A classic example of an inbound only cast is a "hashing" cast. Inbound only custom casts should implement the `CastsInboundAttributes` interface, which only requires a `set` method to be defined.

```
namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;

class Hash implements CastsInboundAttributes
{
    /**
     * The hashing algorithm.
     *
     * @var string
     */
    protected $algorithm;

    /**
     * Create a new cast class instance.
     *
     * @param  string|null  $algorithm
     * @return void
     */
    public function __construct($algorithm = null)
    {
        $this->algorithm = $algorithm;
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Huslab\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return is_null($this->algorithm)
                    ? bcrypt($value)
                    : hash($this->algorithm, $value);
    }
}
```

##### Cast Parameters

[](#cast-parameters)

When attaching a custom cast to a model, cast parameters may be specified by separating them from the class name using a `:` character and comma-delimiting multiple parameters. The parameters will be passed to the constructor of the cast class:

```
/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'secret' => Hash::class.':sha256',
];
```

##### Castables

[](#castables)

You may want to allow your application's value objects to define their own custom cast classes. Instead of attaching the custom cast class to your model, you may alternatively attach a value object class that implements the `Illuminate\Contracts\Database\Eloquent\Castable` interface:

```
use App\Models\Address;

protected $casts = [
    'address' => Address::class,
];
```

Objects that implement the `Castable` interface must define a `castUsing` method that returns the class name of the custom caster class that is responsible for casting to and from the `Castable` class:

```
namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;

class Address implements Castable
{
    /**
     * Get the name of the caster class to use when casting from / to this cast target.
     *
     * @param  array  $arguments
     * @return string
     */
    public static function castUsing(array $arguments): string
    {
        return AddressCast::class;
    }
}
```

When using `Castable` classes, you may still provide arguments in the `$casts` definition. The arguments will be passed to the `castUsing` method:

```
use App\Models\Address;

protected $casts = [
    'address' => Address::class.':argument',
];
```

##### Castables &amp; Anonymous Cast Classes

[](#castables--anonymous-cast-classes)

By combining "castables" with PHP's [anonymous classes](https://www.php.net/manual/en/language.oop5.anonymous.php), you may define a value object and its casting logic as a single castable object. To accomplish this, return an anonymous class from your value object's `castUsing` method. The anonymous class should implement the `CastsAttributes` interface:

```
namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Address implements Castable
{
    // ...

    /**
     * Get the caster class to use when casting from / to this cast target.
     *
     * @param  array  $arguments
     * @return object|string
     */
    public static function castUsing(array $arguments)
    {
        return new class implements CastsAttributes
        {
            public function get($model, $key, $value, $attributes)
            {
                return new Address(
                    $attributes['address_line_one'],
                    $attributes['address_line_two']
                );
            }

            public function set($model, $key, $value, $attributes)
            {
                return [
                    'address_line_one' => $value->lineOne,
                    'address_line_two' => $value->lineTwo,
                ];
            }
        };
    }
}
```

### Route Model Binding

[](#route-model-binding)

When injecting a model ID to a route or controller action, you will often query the Elasticsearch index to retrieve the model that corresponds to that ID. Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire User model instance that matches the given ID.

#### Implicit Binding

[](#implicit-binding)

Laravel automatically resolves Elasticsearch models defined in routes or controller actions whose type-hinted variable names match a route segment name. For example:

```
use App\Models\Post;

Route::get('/posts/{post}', function (Post $post) {
    return $post->content;
});
```

Since the `$post` variable is type-hinted as the `App\Models\Post` Elasticsearch model, and the variable name matches the `{post}` URI segment, Laravel will automatically inject the model instance that has an ID matching the corresponding value from the request URI. If a matching model instance is not found in the database, a `404` HTTP response will automatically be generated.

Of course, implicit binding is also possible when using controller methods. Again, note the `{post}` URI segment matches the `$post` variable in the controller which contains an `App\Models\Post` type-hint:

```
use App\Http\Controllers\PostController;
use App\Models\Post;

// Route definition...
Route::get('/posts/{post}', [PostController::class, 'show']);

// Controller method definition...
public function show(Post $post): View
{
    return view('post.full', ['post' => $post]);
}
```

#### Customizing The Key

[](#customizing-the-key)

Sometimes you may wish to resolve Elasticsearch models using a field other than `_id`. To do so, you may specify the field in the route parameter definition:

```
use App\Models\Post;

Route::get('/posts/{post:slug}', fn(Post $post): Post => $post);
```

If you would like model binding to always use an index field other than `_id` when retrieving a given model class, you may override the `getRouteKeyName` method on the Elasticsearch model:

```
/**
 * Get the route key for the model.
 *
 * @return string
 */
public function getRouteKeyName(): string
{
    return 'slug';
}
```

#### Customizing Missing Model Behavior

[](#customizing-missing-model-behavior)

Typically, a `404` HTTP response will be generated if an implicitly bound model is not found. However, you may customize this behavior by calling the missing method when defining your route. The missing method accepts a closure that will be invoked if an implicitly bound model can not be found:

```
use App\Http\Controllers\LocationsController;
use Illuminate\Http\Request;

Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
    ->missing(fn(Request $request) => Redirect::route('locations.index')
    ->name('locations.view');
```

#### Explicit Binding

[](#explicit-binding)

You are not required to use Laravel's implicit, convention based model resolution in order to use model binding. You can also explicitly define how route parameters correspond to models. To register an explicit binding, use the router's model method to specify the class for a given parameter. You should define your explicit model bindings at the beginning of the `boot` method of your `RouteServiceProvider` class:

```
use App\Models\Post;
use Illuminate\Support\Facades\Route;

/**
 * Define your route model bindings, pattern filters, etc.
 *
 * @return void
 */
public function boot():void
{
    Route::model('post', Post::class);

    // ...
}
```

Next, define a route that contains a `{post}` parameter:

```
use App\Models\Post;

Route::get('/posts/{post}', function (Post $post) {
    // ...
});
```

Since we have bound all `{post}` parameters to the `App\Models\Post` model, an instance of that class will be injected into the route. So, for example, a request to `posts/1` will inject the `Post` instance from the index which has an ID of `1`.

If a matching model instance is not found in the index, a `404` HTTP response will be automatically generated.

#### Customizing The Resolution Logic

[](#customizing-the-resolution-logic)

If you wish to define your own model binding resolution logic, you may use the `Route::bind` method. The closure you pass to the bind method will receive the value of the URI segment and should return the instance of the class that should be injected into the route. Again, this customization should take place in the `boot` method of your application's `RouteServiceProvider`:

```
use App\Models\Post;
use Illuminate\Support\Facades\Route;

/**
 * Define your route model bindings, pattern filters, etc.
 *
 * @return void
 */
public function boot(): void
{
    Route::bind('post', function (string $value): Post {
        return Post::where('title', $value)->firstOrFail();
    });

    // ...
}
```

Alternatively, you may override the `resolveRouteBinding` method on your Elasticsearch model. This method will receive the value of the URI segment and should return the instance of the class that should be injected into the route:

```
/**
 * Retrieve the model for a bound value.
 *
 * @param  mixed  $value
 * @param  string|null  $field
 * @return \Huslab\Elasticsearch\Model|null
 */
public function resolveRouteBinding($value, ?string $field = null): ?self
{
    return $this->where('name', $value)->firstOrFail();
}
```

If a route is utilizing implicit binding scoping, the `resolveChildRouteBinding` method will be used to resolve the child binding of the parent model:

```
/**
 * Retrieve the child model for a bound value.
 *
 * @param  string  $childType
 * @param  mixed  $value
 * @param  string|null  $field
 * @return \Huslab\Elasticsearch\Model|null
 */
public function resolveChildRouteBinding(string $childType, $value, ?string $field): ?self
{
    return parent::resolveChildRouteBinding($childType, $value, $field);
}
```

Usage as a query builder
------------------------

[](#usage-as-a-query-builder)

You can use the `ES` facade to access the query builder directly, from anywhere in your application.

### Creating a new index

[](#creating-a-new-index)

```
ES::create('my_index');

# or

ES::index('my_index')->create();
```

### Creating index with custom options (optional)

[](#creating-index-with-custom-options-optional)

```
use Huslab\Elasticsearch\Facades\ES;
use Huslab\Elasticsearch\Index;

ES::index('my_index')->create(function(Index $index) {
    $index->shards(5)->replicas(1)->mapping([
        'my_type' => [
            'properties' => [
                'first_name' => [
                    'type' => 'string',
                ],
                'age' => [
                    'type' => 'integer'
                ]
            ]
        ]
    ])
});

# or

ES::create('my_index', function(Index $index){

      $index->shards(5)->replicas(1)->mapping([
          'my_type' => [
              'properties' => [
                  'first_name' => [
                      'type' => 'string',
                  ],
                  'age' => [
                      'type' => 'integer'
                  ]
              ]
          ]
      ])
});
```

### Dropping an index

[](#dropping-an-index)

```
ES::drop("my_index");

# or

ES::index("my_index")->drop();
```

### Running queries

[](#running-queries)

To run a query, start by (optionally) selecting the connection and index.

```
$documents = ES::connection("default")
                ->index("my_index")
                ->type("my_type")
                ->get();    # return a collection of results
```

You can shorten the above query to:

```
$documents = ES::type("my_type")->get();    # return a collection of results
```

Explicitly setting connection or index name in the query overrides configuration in `config/es.php`.

### Getting documents by id

[](#getting-documents-by-id)

```
ES::type("my_type")->id(3)->first();

# or

ES::type("my_type")->_id(3)->first();
```

### Sorting

[](#sorting)

```
ES::type("my_type")->orderBy("created_at", "desc")->get();

# Sorting with text search score

ES::type("my_type")->orderBy("_score")->get();
```

### Limit and offset

[](#limit-and-offset)

```
ES::type("my_type")->take(10)->skip(5)->get();
```

### Select only specific fields

[](#select-only-specific-fields)

```
ES::type("my_type")->select("title", "content")->take(10)->skip(5)->get();
```

### Where clause

[](#where-clause)

```
ES::type("my_type")->where("status", "published")->get();

# or

ES::type("my_type")->where("status", "=", "published")->get();
```

### Where greater than

[](#where-greater-than)

```
ES::type("my_type")->where("views", ">", 150)->get();
```

### Where greater than or equal

[](#where-greater-than-or-equal)

```
ES::type("my_type")->where("views", ">=", 150)->get();
```

### Where less than

[](#where-less-than)

```
ES::type("my_type")->where("views", "", 150)->get();
```

### Where not greater than or equal

[](#where-not-greater-than-or-equal)

```
ES::type("my_type")->whereNot("views", ">=", 150)->get();
```

### Where not less than

[](#where-not-less-than)

```
ES::type("my_type")->whereNot("views", "", 150)->query();
```

### Getting the original elasticsearch response

[](#getting-the-original-elasticsearch-response)

```
ES::type("my_type")->search("hello")->where("views", ">", 150)->response();
```

### Ignoring bad HTTP response

[](#ignoring-bad-http-response)

```
ES::type("my_type")->ignore(404, 500)->id(5)->first();
```

### Query Caching (Laravel &amp; Lumen)

[](#query-caching-laravel--lumen)

Package comes with a built-in caching layer based on laravel cache.

```
ES::type("my_type")->search("hello")->remember(10)->get();

# Specify a custom cache key
ES::type("my_type")->search("hello")->remember(10, "last_documents")->get();

# Caching using other available driver
ES::type("my_type")->search("hello")->cacheDriver("redis")->remember(10, "last_documents")->get();

# Caching with cache key prefix
ES::type("my_type")->search("hello")->cacheDriver("redis")->cachePrefix("docs")->remember(10, "last_documents")->get();
```

### Executing elasticsearch raw queries

[](#executing-elasticsearch-raw-queries)

```
ES::raw()->search([
    "index" => "my_index",
    "type"  => "my_type",
    "body" => [
        "query" => [
            "bool" => [
                "must" => [
                    [ "match" => [ "address" => "mill" ] ],
                    [ "match" => [ "address" => "lane" ] ]
                ]
            ]
        ]
    ]
]);
```

### Insert a new document

[](#insert-a-new-document)

```
ES::type("my_type")->id(3)->insert([
    "title" => "Test document",
    "content" => "Sample content"
]);

# A new document will be inserted with _id = 3.
# [id is optional] if not specified, a unique hash key will be generated.
```

### Bulk insert a multiple of documents at once.

[](#bulk-insert-a-multiple-of-documents-at-once)

```
# Main query
ES::index("my_index")->type("my_type")->bulk(function ($bulk){

    # Sub queries
	$bulk->index("my_index_1")->type("my_type_1")->id(10)->insert(["title" => "Test document 1","content" => "Sample content 1"]);
	$bulk->index("my_index_2")->id(11)->insert(["title" => "Test document 2","content" => "Sample content 2"]);
	$bulk->id(12)->insert(["title" => "Test document 3", "content" => "Sample content 3"]);

});

# Notes from the above query:

# As index and type names are required for insertion, Index and type names are extendable. This means that:

# If index() is not specified in subquery:
# -- The builder will get index name from the main query.
# -- if index is not specified in main query, the builder will get index name from configuration file.

# And

# If type() is not specified in subquery:
# -- The builder will get type name from the main query.
# you can use old bulk code style using multidimensional array of [id => data] pairs

ES::type("my_type")->bulk([

	10 => [
		"title" => "Test document 1",
		"content" => "Sample content 1"
	],

	11 => [
		"title" => "Test document 2",
		"content" => "Sample content 2"
	]

]);

# The two given documents will be inserted with its associated ids
```

### Update an existing document

[](#update-an-existing-document)

```
ES::type("my_type")->id(3)->update([
   "title" => "Test document",
   "content" => "sample content"
]);

# Document has _id = 3 will be updated.

# [id is required]
```

```
# Bulk update

ES::type("my_type")->bulk(function ($bulk){
    $bulk->id(10)->update(["title" => "Test document 1","content" => "Sample content 1"]);
    $bulk->id(11)->update(["title" => "Test document 2","content" => "Sample content 2"]);
});
```

### Incrementing field

[](#incrementing-field)

```
ES::type("my_type")->id(3)->increment("views");

# Document has _id = 3 will be incremented by 1.

ES::type("my_type")->id(3)->increment("views", 3);

# Document has _id = 3 will be incremented by 3.

# [id is required]
```

### Decrementing field

[](#decrementing-field)

```
ES::type("my_type")->id(3)->decrement("views");

# Document has _id = 3 will be decremented by 1.

ES::type("my_type")->id(3)->decrement("views", 3);

# Document has _id = 3 will be decremented by 3.

# [id is required]
```

### Update using script

[](#update-using-script)

```
# increment field by script
ES::type("my_type")->id(3)->script(
    "ctx._source.$field += params.count",
    ["count" => 1]
);

# add php tag to tags array list
ES::type("my_type")->id(3)->script(
    "ctx._source.tags.add(params.tag)",
    ["tag" => "php"]
);

# delete the doc if the tags field contain mongodb, otherwise it does nothing (noop)
ES::type("my_type")->id(3)->script(
    "if (ctx._source.tags.contains(params.tag)) { ctx.op = 'delete' } else { ctx.op = 'none' }",
    ["tag" => "mongodb"]
);
```

### Delete a document

[](#delete-a-document)

```
ES::type("my_type")->id(3)->delete();

# Document has _id = 3 will be deleted.
# [id is required]
```

```
# Bulk delete
ES::type("my_type")->bulk(function ($bulk){
    $bulk->id(10)->delete();
    $bulk->id(11)->delete();
});
```

Releases
--------

[](#releases)

See the [release page](https://github.com/Huslab/elasticsearch/releases).

Authors
-------

[](#authors)

[Basem Khirat](http://basemkhirat.com) -  - [@basemkhirat](https://twitter.com/basemkhirat)
[Moritz Friedrich](https://www.Huslab.com) -

Bugs, Suggestions and Contributions
-----------------------------------

[](#bugs-suggestions-and-contributions)

Thanks to [everyone](https://github.com/basemkhirat/elasticsearch/graphs/contributors) who has contributed to the original project and [everyone else](https://github.com/Huslab/elasticsearch/graphs/contributors) who has contributed to this fork!
Please use [Github](https://github.com/Huslab/elasticsearch) for reporting bugs, and making comments or suggestions.

If you're interested in helping out, the most pressing issues would be modernizing the query builder to provide better support for Elasticsearch features as well as completing the test suite!

License
-------

[](#license)

MIT

`Have a happy searching..`

###  Health Score

28

—

LowBetter than 54% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity11

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity61

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

8

Last Release

1703d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6e87dad11517c0ac33edcce546c99ab7a0226289db6eda655b5f129cc44d78d4?d=identicon)[huslab](/maintainers/huslab)

---

Top Contributors

[![huscakmak](https://avatars.githubusercontent.com/u/70260841?v=4)](https://github.com/huscakmak "huscakmak (8 commits)")

---

Tags

phplaravelelasticsearchlumenormmodeleloquentcachingquery builderindexingsearch enginescout

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/huscakmak-elasticsearch/health.svg)

```
[![Health](https://phpackages.com/badges/huscakmak-elasticsearch/health.svg)](https://phpackages.com/packages/huscakmak-elasticsearch)
```

###  Alternatives

[matchory/elasticsearch

The missing elasticsearch ORM for Laravel!

3059.0k](/packages/matchory-elasticsearch)[basemkhirat/elasticsearch

Laravel, Lumen and Native php elasticseach query builder to build complex queries using an elegant syntax

402312.0k](/packages/basemkhirat-elasticsearch)[spiritix/lada-cache

A Redis based, automated and scalable database caching layer for Laravel

591444.8k2](/packages/spiritix-lada-cache)[pdphilip/elasticsearch

An Elasticsearch implementation of Laravel's Eloquent ORM

145360.2k4](/packages/pdphilip-elasticsearch)[sleimanx2/plastic

Plastic is an Elasticsearch ODM and mapper for Laravel. It renders the developer experience more enjoyable while using Elasticsearch by providing a fluent syntax for mapping , querying and storing eloquent models.

508141.9k1](/packages/sleimanx2-plastic)[ymigval/laravel-model-cache

Laravel package for caching Eloquent model queries

7642.2k3](/packages/ymigval-laravel-model-cache)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
