PHP code example of php-es-orm / es-orm

1. Go to this page and download the library: Download php-es-orm/es-orm library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

php-es-orm / es-orm example snippets




declare(strict_types=1);

return [
    'default' => 'default',

    'connections' => [
        'default' => [
            'hosts' => ['http://localhost:9200'],
        ],

        // A second connection, used by models declaring `protected string $connection = 'logs';`
        'logs' => [
            'hosts' => ['http://localhost:9201'],
            'api_key' => 'id:key',
        ],
    ],

    'retry' => [
        'max_retries' => 3,
        'retry_delay' => 100, // milliseconds
        'backoff'     => 2,
        'max_delay'   => 2000,
    ],

    'logging' => [
        'enabled'     => true,
        'max_entries' => 100,
    ],

    'cache' => [
        'enabled' => false,
        'driver'  => EsOrm\Support\Cache\ArrayCacheStore::class,
        'prefix'  => 'es_orm:',
        'ttl'     => 60,
    ],

    'pagination' => [
        'default_size'    => 15,
        'max_size'        => 10000,
        'track_total_hits' => true,
        'scroll_keep_alive' => '1m',
        'pit_keep_alive'    => '1m',
    ],

    'bulk' => [
        'chunk_size' => 1000,
        'refresh'    => false,
    ],

    'index' => [
        'default_settings' => [
            'number_of_shards'   => 1,
            'number_of_replicas' => 1,
        ],
    ],

    'mapping' => [
        'strict' => false,
    ],

    'migrations' => [
        'path'       => 'database/es-migrations',
        'repository' => 'es-orm-migrations.json',
    ],
];

use EsOrm\Client\ConnectionManager;
use EsOrm\Model\Model;
use EsOrm\Support\Config;

Model::setConnectionManager(new ConnectionManager(Config::fromFile('config/es_orm.php')));

// or from an array
Model::setConnectionManager(ConnectionManager::fromArray([
    'default'     => 'default',
    'connections' => ['default' => ['hosts' => ['http://localhost:9200']]],
]));



declare(strict_types=1);

namespace App\Models;

use EsOrm\Model\Model;

final class User extends Model
{
    protected string $index = 'users';

    protected array $fillable = [
        'name',
        'email',
        'age',
        'status',
    ];

    protected array $casts = [
        'age'       => 'integer',
        'active'    => 'boolean',
        'metadata'  => 'array',
        'created_at' => 'datetime',
    ];
}

use App\Models\User;

// Create
$user = User::create([
    'name'   => 'Tom',
    'email'  => '[email protected]',
    'age'    => 20,
    'status' => 'active',
]);

// Read
$user = User::find(1);            // ?User
$user = User::findOrFail(1);      // User (throws ModelNotFoundException)
$user = User::first();
$user = User::firstOrFail();

// Update
$user = User::find(1);
$user->name = 'Tomás';
$user->save();

$user->update(['status' => 'inactive']); // fill + save

// Delete
$user->delete();

// Helpers
$user->exists();     // bool
$user->getKey();     // mixed
$user->refresh();    // re-read from Elasticsearch
$user->toArray();
$user->toJson();

// Find by attributes or return an unsaved instance hydrated with the values
$user = User::query()->firstOrNew(['email' => '[email protected]'], ['name' => 'Tom']);

// Find by attributes or create the document (single index request when missing)
$user = User::query()->firstOrCreate(['email' => '[email protected]'], ['name' => 'Tom']);

// Find by attributes and create/update with the given values
$user = User::query()->updateOrCreate(['email' => '[email protected]'], ['age' => 21]);

$user->increment('login_count');
$user->increment('balance', 25, ['status' => 'vip']); // extra attributes in the same request
$user->decrement('balance', 10);

$users = User::bulkCreate([
    ['name' => 'A', 'status' => 'active'],
    ['name' => 'B', 'status' => 'active'],
]);

$affected = User::bulkUpdate([
    ['id' => 1, 'status' => 'inactive'],
    ['id' => 2, 'status' => 'inactive'],
]);

$affected = User::bulkDelete([1, 2, 3]);

$affected = User::destroy([1, 2, 3]); // alias of bulkDelete

use App\Models\User;

$users = User::query()
    ->where('status', 'active')
    ->where('age', '>', 18)
    ->orderByDesc('created_at')
    ->limit(20)
    ->get();

User::query()
    ->where('status', 'active')
    ->where(function ($query) {
        $query->where('age', '>', 18)
              ->orWhere('vip', true);
    })
    ->get();

where(), orWhere()            // '=', '>', '>=', '<', '<=', '!=', 'like', 'not like', 'in', 'not in'
whereIn(), whereNotIn()
whereNull(), whereNotNull()
whereBetween(), whereNotBetween()
whereExists(), whereNotExists()
whereNested(), orWhereNested()

// Existence and single-record assertions
User::query()->where('status', 'active')->exists(); // bool
User::query()->where('email', $email)->sole();      // User, throws when 0 or > 1 match

// Metric shortcuts (a size-0 search with a single value aggregation)
User::query()->where('status', 'active')->avg('age');    // int|float|null
User::query()->sum('price');
User::query()->min('created_at');
User::query()->max('created_at');

// Delete every document matching the query (_delete_by_query, hard delete)
User::query()->where('status', 'banned')->deleteByQuery(['refresh' => true]);

Article::query()
    ->match('title', 'Elasticsearch')
    ->matchPhrase('excerpt', 'full text phrase')
    ->multiMatch(['title', 'body'], 'Elasticsearch ORM')
    ->term('status', 'published')
    ->terms('category_id', [1, 2, 3])
    ->range('created_at', ['gte' => 'now-30d'])
    ->whereExists('published_at')
    ->prefix('slug', 'elastic')
    ->wildcard('title', 'elastic*')
    ->regexp('slug', 'elas.*')
    ->fuzzy('title', 'elasticseach', ['fuzziness' => 'AUTO'])
    ->ids([10, 20, 30])
    ->queryString('title:elasticsearch AND status:published', ['fields' => ['title^2']])
    ->simpleQueryString('elasticsearch +orm', ['fields' => ['title', 'body']])
    ->nested('attributes', fn ($q) => $q->term('attributes.name', 'color'))
    ->parentId('article', 42)
    ->hasChild('comment', fn ($q) => $q->term('approved', true))
    ->hasParent('article', fn ($q) => $q->term('status', 'published'))
    ->get();

Article::query()
    ->bool(function ($query) {
        $query->must(['term' => ['status' => 'published']]);
        $query->filter(['range' => ['created_at' => ['gte' => 'now-30d']]]);
        $query->should(['match' => ['title' => 'elasticsearch']]);
        $query->mustNot(['term' => ['flagged' => true]]);
    })
    ->minimumShouldMatch(1)
    ->get();

// Explicit occurrences
Article::query()
    ->must(['term' => ['status' => 'published']])
    ->filter(['term' => ['tenant_id' => 7]])
    ->should(['match' => ['title' => 'orm']])
    ->mustNot(['exists' => ['field' => 'deleted_at']])
    ->get();

Product::query()
    ->nested('attributes', function ($query) {
        $query->term('attributes.name', 'color')
              ->term('attributes.value', 'red');
    }, ['score_mode' => 'avg'])
    ->get();

Article::query()
    ->match('title', 'Elasticsearch')
    ->rawQuery([
        'bool' => [
            'must' => [
                ['term' => ['locale' => 'en']],
            ],
        ],
    ])
    ->aggregate('by_status', ['terms' => ['field' => 'status']])
    ->get();

// Or merge a whole raw request body fragment
Article::query()->raw([
    'query' => ['bool' => ['must' => [...]]],
    'aggs'  => [...],
])->get();

User::query()
    ->select(['id', 'name', 'email']) // -> _source.tExcept(['password', 'internal_notes']) // -> _source.excludes
    ->get();

User::query()->orderBy('name')->get();
User::query()->orderByAsc('name')->get();
User::query()->orderByDesc('created_at')->get();

// Multiple fields
Article::query()->orderBy([
    'created_at' => 'desc',
    'price'      => 'asc',
])->get();

// Full Elasticsearch sort DSL
Article::query()->sort('price', 'asc', ['missing' => '_last', 'mode' => 'avg'])->get();
Article::query()->orderByDesc('_score')->get();

// Length aware (knows the total)
$page = User::query()->where('status', 'active')->paginate(20);
$page->items();      // ModelCollection
$page->total();      // int
$page->currentPage();

// Simple (next / previous only)
$page = User::query()->simplePaginate(20);

// Cursor pagination built on search_after (deep pagination safe)
$page = Article::query()
    ->orderByDesc('created_at')
    ->cursorPaginate(20);

$page->nextCursor(); // pass it back to continue
$next = Article::query()
    ->orderByDesc('created_at')
    ->cursorPaginate(20, $page->nextCursor());

// search_after
Article::query()->orderByDesc('created_at')->searchAfter([/* ... */])->get();

// Point In Time
$pit = $connection->request('openPointInTime', ['index' => 'articles', 'keep_alive' => '1m'])['id'];

Article::query()->pit($pit)->orderByDesc('created_at')->get();

$connection->request('closePointInTime', ['body' => ['id' => $pit]]);

Product::query()
    ->termsAgg('categories', 'category_id')
    ->statsAgg('price_stats', 'price')
    ->avgAgg('avg_price', 'price')
    ->sumAgg('total_stock', 'stock')
    ->minAgg('min_price', 'price')
    ->maxAgg('max_price', 'price')
    ->cardinalityAgg('unique_users', 'user_id')
    ->dateHistogramAgg('sales', 'created_at', 'day')
    ->histogramAgg('price_histogram', 'price', 50)
    ->rangeAgg('price_ranges', 'price', [['to' => 50], ['from' => 50, 'to' => 100]])
    ->dateRangeAgg('recent', 'created_at', [['from' => 'now-7d/d', 'to' => 'now/d']])
    ->filtersAgg('by_status', ['active' => ['term' => ['status' => 'active']]])
    ->nestedAgg('attributes', 'attributes')
    ->reverseNestedAgg('root')
    ->topHitsAgg('top_products', 3)
    ->percentilesAgg('price_percentiles', 'price', [50, 95, 99])
    ->significantTermsAgg('trending', 'tags')
    ->filterAgg('published', fn ($q) => $q->term('status', 'published'))
    ->get();

$result = Article::query()
    ->aggregate('sales', function ($agg) {
        $agg->dateHistogram('per_day', 'created_at', 'day');
        $agg->avg('avg_price', 'price'); // became a sub-aggregation of 'sales'
    })
    ->get();

$result->aggregation('sales');  // mixed
$result->aggregations();        // array<string, mixed>

$articles = Article::query()
    ->match('content', 'Elasticsearch')
    ->highlight(['title', 'content'], [
        'pre_tags'           => ['<em>'],
        'post_tags'          => ['</em>'],
        'fragment_size'      => 150,
        'number_of_fragments' => 3,
        'type'               => 'unified',
    ])
    ->get();

foreach ($articles as $article) {
    $article->highlight('content'); // list<string>|null
    $article->highlights;           // array<string, list<string>>
}

$result = Article::query()
    ->suggest([
        'title_suggest' => [
            'text'      => 'elasticseach',
            'term'      => ['field' => 'title'],
            // 'phrase'  => ['field' => 'body'],
            // 'completion' => ['field' => 'suggest_field'],
        ],
    ])
    ->search();

$result->suggest();

Store::query()
    ->geoDistance('location', 10, 35.68, 139.76) // field, distance(km), lat, lon
    ->get();

Store::query()
    ->geoDistance('location', '2km', ['lat' => 35.68, 'lon' => 139.76])
    ->get();

Store::query()
    ->geoBoundingBox('location', ['top_left' => [139.7, 35.7], 'bottom_right' => [139.8, 35.6]])
    ->get();

Store::query()
    ->geoPolygon('location', [[139.7, 35.7], [139.8, 35.7], [139.8, 35.6], [139.7, 35.6]])
    ->get();

User::query()->each(function (User $user): void {
    // export / migrate / batch process
}, chunkSize: 1000);

// Break out of the iteration by returning false
User::query()->each(function (User $user): bool {
    return $user->age < 100; // stop at the first user older than 100
});

$users = User::bulkCreate([...]);
$count = User::bulkUpdate([...]); // each row must contain the primary key
$count = User::bulkDelete([...]);

// Multi get (single request)
$users = User::mget([1, 2, 3]);

// Multi search: one _msearch request for many queries
use EsOrm\Query\Search;

$results = Search::multi([
    'users'    => User::query()->match('name', 'Tom'),
    'articles' => Article::query()->term('status', 'published'),
]);

$results['users']->hits();    // ModelCollection
$results['users']->total();   // int

protected array $casts = [
    'age'        => 'integer',
    'price'      => 'float',
    'active'     => 'boolean',
    'name'       => 'string',
    'metadata'   => 'array',
    'options'    => 'json',
    'created_at' => 'datetime',
    'birthday'   => 'date',
    'profile'    => 'object',
];

use EsOrm\Model\Attribute;

final class User extends Model
{
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => ucfirst((string) $value),
            set: fn ($value) => strtolower((string) $value),
        );
    }
}

protected array $fillable = ['name', 'email'];
protected array $guarded = ['*']; // default: everything is guarded unless fillable

$user->fill(['name' => 'Tom', 'role' => 'admin']); // 'role' is ignored
$user->forceFill(['role' => 'admin']);             // bypasses the guard

final class User extends Model
{
    public function scopeActive($query)
    {
        return $query->term('status', 'active');
    }
}

User::active()->get();        // via __callStatic -> query()
User::query()->active()->get();

use EsOrm\Scope\Scope;

Model::addGlobalScope(new class implements Scope {
    public function apply(\EsOrm\Contracts\QueryBuilderInterface $builder): void
    {
        $builder->term('tenant_id', 7);
    }
});

use EsOrm\Model\Model;
use EsOrm\Model\SoftDeletes;

final class User extends Model
{
    use SoftDeletes;

    protected string $index = 'users';
    protected array $casts = ['deleted_at' => 'datetime'];
}

$user->delete();          // sets deleted_at, keeps the document
$user->trashed();         // bool
$user->restore();         // clears deleted_at
$user->forceDelete();     // removes the document

User::query()->get();            // excludes trashed
User::withTrashed()->get();      // 

use EsOrm\Model\Model;

Model::eventDispatcher()->listen('created', function (Model $model): void {
    // ...
});

// A listener returning false aborts the operation (e.g. saving/creating/deleting)

// Observers
final class UserObserver
{
    public function created(User $user): void {}
    public function updated(User $user): void {}
    public function deleted(User $user): void {}
}

User::observe(UserObserver::class);

final class Article extends Model
{
    protected string $index = 'articles';

    public function author(): \EsOrm\Relation\BelongsTo
    {
        return $this->belongsTo(Author::class, 'author_id');
    }
}

final class Author extends Model
{
    protected string $index = 'authors';

    public function articles(): \EsOrm\Relation\HasMany
    {
        return $this->hasMany(Article::class, 'author_id');
    }
}

// Eager load in a single batch query
$articles = Article::with('author')->get();

// Lazy eager load on loaded models
$article->load('author');
$article->loadMissing('author');

final class Article extends Model
{
    protected string $index = 'articles';

    protected function mapping(): array
    {
        return [
            'properties' => [
                'title' => [
                    'type'   => 'text',
                    'fields' => ['keyword' => ['type' => 'keyword']],
                ],
                'status'     => ['type' => 'keyword'],
                'created_at' => ['type' => 'date'],
                'price'      => ['type' => 'double'],
            ],
        ];
    }
}

$article->mappingDefinition(); // EsOrm\Contracts\MappingInterface

use EsOrm\Mapping\Mapping;

$mapping = Mapping::create('articles')
    ->text('title', ['fields' => ['keyword' => ['type' => 'keyword']]])
    ->keyword('status')
    ->date('created_at')
    ->double('price')
    ->nested('attributes')
    ->geoPoint('location');

use EsOrm\Index\IndexManager;

$indices = IndexManager::on(Model::connectionManager()); // or Es::index() in Laravel

$indices->createIndex('articles', $mapping, ['number_of_replicas' => 0]);
$indices->indexExists('articles');
$indices->getIndex('articles');
$indices->getMapping('articles');
$indices->putMapping('articles', $extraMapping);
$indices->refresh('articles');
$indices->flush('articles');
$indices->forceMerge('articles', ['max_num_segments' => 1]);
$indices->close('articles');
$indices->open('articles');
$indices->deleteIndex('articles');

use EsOrm\Index\AliasManager;

$aliases = AliasManager::on(Model::connectionManager());

$aliases->createAlias('articles_v1', 'articles');
$aliases->switchAlias('articles', 'articles_v2'); // atomic, auto-detects current index
$aliases->deleteAlias('articles_v2', 'articles');
$aliases->getAlias('articles');

use EsOrm\Index\ReindexManager;

$reindex = ReindexManager::on(Model::connectionManager());

$reindex->reindexTo('articles_v1', 'articles_v2', [
    'query'      => ['term' => ['status' => 'published']],
    'batch_size' => 1000,
    'script'     => ['source' => 'ctx._source.migrated = true'],
    'conflicts'  => 'proceed',
    'refresh'    => true,
]);

// Zero-downtime upgrade:
$indices->createIndex('articles_v2', $newMapping);
$reindex->reindexTo('articles_v1', 'articles_v2');
$aliases->switchAlias('articles', 'articles_v2');



declare(strict_types=1);

namespace Database\EsMigrations;

use EsOrm\Migration\IndexMigration;
use EsOrm\Mapping\Mapping;

final class CreateArticlesIndex extends IndexMigration
{
    public function up(): void
    {
        $this->createIndex('articles', Mapping::create('articles')
            ->text('title')
            ->keyword('status')
            ->date('created_at'));
    }

    public function down(): void
    {
        $this->deleteIndex('articles');
    }
}

use EsOrm\Migration\Migrator;

$migrator = new Migrator($indices, 'database/es-migrations');

$migrator->migrate();          // apply pending migrations
$migrator->rollback(1);        // roll back the last batch
$migrator->status();           // list the discovered migrations and their state

Article::query()
    ->where('status', 'active')
    ->cache(60)        // seconds, optional custom key as 2nd argument
    ->get();

$query = User::query()->where('status', 'active');

$query->toDsl();   // array<string, mixed>
$query->toJson();  // string
$query->dump();    // pretty print (VarDumper aware)
$query->dd();      // dump and die

use EsOrm\Laravel\Facades\Es;

Es::connection();          // default connection
Es::connection('logs');    // named connection
Es::index();               // IndexManager
Es::alias();               // AliasManager
Es::reindex();             // ReindexManager
Es::migrator();            // Migrator
Es::search([...]);         // raw search
bash
composer 
bash
cp vendor/php-es-orm/es-orm/config/es_orm.php config/es_orm.php
bash
export ES_ORM_CONFIG=/absolute/path/to/es_orm.php
bash
php artisan vendor:publish --tag=es_orm-config
bash
AppServiceProvider.php
use EsOrm\Contracts\ConnectionManagerInterface;
public function register(): void
{    
    $this->app->make(ConnectionManagerInterface::class);
}