{"id":17696812,"url":"https://github.com/datashaman/elasticsearch-model","last_synced_at":"2025-06-28T19:33:37.448Z","repository":{"id":56962852,"uuid":"61363628","full_name":"datashaman/elasticsearch-model","owner":"datashaman","description":"Laravel implementation of Ruby on Rails' elasticsearch-model gem, integrating Elasticsearch with Eloquent.","archived":false,"fork":false,"pushed_at":"2023-08-08T11:07:11.000Z","size":324,"stargazers_count":7,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-05-13T04:53:04.938Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/datashaman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-06-17T09:47:48.000Z","updated_at":"2024-10-09T01:19:26.000Z","dependencies_parsed_at":"2024-10-24T17:50:08.575Z","dependency_job_id":"7a0f0dce-9b34-44c1-84a8-00249bae99b9","html_url":"https://github.com/datashaman/elasticsearch-model","commit_stats":{"total_commits":187,"total_committers":1,"mean_commits":187.0,"dds":0.0,"last_synced_commit":"9adc9c09fa19ce347767e2e51042bafb97704857"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datashaman%2Felasticsearch-model","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datashaman%2Felasticsearch-model/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datashaman%2Felasticsearch-model/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datashaman%2Felasticsearch-model/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/datashaman","download_url":"https://codeload.github.com/datashaman/elasticsearch-model/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253877496,"owners_count":21977642,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-24T14:45:26.023Z","updated_at":"2025-05-13T04:53:15.261Z","avatar_url":"https://github.com/datashaman.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# datashaman/elasticsearch-model\n\nLaravel-oriented implementation of [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model).\n\nSupports Laravel 5.4 and higher on PHP 7.0/7.1. PHP 7.2 will be supported and tested once Travis directly supports it.\n\nNote that at least PHP 7.1 is required for Laravel 5.6+.\n\n[![Build Status](https://travis-ci.org/datashaman/elasticsearch-model.svg?branch=master)](https://travis-ci.org/datashaman/elasticsearch-model)\n[![StyleCI](https://styleci.io/repos/61363628/shield?style=flat)](https://styleci.io/repos/61363628)\n\n*NB* This is currently *BETA* quality software. Use on production at your own risk, and be aware that there might some\nfurther simplifications of the API in the next version or two.\n\nThe idea is to stay fairly faithful to the *Ruby on Rails* implementation, but housed in *Laravel*.\n\n## Installation\n\nInstall the package using composer:\n\n    composer require datashaman/elasticsearch-model\n\nConfigure the service provider in `config/app.php`:\n\n```\n...\nDatashaman\\Elasticsearch\\Model\\ServiceProvider::class,\n...\n```\n\nConfigure the alias in `config.app.php`:\n\n```\n...\n'Elasticsearch' =\u003e Datashaman\\Elasticsearch\\Model\\ElasticsearchFacade::class,\n...\n```\n\nCopy base config into your applicatin:\n\n    php artisan vendor:publish --tag=config --provider='Datashaman\\Elasticsearch\\Model\\ServiceProvider'\n\nEdit `config/elasticsearch.php` to your liking, setting `ELASTICSEARCH_HOSTS` (comma-delimited definition of host:port)  in `.env` should cover most use cases.\n\n## Usage\n\nLet's suppose you have an `Article` model:\n\n```php\nSchema::create('articles', function (Blueprint $table) {\n    $table-\u003eincrements('id');\n    $table-\u003estring('title');\n});\n\nclass Article extends Eloquent\n{\n}\n\nArticle::create([ 'title' =\u003e 'Quick brown fox' ]);\nArticle::create([ 'title' =\u003e 'Fast black dogs' ]);\nArticle::create([ 'title' =\u003e 'Swift green frogs' ]);\n```\n\n## Setup\n\nTo add the Elasticsearch integration for this model, use the `Datashaman\\Elasticsearch\\Model\\ElasticsearchModel` trait in your class. You must also add a protected static `$elasticsearch` property for storage:\n\n```php\nuse Datashaman\\Elasticsearch\\Model\\ElasticsearchModel;\n\nclass Article extends Eloquent\n{\n    use ElasticsearchModel;\n    protected static $elasticsearch;\n}\n```\n\nThis will extend the model with functionality related to Elasticsearch.\n\n### Proxy\n\nThe package contains a big amount of class and instance methods to provide all this functionality.\n\nTo prevent polluting your model namespace, *nearly* all functionality is accessed via static method `Article::elasticsearch()`.\n\n### Elasticsearch client\n\nThe module will setup a [client](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch), connected to `localhost:9200`, by default. You can access and use it like any other `Elasticsearch::Client`:\n\n```php\nArticle::elasticsearch()-\u003eclient()-\u003ecluster()-\u003ehealth();\n=\u003e [ \"cluster_name\" =\u003e \"elasticsearch\", \"status\" =\u003e \"yellow\", ... ]\n```\n\nTo use a client with a different configuration, set a client for the model using `Elasticsearch\\ClientBuilder`:\n\n```php\nArticle::elasticsearch()-\u003eclient(ClientBuilder::fromConfig([ 'hosts' =\u003e [ 'api.server.org' ] ]));\n```\n\n### Importing the data\n\nThe first thing you'll want to do is import your data to the index:\n\n```php\nArticle::elasticsearch()-\u003eimport([ 'force' =\u003e true ]);\n```\n\nIt's possible to import only records from a specific scope or query, transform the batch with the transform and preprocess options,\nor re-create the index by deleting it and creating it with correct mapping with the force option -- look for examples in the method documentation.\n\nNo errors were reported during importing, so... let's search the index!\n\n### Searching\n\nFor starters, we can try the *simple* type of search:\n\n```php\n$response = Article::search('fox dogs');\n\n$response-\u003etook();\n=\u003e 3\n\n$response-\u003etotal();\n=\u003e 2\n\n$response[0]-\u003e_score;\n=\u003e 0.02250402\n\n$response[0]-\u003etitle;\n=\u003e \"Fast black dogs\"\n```\n\n#### Search results\n\nThe returned `response` object is a rich wrapper around the JSON returned from Elasticsearch, providing access to response metadata and the actual results (*hits*).\n\nThe `response` object delegates to an internal `LengthAwarePaginator`. You can get a `Collection` via the delegate `getCollection` method, althought the paginator also delegates mmethods to its `Collection` so either of these work:\n\n```php\n$response-\u003eresults()\n    -\u003emap(function ($r) { return $r-\u003etitle; })\n    -\u003eall();\n=\u003e [\"Fast black dogs\", \"Quick brown fox\"]\n\n$response-\u003egetCollection()\n    -\u003emap(function ($r) { return $r-\u003etitle; })\n    -\u003eall();\n=\u003e [\"Fast black dogs\", \"Quick brown fox\"]\n\n$response-\u003efilter(function ($r) { return preg_match('/^Q/', $r-\u003etitle); })\n    -\u003emap(function ($r) { return $r-\u003etitle; })\n    -\u003eall();\n=\u003e [\"Quick brown fox\"]\n```\n\nAs you can see in the examples above, use the `Collection::all()` method to get a regular array.\n\nEach Elasticsearch *hit* is wrapped in the `Result` class.\n\n`Result` has a dynamic getter:\n\n* *index*, *type*, *id*, *score* and *source* are pulled from the top-level of the *hit*.\n  e.g. *index* is *hit\\[_index\\]*, *type* is *hit\\[_type\\]*, etc\n* if not one of the above, it looks for an existing item in the top-level hit.\n  e.g. *_version* is *hit\\[_version\\]* (if defined)\n* if not one of the above, it looks for an existing item in *hit\\[_source\\]* \\(the document\\).\n  e.g. *title* is *hit\\[_source\\]\\[title\\]* (if defined)\n* if nothing resolves from above, it triggers a notice and returns null\n\nIt also has a `toArray` method which returns the hit as an array.\n\n#### Search results as database records\n\nInstead of returning documents from Elasticsearch, the records method will return a collection of model instances, fetched from the primary database, ordered by score:\n\n```php\n$response-\u003erecords()\n    -\u003emap(function ($article) { return $article-\u003etitle; })\n    -\u003eall();\n=\u003e [\"Fast black dogs\", \"Quick brown fox\"]\n```\n\nThe returned object is a `Collection` of model instances returned by your database, i.e. the `Eloquent` instance.\n\nThe records method returns the real instances of your model, which is useful when you want to access your model methods - at the expense of slowing down your application, of course.\n\nIn most cases, working with results coming from Elasticsearch is sufficient, and much faster.\n\nWhen you want to access both the database `records` and search `results`, use the `eachWithHit` (or `mapWithHit`) iterator:\n\n```php\n$lines = [];\n$response-\u003erecords()-\u003eeachWithHit(function ($record, $hit) {\n    $lines[] = \"* {$record-\u003etitle}: {$hit-\u003e_score}\";\n});\n\n$lines;\n=\u003e [ \"* Fast black dogs: 0.01125201\", \"* Quick brown fox: 0.01125201\" ]\n\n$lines = $response-\u003erecords()-\u003emapWithHit(function ($record, $hit) {\n    return \"* {$record-\u003etitle}: {$hit-\u003e_score}\";\n})-\u003eall();\n\n$lines;\n=\u003e [ \"* Fast black dogs: 0.01125201\", \"* Quick brown fox: 0.01125201\" ]\n```\n\nNote the use `Collection::all()` to convert to a regular array in the `mapWithHit` example. `Collection` methods prefer to return `Collection` instances instead of regular arrays.\n\nThe first argument to `records` is an `options` array, the second argument is a callback which is passed the query builder to modify it on-the-fly. For example, to re-order the records differently to the results (from above):\n\n```php\n$response\n    -\u003erecords([], function ($query) {\n        $query-\u003eorderBy('title', 'desc');\n    })\n    -\u003emap(function ($article) { return $article-\u003etitle; })\n    -\u003eall();\n\n=\u003e [ 'Quick brown fox', 'Fast black dogs' ]\n```\n\nNotice that adding an `orderBy` call to the query overrides the ordering of the records, so that it is no longer the same as the results.\n\n#### Searching multiple models\n\n**TODO** Implement a Facade for cross-model searching.\n\n#### Pagination\n\nYou can implement pagination with the `from` and `size` search parameters. However, search results can be automatically paginated much like Laravel does.\n\n```php\n# Delegates to the results on page 2 with 20 per page\n$response-\u003eperPage(20)-\u003epage(2);\n\n# Records on page 2 with 20 per page; records ordered the same as results\n# Order of the `page` and `perPage` calls doesn't matter\n$response-\u003epage(2)-\u003eperPage(20)-\u003erecords();\n\n# Results on page 2 with (default) 15 results per page\n$response-\u003epage(2)-\u003eresults();\n\n# Records on (default) page 1 with 10 records per page\n$response-\u003eperPage(10)-\u003erecords();\n```\n\nYou have access to a length-aware paginator (the response delegates internally to the `results()` call, so you don't need to call results() on the chain):\n\n```php\n$response-\u003epage(2)-\u003eresults();\n=\u003e object(Illuminate\\Pagination\\LengthAwarePaginator) ...\n\n$results = response-\u003epage(2);\n\n$results-\u003esetPath('/articles');\n$results-\u003erender();\n=\u003e \u003cul class=\"pagination\"\u003e\n    \u003cli\u003e\u003ca href=\"/articles?page=1\" rel=\"prev\"\u003e\u0026laquo;\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"/articles?page=1\"\u003e1\u003c/a\u003e\u003c/li\u003e\n    \u003cli class=\"active\"\u003e\u003cspan\u003e2\u003c/span\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"/articles?page=3\"\u003e3\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"/articles?page=3\" rel=\"next\"\u003e\u0026raquo;\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nThe rendered HTML was tidied up slightly for readability.\n\n#### The Elasticsearch DSL\n\n**TODO** Integrate this with a query builder.\n\n### Index Configuration\n\nFor proper search engine function, it's often necessary to configure the index properly. This package provides class methods to set up index settings and mappings.\n\n```php\nArticle::settings(['index' =\u003e ['number_of_shards' =\u003e 1]], function ($s) {\n    $s['index'] = array_merge($s['index'], [\n        'number_of_replicas' =\u003e 4,\n    ]);\n});\n\nArticle::settings-\u003etoArray();\n=\u003e [ 'index' =\u003e [ 'number_of_shards' =\u003e 1, 'number_of_replicas' =\u003e 4 ] ]\n\nArticle::mappings(['dynamic' =\u003e false], function ($m) {\n    $m-\u003eindexes('title', [\n        'analyzer' =\u003e 'english',\n        'index_options' =\u003e 'offsets'\n    ]);\n});\n\nArticle::mappings()-\u003etoArray();\n=\u003e [ \"article\" =\u003e [\n    \"dynamic\" =\u003e false,\n    \"properties\" =\u003e [\n        \"title\" =\u003e [\n            \"analyzer\" =\u003e \"english\",\n            \"index_options\" =\u003e \"offsets\",\n            \"type\" =\u003e \"string\",\n        ]\n    ]\n]]\n```\n\nYou can use the defined settings and mappings to create an index with desired configuration:\n\n```php\nArticle::elasticsearch()-\u003eclient()-\u003eindices()-\u003edelete(['index' =\u003e Article::indexName()]);\nArticle::elasticsearch()-\u003eclient()-\u003eindices()-\u003ecreate([\n    'index' =\u003e Article::indexName(),\n    'body' =\u003e [\n        'settings' =\u003e Article::settings()-\u003etoArray(),\n        'mappings' =\u003e Article::mappings()-\u003etoArray(),\n    ],\n]);\n```\n\nThere's a shortcut available for this common operation (convenient e.g. in tests):\n\n```php\nArticle::elasticsearch()-\u003ecreateIndex(['force' =\u003e true]);\nArticle::elasticsearch()-\u003erefreshIndex();\n```\n\nBy default, index name and document type will be inferred from your class name, you can set it explicitely, however:\n\n```php\nclass Article {\n    protected static $indexName = 'article-production';\n    protected static $documentType = 'post';\n}\n```\n\nAlternately, you can set them using the following static methods:\n\n```php\nArticle::indexName('article-production');\nArticle::documentType('post');\n```\n\n## Updating the Documents in the Index\n\nUsually, we need to update the Elasticsearch index when records in the database are created, updated or deleted; use the index_document, update_document and delete_document methods, respectively:\n\n```php\nArticle::first()-\u003eindexDocument();\n=\u003e [ 'ok' =\u003e true, ... \"_version\" =\u003e 2 ]\n\nNote that this implementation differs from the Ruby one, where the instance has an elasticsearch() method and proxy object. In this package, the instance methods are added directly to the model. Implementing the same pattern in PHP is not easy to do cleanly.\n\n### Automatic callbacks\n\nYou can auomatically update the index whenever the record changes, by using the `Datashaman\\\\Elasticsearch\\\\Model\\\\Callbacks` trait in your model:\n\n```php\nuse Datashaman\\Elasticsearch\\Model\\ElasticsearchModel;\nuse Datashaman\\Elasticsearch\\Model\\Callbacks;\n\nclass Article\n{\n    use ElasticsearchModel;\n    use Callbacks;\n}\n\nArticle::first()-\u003eupdate([ 'title' =\u003e 'Updated!' ]);\n\nArticle::search('*')-\u003emap(function ($r) { return $r-\u003etitle; });\n=\u003e [ 'Updated!', 'Fast black dogs', 'Swift green Frogs' ]\n```\n\nThe automatic callback on record update keeps track of changes in your model (via Laravel's `getDirty` implementation), and performs a partial update when this support is available.\n\nThe automatic callbacks are implemented in database adapters coming with this package. You can easily implement your own adapter: please see the relevant chapter below.\n\n### Custom Callbacks\n\nIn case you would need more control of the indexing process, you can implement these callbacks yourself, by hooking into `created`, `saved`, `updated` or `deleted` events:\n\n```php\nArticle::saved(function ($article) {\n    $result = $article-\u003eindexDocument();\n    Log::debug(\"Saved document\", compact('result'));\n});\n\nArticle::deleted(function ($article) {\n    $result = $article-\u003edeleteDocument();\n    Log::debug(\"Deleted document\", compact('result'));\n});\n```\n\nRegrettably there are no `committed` events in `Eloquent` like in Ruby's `ActiveRecord`.\n\n### Asychronous Callbacks\n\nOf course, you're still performing an HTTP request during your database transaction, which is not optimal for large-scale applications. A better option would be to process the index operations in background, with Laravel's `Queue` facade:\n\n```php\nArticle::saved(function ($article) {\n    Queue::pushOn('default', new Indexer('index', Article::class, $article-\u003eid));\n});\n```\n\nAn example implementation of the `Indexer` class could look like this (source included in package):\n\n```php\nclass Indexer implements SelfHandling, ShouldQueue\n{\n    use InteractsWithQueue, SerializesModels;\n\n    public function __construct($operation, $class, $id)\n    {\n        $this-\u003eoperation = $operation;\n        $this-\u003eclass = $class;\n        $this-\u003eid = $id;\n    }\n\n    public function handle()\n    {\n        $class = $this-\u003eclass;\n\n        switch ($this-\u003eoperation) {\n        case 'index':\n            $record = $class::find($this-\u003eid);\n            $class::elasticsearch()-\u003eclient()-\u003eindex([\n                'index' =\u003e $class::indexName(),\n                'type' =\u003e $class::documentType(),\n                'id' =\u003e $record-\u003eid,\n                'body' =\u003e $record-\u003etoIndexedArray(),\n            ]);\n            $record-\u003eindexDocument();\n            break;\n        case 'delete':\n            $class::elasticsearch()-\u003eclient()-\u003edelete([\n                'index' =\u003e $class::indexName(),\n                'type' =\u003e $class::documentType(),\n                'id' =\u003e $this-\u003eid,\n            ]);\n            break;\n        default:\n            throw new Exception('Unknown operation: '.$this-\u003eoperation);\n        }\n    }\n}\n```\n\n## Model Serialization\n\nBy default, the model instance will be serialized to JSON using the output of the `toIndexedArray` method, which is defined automatically by the package:\n\n```php\nArticle::first()-\u003etoIndexedArray();\n=\u003e [ 'title' =\u003e 'Quick brown fox' ]\n```\n\nIf you want to customize the serialization, just implement the `toIndexedArray` method yourself, for instance with the `toArray` method:\n\n```php\nclass Article\n{\n    use ElasticsearchModel;\n\n    public function toIndexedArray($options = null)\n    {\n        return $this-\u003etoArray();\n    }\n}\n```\n\nThe re-defined method will be used in the indexing methods, such as `indexDocument`.\n\n## Attribution\n\nOriginal design from [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model) which is:\n\n* Copyright (c) 2014 Elasticsearch \u003chttp://www.elasticsearch.org\u003e\n* Licensed with Apache 2.0 license (detail in LICENSE.txt)\n\nChanges include a rewrite of the core logic in PHP, as well as slight enhancements to accomodate Laravel and Eloquent.\n\n## License\n\nThis package inherits the same license as its original. It is licensed under the Apache2 license, quoted below:\n\n    Copyright (c) 2016 datashaman \u003cmarlinf@datashaman.com\u003e\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatashaman%2Felasticsearch-model","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatashaman%2Felasticsearch-model","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatashaman%2Felasticsearch-model/lists"}