├── LICENSE.md ├── composer.json ├── readme.md └── src ├── Connection.php ├── Console ├── Index │ └── Populate.php └── Mapping │ ├── BaseCommand.php │ ├── Install.php │ ├── Make.php │ ├── ReRun.php │ ├── Reset.php │ └── Run.php ├── DSL ├── AggregationBuilder.php ├── FunctionScoreBuilder.php ├── SearchBuilder.php └── SuggestionBuilder.php ├── Exception ├── InvalidArgumentException.php └── MissingArgumentException.php ├── Facades ├── Map.php └── Plastic.php ├── Fillers ├── EloquentFiller.php └── FillerInterface.php ├── IndexServiceProvider.php ├── Map ├── Blueprint.php ├── Builder.php └── Grammar.php ├── MappingServiceProvider.php ├── Mappings ├── Creator.php ├── Mapper.php ├── Mapping.php ├── Mappings.php └── stubs │ └── default.stub ├── Persistence └── EloquentPersistence.php ├── PlasticManager.php ├── PlasticPaginator.php ├── PlasticResult.php ├── PlasticServiceProvider.php ├── Resources ├── config.php └── database │ └── mappings │ └── .gitkeep └── Searchable.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sleiman Sleiman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sleimanx2/plastic", 3 | "license": "MIT", 4 | "description": "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.", 5 | "keywords": ["elasticsearch","elastic","laravel","eloquent","model","ODM","mapper"], 6 | "homepage": "https://github.com/sleimanx2/plastic", 7 | "authors": [ 8 | { 9 | "name": "Sleiman Sleiman", 10 | "email": "sleiman@outlook.com", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.0", 16 | "elasticsearch/elasticsearch": "~5.3.0", 17 | "ongr/elasticsearch-dsl": "5.*", 18 | "illuminate/container": "5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*", 19 | "illuminate/contracts": "5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*", 20 | "illuminate/console": "5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*", 21 | "illuminate/pagination": "5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*", 22 | "illuminate/support": "5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*", 23 | "illuminate/database": "5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "~4.0", 27 | "mockery/mockery": "^0.9.4", 28 | "phpunit/php-code-coverage": "^2.1" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Sleimanx2\\Plastic\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Sleimanx2\\Plastic\\Tests\\": "tests/" 38 | } 39 | }, 40 | "config": { 41 | "preferred-install": "dist" 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Sleimanx2\\Plastic\\PlasticServiceProvider" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Plastic Logo](http://i.imgur.com/PyolY7g.png) 2 | 3 | > 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. 4 | 5 | [![License](https://poser.pugx.org/laravel/framework/license.svg)](https://packagist.org/packages/sleimanx2/plastic) [![Build Status](https://travis-ci.org/sleimanx2/plastic.svg?branch=master&&refresh=2)](https://travis-ci.org/sleimanx2/plastic) [![StyleCI](https://styleci.io/repos/58264395/shield)](https://styleci.io/repos/58264395) 6 | 7 | > This package is still under active development and may change. 8 | 9 | > For Elasticsearch v2 please refer to version < 0.4.0. 10 | 11 | # Installing Plastic 12 | 13 | ```bash 14 | composer require sleimanx2/plastic 15 | ``` 16 | 17 | If you are using **Laravel >=5.5** the service provider will be **automatically discovered** otherwise we need to add the plastic service provider to `config/app.php` under the providers key: 18 | 19 | ```php 20 | Sleimanx2\Plastic\PlasticServiceProvider::class 21 | ``` 22 | 23 | Finally we need to run: 24 | 25 | ```bash 26 | php artisan vendor:publish 27 | ``` 28 | 29 | This will create a config file at `config/plastic.php` and a mapping directory at `database/mappings`. 30 | 31 | # Usage 32 | 33 | - [Defining Searchable Models](#searchable-models) 34 | - [Storing Model Content](#store-content) 35 | - [Searching](#searching) 36 | - [Aggregation](#aggregation) 37 | - [Suggestions](#suggestions) 38 | - [Mappings](#mappings) 39 | - [Populate An Index](#populate-an-index) 40 | - [Access The Client](#access-client) 41 | 42 | ## [Defining Searchable Models]() 43 | 44 | To get started, enable searching capabilities in your model by adding the `Sleimanx2\Plastic\Searchable` trait: 45 | 46 | ```php 47 | use Sleimanx2\Plastic\Searchable; 48 | 49 | class Book extends Model 50 | { 51 | use Searchable; 52 | } 53 | ``` 54 | 55 | ### Defining what data to store. 56 | 57 | By default, Plastic will store all visible properties of your model, using `$model->toArray()`. 58 | 59 | In addition, Plastic provides you with two ways to manually specify which attributes/relations should be stored in Elasticsearch. 60 | 61 | #### 1 - Providing a searchable property to our model 62 | 63 | ```php 64 | public $searchable = ['id', 'name', 'body', 'tags', 'images']; 65 | ``` 66 | 67 | #### 2 - Providing a buildDocument method 68 | 69 | ```php 70 | public function buildDocument() 71 | { 72 | return [ 73 | 'id' => $this->id, 74 | 'tags' => $this->tags 75 | ]; 76 | } 77 | ``` 78 | 79 | ### Custom elastic type name 80 | 81 | By the default Plastic will use the model table name as the model type. You can customize it by adding a `$documentType` property to your model: 82 | 83 | ```php 84 | public $documentType = 'custom_type'; 85 | ``` 86 | 87 | ### Custom elastic index name 88 | 89 | By the default Plastic will use the index defined in the configuration file. You can customize in which index your model data will be stored by setting the `$documentIndex` property to your model: 90 | 91 | ```php 92 | public $documentIndex = 'custom_index'; 93 | ``` 94 | 95 | ## [Storing Model Content]() 96 | 97 | Plastic automatically syncs model data with elastic when you save or delete your model from our SQL DB, however this feature can be disabled by adding `public $syncDocument = false` to your model. 98 | 99 | > Its important to note that manual document update should be performed in multiple scenarios: 100 | 101 | > 1 - When you perform a bulk update or delete, no Eloquent event is triggered, therefore the document data won't be synced. 102 | 103 | > 2 - Plastic doesn't listen to related models events (yet), so when you update a related model's content you should consider updating the parent document. 104 | 105 | ### Saving a document 106 | 107 | ```php 108 | $book = Book::first()->document()->save(); 109 | ``` 110 | 111 | ### Partial updating a document 112 | 113 | ```php 114 | $book = Book::first()->document()->update(); 115 | ``` 116 | 117 | ### Deleting a document 118 | 119 | ```php 120 | $book = Book::first()->document()->delete(); 121 | ``` 122 | 123 | ### Saving documents in bulk 124 | 125 | ```php 126 | Plastic::persist()->bulkSave(Tag::find(1)->books); 127 | ``` 128 | 129 | ### Deleting documents in bulk 130 | 131 | ```php 132 | $authors = Author::where('age','>',25)->get(); 133 | 134 | Plastic::persist()->bulkDelete($authors); 135 | ``` 136 | 137 | ## [Searching Model Content]() 138 | 139 | Plastic provides a fluent syntax to query Elasticsearch which leads to compact readable code. Lets dig into it: 140 | 141 | ```php 142 | $result = Book::search()->match('title','pulp')->get(); 143 | 144 | // Returns a collection of Book Models 145 | $books = $result->hits(); 146 | 147 | // Returns the total number of matched documents 148 | $result->totalHits(); 149 | 150 | // Returns the highest query score 151 | $result->maxScore(); 152 | 153 | //Returns the time needed to execute the query 154 | $result->took(); 155 | ``` 156 | 157 | To get the raw DSL query that will be executed you can call `toDSL()`: 158 | 159 | ```php 160 | $dsl = Book::search()->match('title','pulp')->toDSL(); 161 | ``` 162 | 163 | ### Pagination 164 | 165 | ```php 166 | $books = Book::search() 167 | ->multiMatch(['title', 'description'], 'ham on rye', ['fuzziness' => 'AUTO']) 168 | ->sortBy('date') 169 | ->paginate(); 170 | ``` 171 | 172 | You can still access the result object after pagination using the result method: 173 | 174 | ```php 175 | $books->result(); 176 | ``` 177 | 178 | ### Bool Query 179 | 180 | ```php 181 | User::search() 182 | ->must() 183 | ->term('name','kimchy') 184 | ->mustNot() 185 | ->range('age',['from'=>10,'to'=>20]) 186 | ->should() 187 | ->match('bio','developer') 188 | ->match('bio','elastic') 189 | ->filter() 190 | ->term('tag','tech') 191 | ->get(); 192 | ``` 193 | 194 | ### Nested Query 195 | 196 | ```php 197 | $contain = 'foo'; 198 | 199 | Post::search() 200 | ->multiMatch(['title', 'body'], $contain) 201 | ->nested('tags', function (SearchBuilder $builder) use ($contain) { 202 | $builder->match('tags.name', $contain); 203 | })->get(); 204 | ``` 205 | 206 | > Check out this [documentation](docs/search.md) of supported search queries within Plastic and how to apply unsupported queries. 207 | 208 | ### Change index on the fly 209 | 210 | To switch to a different index for a single query, simply use the `index` method. 211 | 212 | ```php 213 | $result = Book::search()->index('special-books')->match('title','pulp')->get(); 214 | ``` 215 | 216 | ## [Aggregation]() 217 | 218 | ```php 219 | $result = User::search() 220 | ->match('bio', 'elastic') 221 | ->aggregate(function (AggregationBuilder $builder) { 222 | $builder->average('average_age', 'age'); 223 | })->get(); 224 | 225 | $aggregations = $result->aggregations(); 226 | ``` 227 | 228 | > Check out this [documentation](docs/aggregation.md) of supported aggregations within plastic and how to apply unsupported aggregations. 229 | 230 | ## [Suggestions]() 231 | 232 | ```php 233 | Plastic::suggest()->completion('tag_suggest', 'photo')->get(); 234 | ``` 235 | 236 | The suggestions query builder can also be accessed directly from the model as well: 237 | 238 | ```php 239 | //this be handy if you have a custom index for your model 240 | Tag::suggest()->term('tag_term','admin')->get(); 241 | ``` 242 | 243 | ## [Model Mapping]() 244 | 245 | Mappings are an important aspect of Elasticsearch. You can compare them to indexing in SQL databases. Mapping your models yields better and more efficient search results, and allows us to use some special query functions like nested fields and suggestions. 246 | 247 | ### Generate a Model Mapping 248 | 249 | ```bash 250 | php artisan make:mapping "App\User" 251 | ``` 252 | 253 | The new mapping will be placed in your `database/mappings` directory. 254 | 255 | ### Mapping Structure 256 | 257 | A mapping class contains a single method `map`. The map method is used to map the given model fields. 258 | 259 | Within the `map` method you may use the Plastic Map builder to expressively create field maps. For example, let's look at a sample mapping that creates a Tag model map: 260 | 261 | ```php 262 | use Sleimanx2\Plastic\Map\Blueprint; 263 | use Sleimanx2\Plastic\Mappings\Mapping; 264 | 265 | class AppTag extends Mapping 266 | { 267 | /** 268 | * Full name of the model that should be mapped 269 | * 270 | * @var string 271 | */ 272 | protected $model = App\Tag::class; 273 | 274 | /** 275 | * Run the mapping. 276 | * 277 | * @return void 278 | */ 279 | public function map() 280 | { 281 | Map::create($this->getModelType(), function (Blueprint $map) { 282 | $map->string('name')->store('true')->index('analyzed'); 283 | 284 | // instead of the fluent syntax we can use the second method argument to fill the attributes 285 | $map->completion('suggestion', ['analyzer' => 'simple', 'search_analyzer' => 'simple']); 286 | },$this->getModelIndex()); 287 | } 288 | } 289 | ``` 290 | 291 | > To learn about all of the methods available on the Map builder, check out this [documentation](docs/mapping.md). 292 | 293 | ### Run Mappings 294 | 295 | Running the created mappings can be done using the Artisan console command: 296 | 297 | ```bash 298 | php artisan mapping:run 299 | ``` 300 | 301 | ### Updating Mappings 302 | 303 | If your update consists only of adding a new field mapping you can always update our model map with your new field and run: 304 | 305 | ```bash 306 | php artisan mapping:rerun 307 | ``` 308 | 309 | The mapping for existing fields cannot be updated or deleted, so you'll need to use one of following techniques to update existing fields. 310 | 311 | #### 1 - Create a new index 312 | 313 | You can always create a new Elasticsearch index and re-run the mappings. After running the mappings you can use the `bulkSave` method to sync your SQL data with Elasticsearch. 314 | 315 | #### 2 - Using aliases 316 | 317 | Its recommended to create your Elasticsearch index with an alias to ease the process of updating your model mappings with zero downtime. To learn more check out: 318 | 319 | 320 | 321 | ## [Populate An Index]() 322 | 323 | Populating an index with searchable models can be done by running an Artisan console command : 324 | 325 | ```bash 326 | php artisan plastic:populate [--mappings][--index=...][--database=...] 327 | ``` 328 | 329 | - `--mappings` Create the models mappings before populating the index 330 | - `--database=...` Database connection to use for mappings instead of the default one 331 | - `--index=...` Index to populate instead of the default one 332 | 333 | The list of models from which to recreate the documents has to be configured **per index** in `config/plastic.php`: 334 | ``` 335 | 'populate' => [ 336 | 'models' => [ 337 | // Models for the default index 338 | env('PLASTIC_INDEX', 'plastic') => [ 339 | App\Models\Article::class, 340 | App\Models\Page::class, 341 | ], 342 | // Models for the index "another_index" 343 | 'another_index' => [ 344 | App\Models\User::class, 345 | ], 346 | ], 347 | ], 348 | ``` 349 | 350 | ## [Access The Client]() 351 | 352 | You can access the Elasticsearch client to manage your indices and aliases as follows: 353 | 354 | ```php 355 | $client = Plastic::getClient(); 356 | 357 | //index delete 358 | $client->indices()->delete(['index'=> Plastic::getDefaultIndex()]); 359 | //index create 360 | $client->indices()->create(['index' => Plastic::getDefaultIndex()]); 361 | ``` 362 | 363 | More about the official elastic client : 364 | 365 | # Contributing 366 | 367 | Thank you for contributing, The contribution guide can be found [Here](CONTRIBUTING.md). 368 | 369 | # License 370 | 371 | Plastic is open-sourced software licensed under the [MIT license](LICENSE.md). 372 | 373 | # To Do 374 | 375 | ## Search Query Builder 376 | 377 | - [ ] implement Boosting query 378 | - [ ] implement ConstantScore query 379 | - [ ] implement DisMaxQuery query 380 | - [ ] implement MoreLikeThis query (with raw eloquent models) 381 | - [ ] implement GeoShape query 382 | 383 | ## Aggregation Query Builder 384 | 385 | - [ ] implement Nested aggregation 386 | - [ ] implement ExtendedStats aggregation 387 | - [ ] implement TopHits aggregation 388 | 389 | ## Mapping 390 | 391 | - [ ] Find a seamless way to update field mappings with zero downtime with aliases 392 | 393 | ## General 394 | 395 | - [ ] Better query builder documentation 396 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | elastic = $this->buildClient($config['connection']); 38 | 39 | $this->setDefaultIndex($config['index']); 40 | } 41 | 42 | /** 43 | * Get the default elastic index. 44 | * 45 | * @return string 46 | */ 47 | public function getDefaultIndex() 48 | { 49 | return $this->index; 50 | } 51 | 52 | /** 53 | * Get map builder instance for this connection. 54 | * 55 | * @return MapBuilder 56 | */ 57 | public function getMapBuilder() 58 | { 59 | return new MapBuilder($this); 60 | } 61 | 62 | /** 63 | * Get map grammar instance for this connection. 64 | * 65 | * @return MapBuilder 66 | */ 67 | public function getMapGrammar() 68 | { 69 | return new MapGrammar(); 70 | } 71 | 72 | /** 73 | * Get DSL grammar instance for this connection. 74 | * 75 | * @return DSLGrammar 76 | */ 77 | public function getDSLQuery() 78 | { 79 | return new DSLQuery(); 80 | } 81 | 82 | /** 83 | * Get the elastic search client instance. 84 | * 85 | * @return Client 86 | */ 87 | public function getClient() 88 | { 89 | return $this->elastic; 90 | } 91 | 92 | /** 93 | * Set a custom elastic client. 94 | * 95 | * @param Client $client 96 | */ 97 | public function setClient(Client $client) 98 | { 99 | $this->elastic = $client; 100 | } 101 | 102 | /** 103 | * Set the default index. 104 | * 105 | * @param $index 106 | * 107 | * @return Connection 108 | */ 109 | public function setDefaultIndex($index) 110 | { 111 | $this->index = $index; 112 | } 113 | 114 | /** 115 | * Execute a map statement on index;. 116 | * 117 | * @param array $mappings 118 | * 119 | * @return array 120 | */ 121 | public function mapStatement(array $mappings) 122 | { 123 | return $this->elastic->indices()->putMapping($this->setStatementIndex($mappings)); 124 | } 125 | 126 | /** 127 | * Execute a map statement on index;. 128 | * 129 | * @param array $search 130 | * 131 | * @return array 132 | */ 133 | public function searchStatement(array $search) 134 | { 135 | return $this->elastic->search($this->setStatementIndex($search)); 136 | } 137 | 138 | /** 139 | * Execute a map statement on index;. 140 | * 141 | * @param array $suggestions 142 | * 143 | * @return array 144 | */ 145 | public function suggestStatement(array $suggestions) 146 | { 147 | return $this->elastic->suggest($this->setStatementIndex($suggestions)); 148 | } 149 | 150 | /** 151 | * Execute a insert statement on index;. 152 | * 153 | * @param $params 154 | * 155 | * @return array 156 | */ 157 | public function indexStatement(array $params) 158 | { 159 | return $this->elastic->index($this->setStatementIndex($params)); 160 | } 161 | 162 | /** 163 | * Execute a update statement on index;. 164 | * 165 | * @param $params 166 | * 167 | * @return array 168 | */ 169 | public function updateStatement(array $params) 170 | { 171 | return $this->elastic->update($this->setStatementIndex($params)); 172 | } 173 | 174 | /** 175 | * Execute a update statement on index;. 176 | * 177 | * @param $params 178 | * 179 | * @return array 180 | */ 181 | public function deleteStatement(array $params) 182 | { 183 | return $this->elastic->delete($this->setStatementIndex($params)); 184 | } 185 | 186 | /** 187 | * Execute a exists statement on index. 188 | * 189 | * @param array $params 190 | * 191 | * @return array|bool 192 | */ 193 | public function existsStatement(array $params) 194 | { 195 | return $this->elastic->exists($this->setStatementIndex($params)); 196 | } 197 | 198 | /** 199 | * Execute a bulk statement on index;. 200 | * 201 | * @param $params 202 | * 203 | * @return array 204 | */ 205 | public function bulkStatement(array $params) 206 | { 207 | return $this->elastic->bulk($params); 208 | } 209 | 210 | /** 211 | * Begin a fluent search query builder. 212 | * 213 | * @return SearchBuilder 214 | */ 215 | public function search() 216 | { 217 | return new SearchBuilder($this, $this->getDSLQuery()); 218 | } 219 | 220 | /** 221 | * Begin a fluent suggest query builder. 222 | * 223 | * @return SuggestionBuilder 224 | */ 225 | public function suggest() 226 | { 227 | return new SuggestionBuilder($this, $this->getDSLQuery()); 228 | } 229 | 230 | /** 231 | * Create a new elastic persistence handler. 232 | * 233 | * @return EloquentPersistence 234 | */ 235 | public function persist() 236 | { 237 | return new EloquentPersistence($this); 238 | } 239 | 240 | /** 241 | * Create an elastic search instance. 242 | * 243 | * @param array $config 244 | * 245 | * @return Client 246 | */ 247 | private function buildClient(array $config) 248 | { 249 | $client = ClientBuilder::create() 250 | ->setHosts($config['hosts']); 251 | 252 | if (isset($config['retries'])) { 253 | $client->setRetries($config['retries']); 254 | } 255 | 256 | if (isset($config['logging']) and $config['logging']['enabled'] == true) { 257 | $logger = ClientBuilder::defaultLogger($config['logging']['path'], $config['logging']['level']); 258 | $client->setLogger($logger); 259 | } 260 | 261 | return $client->build(); 262 | } 263 | 264 | /** 265 | * @param array $params 266 | * 267 | * @return array 268 | */ 269 | private function setStatementIndex(array $params) 270 | { 271 | if (isset($params['index']) and $params['index']) { 272 | return $params; 273 | } 274 | 275 | // merge the default index with the given params if the index is not set. 276 | return array_merge($params, ['index' => $this->getDefaultIndex()]); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/Console/Index/Populate.php: -------------------------------------------------------------------------------- 1 | index(); 45 | 46 | // Checks if the target index exists 47 | if (!$this->existsStatement($index)) { 48 | $this->error('Index « '.$index.' » does not exists.'); 49 | 50 | return; 51 | } 52 | 53 | // Runs the mappings 54 | if ($this->option('mappings')) { 55 | $this->call('mapping:rerun', [ 56 | '--index' => $index, 57 | '--database' => $this->option('database'), 58 | '--force' => true, 59 | ]); 60 | } 61 | 62 | // Populates the index 63 | try { 64 | $this->populateIndex($index); 65 | } catch (\Exception $e) { 66 | $this->warn('An error occured while populating the new index !'); 67 | 68 | throw $e; 69 | } 70 | } 71 | 72 | /** 73 | * Populates the index. 74 | * 75 | * @param string $index The index name 76 | * 77 | * @throws \Exception 78 | */ 79 | protected function populateIndex($index) 80 | { 81 | $this->line('Populating the index « '.$index.' » ...'); 82 | 83 | // Replaces the current default index by the one we want to populate 84 | $defaultIndex = Plastic::getDefaultIndex(); 85 | Plastic::setDefaultIndex($index); 86 | 87 | // Disables query logging to prevent memory leak 88 | $logging = DB::connection()->logging(); 89 | DB::connection()->disableQueryLog(); 90 | 91 | // Populates from models 92 | $models = $this->models($index); 93 | $chunkSize = $this->chunkSize(); 94 | foreach ($models as $model) { 95 | $this->line('Indexing documents of model « '.$model.' » ...'); 96 | $model::chunk($chunkSize, function ($items) { 97 | $this->line('Indexing chunk of '.$items->count().' documents ...'); 98 | Plastic::persist()->bulkSave($items); 99 | }); 100 | } 101 | 102 | // Restores query logging 103 | if ($logging) { 104 | DB::connection()->enableQueryLog(); 105 | } 106 | 107 | // Restores the current default index 108 | Plastic::setDefaultIndex($defaultIndex); 109 | } 110 | 111 | /** 112 | * Gets the index to populate. 113 | * 114 | * @return array|string 115 | */ 116 | protected function index() 117 | { 118 | return $this->option('index') ?? Plastic::getDefaultIndex(); 119 | } 120 | 121 | /** 122 | * Execute a exists statement for index. 123 | * 124 | * @param $index 125 | * 126 | * @return bool 127 | */ 128 | protected function existsStatement($index) 129 | { 130 | return $this->client()->indices()->exists(['index' => $index]); 131 | } 132 | 133 | /** 134 | * Gets the models to index for the given index. 135 | * 136 | * @param $index 137 | * 138 | * @return array 139 | */ 140 | protected function models($index) 141 | { 142 | return collect(config('plastic.populate.models'))->get($index, []); 143 | } 144 | 145 | /** 146 | * Gets the chunk size. 147 | * 148 | * @return int 149 | */ 150 | protected function chunkSize() 151 | { 152 | return (int) config('plastic.populate.chunk_size'); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Console/Mapping/BaseCommand.php: -------------------------------------------------------------------------------- 1 | laravel->databasePath().DIRECTORY_SEPARATOR.'mappings'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Console/Mapping/Install.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 34 | } 35 | 36 | /** 37 | * Execute the console command. 38 | */ 39 | public function handle() 40 | { 41 | $this->repository->setSource($this->option('database')); 42 | 43 | $this->repository->createRepository(); 44 | 45 | $this->info('Mapping table created successfully'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Console/Mapping/Make.php: -------------------------------------------------------------------------------- 1 | creator = $creator; 45 | $this->composer = $composer; 46 | } 47 | 48 | /** 49 | * Execute the console command. 50 | */ 51 | public function handle() 52 | { 53 | $model = trim($this->argument('model')); 54 | 55 | $this->writeMapping($model); 56 | 57 | $this->composer->dumpAutoloads(); 58 | } 59 | 60 | /** 61 | * Create the mapping file. 62 | * 63 | * @param $model 64 | */ 65 | private function writeMapping($model) 66 | { 67 | $path = $this->getMappingPath(); 68 | 69 | $file = pathinfo($this->creator->create($model, $path), PATHINFO_FILENAME); 70 | 71 | $this->comment($file.' was created successfully'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Console/Mapping/ReRun.php: -------------------------------------------------------------------------------- 1 | confirmToProceed()) { 40 | return; 41 | } 42 | 43 | $this->call('mapping:reset', [ 44 | '--database' => $this->option('database'), 45 | '--force' => true, 46 | ]); 47 | 48 | $this->call('mapping:run', [ 49 | '--index' => $this->option('index'), 50 | '--database' => $this->option('database'), 51 | '--force' => true, 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Console/Mapping/Reset.php: -------------------------------------------------------------------------------- 1 | mappings = $mappings; 42 | } 43 | 44 | /** 45 | * Execute the console command. 46 | */ 47 | public function handle() 48 | { 49 | if (!$this->confirmToProceed()) { 50 | return; 51 | } 52 | 53 | $this->mappings->setSource($this->option('database')); 54 | 55 | $this->mappings->reset(); 56 | 57 | $this->comment('Mapping repository reset successfully'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Console/Mapping/Run.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | */ 46 | public function handle() 47 | { 48 | if (!$this->confirmToProceed()) { 49 | return; 50 | } 51 | 52 | $this->prepareDatabase(); 53 | 54 | $path = $this->getMappingPath(); 55 | 56 | $this->mapper->run($path, [ 57 | 'step' => $this->option('step'), 58 | 'index' => $this->option('index'), 59 | ]); 60 | 61 | // Once the mapper has run we will grab the note output and send it out to 62 | // the console screen, since the mapper itself functions without having 63 | // any instances of the OutputInterface contract passed into the class. 64 | foreach ($this->mapper->getNotes() as $note) { 65 | $this->output->writeln($note); 66 | } 67 | } 68 | 69 | protected function prepareDatabase() 70 | { 71 | $this->mapper->setConnection($this->option('database')); 72 | 73 | if (!$this->mapper->repositoryExists()) { 74 | $options = ['--database' => $this->option('database')]; 75 | 76 | $this->call('mapping:install', $options); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/DSL/AggregationBuilder.php: -------------------------------------------------------------------------------- 1 | query = $query; 43 | } 44 | 45 | /** 46 | * Add an average aggregate. 47 | * 48 | * @param $alias 49 | * @param string|null $field 50 | * @param string|null $script 51 | */ 52 | public function average($alias, $field = null, $script = null) 53 | { 54 | $aggregation = new AvgAggregation($alias, $field, $script); 55 | 56 | $this->append($aggregation); 57 | } 58 | 59 | /** 60 | * Add an cardinality aggregate. 61 | * 62 | * @param $alias 63 | * @param string|null $field 64 | * @param string|null $script 65 | * @param int $precision 66 | * @param bool $rehash 67 | */ 68 | public function cardinality($alias, $field = null, $script = null, $precision = null, $rehash = null) 69 | { 70 | $aggregation = new CardinalityAggregation($alias); 71 | 72 | $aggregation->setField($field); 73 | 74 | $aggregation->setScript($script); 75 | 76 | $aggregation->setPrecisionThreshold($precision); 77 | 78 | $aggregation->setRehash($rehash); 79 | 80 | $this->append($aggregation); 81 | } 82 | 83 | /** 84 | * Add a date range aggregate. 85 | * 86 | * @param $alias 87 | * @param $field 88 | * @param $format 89 | * @param array $ranges 90 | * 91 | * @internal param null $from 92 | * @internal param null $to 93 | */ 94 | public function dateRange($alias, $field, $format, array $ranges) 95 | { 96 | $aggregation = new DateRangeAggregation($alias, $field, $format, $ranges); 97 | 98 | $this->append($aggregation); 99 | } 100 | 101 | /** 102 | * Add a geo bounds aggregate. 103 | * 104 | * @param string $alias 105 | * @param null|string $field 106 | * @param bool $wrap_longitude 107 | */ 108 | public function geoBounds($alias, $field, $wrap_longitude = true) 109 | { 110 | $aggregation = new GeoBoundsAggregation($alias, $field, $wrap_longitude); 111 | 112 | $this->append($aggregation); 113 | } 114 | 115 | /** 116 | * Add a geo bounds aggregate. 117 | * 118 | * @param string $alias 119 | * @param null|string $field 120 | * @param string $origin 121 | * @param array $ranges 122 | */ 123 | public function geoDistance($alias, $field, $origin, array $ranges) 124 | { 125 | $aggregation = new GeoDistanceAggregation($alias, $field, $origin, $ranges); 126 | 127 | $this->append($aggregation); 128 | } 129 | 130 | /** 131 | * Add a geo hash grid aggregate. 132 | * 133 | * @param string $alias 134 | * @param null|string $field 135 | * @param float $precision 136 | * @param null $size 137 | * @param null $shardSize 138 | */ 139 | public function geoHashGrid($alias, $field, $precision, $size = null, $shardSize = null) 140 | { 141 | $aggregation = new GeoHashGridAggregation($alias, $field, $precision, $size, $shardSize); 142 | 143 | $this->append($aggregation); 144 | } 145 | 146 | /** 147 | * Add a histogram aggregate. 148 | * 149 | * @param $alias 150 | * @param string $field 151 | * @param int $interval 152 | * @param int $minDocCount 153 | * @param string $orderMode 154 | * @param string $orderDirection 155 | * @param int $extendedBoundsMin 156 | * @param int $extendedBoundsMax 157 | * @param bool $keyed 158 | */ 159 | public function histogram( 160 | $alias, 161 | $field, 162 | $interval, 163 | $minDocCount = null, 164 | $orderMode = null, 165 | $orderDirection = 'asc', 166 | $extendedBoundsMin = null, 167 | $extendedBoundsMax = null, 168 | $keyed = null 169 | ) { 170 | $aggregation = new HistogramAggregation( 171 | $alias, 172 | $field, 173 | $interval, 174 | $minDocCount, 175 | $orderMode, 176 | $orderDirection, 177 | $extendedBoundsMin, 178 | $extendedBoundsMax, 179 | $keyed 180 | ); 181 | 182 | $this->append($aggregation); 183 | } 184 | 185 | /** 186 | * Add an ipv4 range aggregate. 187 | * 188 | * @param $alias 189 | * @param null $field 190 | * @param array $ranges 191 | */ 192 | public function ipv4Range($alias, $field, array $ranges) 193 | { 194 | $aggregation = new Ipv4RangeAggregation($alias, $field, $ranges); 195 | 196 | $this->append($aggregation); 197 | } 198 | 199 | /** 200 | * Add an max aggregate. 201 | * 202 | * @param $alias 203 | * @param string|null $field 204 | * @param string|null $script 205 | */ 206 | public function max($alias, $field = null, $script = null) 207 | { 208 | $aggregation = new MaxAggregation($alias, $field, $script); 209 | 210 | $this->append($aggregation); 211 | } 212 | 213 | /** 214 | * Add an min aggregate. 215 | * 216 | * @param $alias 217 | * @param string|null $field 218 | * @param string|null $script 219 | */ 220 | public function min($alias, $field = null, $script = null) 221 | { 222 | $aggregation = new MinAggregation($alias, $field, $script); 223 | 224 | $this->append($aggregation); 225 | } 226 | 227 | /** 228 | * Add an missing aggregate. 229 | * 230 | * @param string $alias 231 | * @param string $field 232 | */ 233 | public function missing($alias, $field) 234 | { 235 | $aggregation = new MissingAggregation($alias, $field); 236 | 237 | $this->append($aggregation); 238 | } 239 | 240 | /** 241 | * Add an percentile aggregate. 242 | * 243 | * @param $alias 244 | * @param string $field 245 | * @param $percents 246 | * @param null $script 247 | * @param null $compression 248 | */ 249 | public function percentile($alias, $field, $percents, $script = null, $compression = null) 250 | { 251 | $aggregation = new PercentilesAggregation($alias, $field, $percents, $script, $compression); 252 | 253 | $this->append($aggregation); 254 | } 255 | 256 | /** 257 | * Add an percentileRanks aggregate. 258 | * 259 | * @param $alias 260 | * @param string $field 261 | * @param array $values 262 | * @param null $script 263 | * @param null $compression 264 | */ 265 | public function percentileRanks($alias, $field, array $values, $script = null, $compression = null) 266 | { 267 | $aggregation = new PercentileRanksAggregation($alias, $field, $values, $script, $compression); 268 | 269 | $this->append($aggregation); 270 | } 271 | 272 | /** 273 | * Add an stats aggregate. 274 | * 275 | * @param $alias 276 | * @param string $field 277 | * @param string|null $script 278 | */ 279 | public function stats($alias, $field = null, $script = null) 280 | { 281 | $aggregation = new StatsAggregation($alias, $field, $script); 282 | 283 | $this->append($aggregation); 284 | } 285 | 286 | /** 287 | * Add an sum aggregate. 288 | * 289 | * @param $alias 290 | * @param string $field 291 | * @param string|null $script 292 | */ 293 | public function sum($alias, $field = null, $script = null) 294 | { 295 | $aggregation = new SumAggregation($alias, $field, $script); 296 | 297 | $this->append($aggregation); 298 | } 299 | 300 | /** 301 | * Add a value count aggregate. 302 | * 303 | * @param $alias 304 | * @param string $field 305 | * @param string|null $script 306 | */ 307 | public function valueCount($alias, $field = null, $script = null) 308 | { 309 | $aggregation = new ValueCountAggregation($alias, $field, $script); 310 | 311 | $this->append($aggregation); 312 | } 313 | 314 | /** 315 | * Add a range aggregate. 316 | * 317 | * @param string $alias 318 | * @param string $field 319 | * @param array $ranges 320 | * @param bool $keyed 321 | */ 322 | public function range($alias, $field, array $ranges, $keyed = false) 323 | { 324 | $aggregation = new RangeAggregation($alias, $field, $ranges, $keyed); 325 | 326 | $this->append($aggregation); 327 | } 328 | 329 | /** 330 | * Add a terms aggregate. 331 | * 332 | * @param string $alias 333 | * @param string|null $field 334 | * @param string|null $script 335 | */ 336 | public function terms($alias, $field = null, $script = null) 337 | { 338 | $aggregation = new TermsAggregation($alias, $field, $script); 339 | 340 | $this->append($aggregation); 341 | } 342 | 343 | /** 344 | * Return the DSL query. 345 | * 346 | * @return array 347 | */ 348 | public function toDSL() 349 | { 350 | return $this->query->toArray(); 351 | } 352 | 353 | /** 354 | * Append an aggregation to the aggregation query builder. 355 | * 356 | * @param AbstractAggregation $aggregation 357 | */ 358 | public function append(AbstractAggregation $aggregation) 359 | { 360 | $this->query->addAggregation($aggregation); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/DSL/FunctionScoreBuilder.php: -------------------------------------------------------------------------------- 1 | query = new FunctionScoreQuery($search->query->getQueries(), $parameters); 26 | } 27 | 28 | /** 29 | * @param $field 30 | * @param $factor 31 | * @param string $modifier 32 | * @param null $query 33 | */ 34 | public function field($field, $factor, $modifier = 'none', $query = null) 35 | { 36 | $this->query->addFieldValueFactorFunction($field, $factor, $modifier, $query); 37 | } 38 | 39 | /** 40 | * @param $type 41 | * @param $field 42 | * @param $function 43 | * @param array $options 44 | * @param null $query 45 | */ 46 | public function decay($type, $field, $function, $options = [], $query = null) 47 | { 48 | $this->query->addDecayFunction($type, $field, $function, $options, $query); 49 | } 50 | 51 | /** 52 | * @param $weight 53 | * @param null $query 54 | */ 55 | public function weight($weight, $query = null) 56 | { 57 | $this->query->addWeightFunction($weight, $query); 58 | } 59 | 60 | /** 61 | * @param $seed 62 | * @param null $query 63 | */ 64 | public function random($seed = null, $query = null) 65 | { 66 | $this->query->addRandomFunction($seed, $query); 67 | } 68 | 69 | /** 70 | * @param $inline 71 | * @param array $params 72 | * @param array $options 73 | * @param null $query 74 | */ 75 | public function script($inline, $params = [], $options = [], $query = null) 76 | { 77 | $this->query->addScriptScoreFunction($inline, $params, $options, $query); 78 | } 79 | 80 | /** 81 | * @param $functions 82 | */ 83 | public function simple($functions) 84 | { 85 | $this->query->addSimpleFunction($functions); 86 | } 87 | 88 | /** 89 | * Return the DSL query. 90 | * 91 | * @return array 92 | */ 93 | public function toDSL() 94 | { 95 | return $this->query->toArray(); 96 | } 97 | 98 | /** 99 | * @return FunctionScoreQuery 100 | */ 101 | public function getQuery() 102 | { 103 | return $this->query; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/DSL/SearchBuilder.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 102 | $this->query = $grammar ?: $connection->getDSLGrammar(); 103 | } 104 | 105 | /** 106 | * Set the elastic type to query against. 107 | * 108 | * @param string $type 109 | * 110 | * @return $this 111 | */ 112 | public function type($type) 113 | { 114 | $this->type = $type; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Set the elastic index to query against. 121 | * 122 | * @param string $index 123 | * 124 | * @return $this 125 | */ 126 | public function index($index) 127 | { 128 | $this->index = $index; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Set the eloquent model to use when querying elastic search. 135 | * 136 | * @param Model|Searchable $model 137 | * 138 | * @throws InvalidArgumentException 139 | * 140 | * @return $this 141 | */ 142 | public function model(Model $model) 143 | { 144 | // Check if the model is searchable before setting the query builder model 145 | $traits = class_uses_recursive(get_class($model)); 146 | 147 | if (!isset($traits[Searchable::class])) { 148 | throw new InvalidArgumentException(get_class($model).' does not use the searchable trait'); 149 | } 150 | 151 | $this->type($model->getDocumentType()); 152 | 153 | if ($index = $model->getDocumentIndex()) { 154 | $this->index($index); 155 | } 156 | 157 | $this->model = $model; 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * Set the query from/offset value. 164 | * 165 | * @param int $offset 166 | * 167 | * @return $this 168 | */ 169 | public function from($offset) 170 | { 171 | $this->query->setFrom($offset); 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Set the query limit/size value. 178 | * 179 | * @param int $limit 180 | * 181 | * @return $this 182 | */ 183 | public function size($limit) 184 | { 185 | $this->query->setSize($limit); 186 | 187 | return $this; 188 | } 189 | 190 | /** 191 | * Set the query sort values values. 192 | * 193 | * @param string|array $fields 194 | * @param null $order 195 | * @param array $parameters 196 | * 197 | * @return $this 198 | */ 199 | public function sortBy($fields, $order = null, array $parameters = []) 200 | { 201 | $fields = is_array($fields) ? $fields : [$fields]; 202 | 203 | foreach ($fields as $field) { 204 | $sort = new FieldSort($field, $order, $parameters); 205 | 206 | $this->query->addSort($sort); 207 | } 208 | 209 | return $this; 210 | } 211 | 212 | /** 213 | * Set the query min score value. 214 | * 215 | * @param $score 216 | * 217 | * @return $this 218 | */ 219 | public function minScore($score) 220 | { 221 | $this->query->setMinScore($score); 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * Switch to a should statement. 228 | */ 229 | public function should() 230 | { 231 | $this->boolState = BoolQuery::SHOULD; 232 | 233 | return $this; 234 | } 235 | 236 | /** 237 | * Switch to a must statement. 238 | */ 239 | public function must() 240 | { 241 | $this->boolState = BoolQuery::MUST; 242 | 243 | return $this; 244 | } 245 | 246 | /** 247 | * Switch to a must not statement. 248 | */ 249 | public function mustNot() 250 | { 251 | $this->boolState = BoolQuery::MUST_NOT; 252 | 253 | return $this; 254 | } 255 | 256 | /** 257 | * Switch to a filter query. 258 | */ 259 | public function filter() 260 | { 261 | $this->boolState = BoolQuery::FILTER; 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Add an ids query. 268 | * 269 | * @param array | string $ids 270 | * 271 | * @return $this 272 | */ 273 | public function ids($ids) 274 | { 275 | $ids = is_array($ids) ? $ids : [$ids]; 276 | 277 | $query = new IdsQuery($ids); 278 | 279 | $this->append($query); 280 | 281 | return $this; 282 | } 283 | 284 | /** 285 | * Add an term query. 286 | * 287 | * @param string $field 288 | * @param string $term 289 | * @param array $attributes 290 | * 291 | * @return $this 292 | */ 293 | public function term($field, $term, array $attributes = []) 294 | { 295 | $query = new TermQuery($field, $term, $attributes); 296 | 297 | $this->append($query); 298 | 299 | return $this; 300 | } 301 | 302 | /** 303 | * Add an terms query. 304 | * 305 | * @param string $field 306 | * @param array $terms 307 | * @param array $attributes 308 | * 309 | * @return $this 310 | */ 311 | public function terms($field, array $terms, array $attributes = []) 312 | { 313 | $query = new TermsQuery($field, $terms, $attributes); 314 | 315 | $this->append($query); 316 | 317 | return $this; 318 | } 319 | 320 | /** 321 | * Add an exists query. 322 | * 323 | * @param string|array $fields 324 | * 325 | * @return $this 326 | */ 327 | public function exists($fields) 328 | { 329 | $fields = is_array($fields) ? $fields : [$fields]; 330 | 331 | foreach ($fields as $field) { 332 | $query = new ExistsQuery($field); 333 | 334 | $this->append($query); 335 | } 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * Add a wildcard query. 342 | * 343 | * @param string $field 344 | * @param string $value 345 | * @param float $boost 346 | * 347 | * @return $this 348 | */ 349 | public function wildcard($field, $value, $boost = 1.0) 350 | { 351 | $query = new WildcardQuery($field, $value, ['boost' => $boost]); 352 | 353 | $this->append($query); 354 | 355 | return $this; 356 | } 357 | 358 | /** 359 | * Add a boost query. 360 | * 361 | * @param float|null $boost 362 | * 363 | * @return $this 364 | * 365 | * @internal param $field 366 | */ 367 | public function matchAll($boost = 1.0) 368 | { 369 | $query = new MatchAllQuery(['boost' => $boost]); 370 | 371 | $this->append($query); 372 | 373 | return $this; 374 | } 375 | 376 | /** 377 | * Add a match query. 378 | * 379 | * @param string $field 380 | * @param string $term 381 | * @param array $attributes 382 | * 383 | * @return $this 384 | */ 385 | public function match($field, $term, array $attributes = []) 386 | { 387 | $query = new MatchQuery($field, $term, $attributes); 388 | 389 | $this->append($query); 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * Add a multi match query. 396 | * 397 | * @param array $fields 398 | * @param string $term 399 | * @param array $attributes 400 | * 401 | * @return $this 402 | */ 403 | public function multiMatch(array $fields, $term, array $attributes = []) 404 | { 405 | $query = new MultiMatchQuery($fields, $term, $attributes); 406 | 407 | $this->append($query); 408 | 409 | return $this; 410 | } 411 | 412 | /** 413 | * Add a geo bounding box query. 414 | * 415 | * @param string $field 416 | * @param array $values 417 | * @param array $parameters 418 | * 419 | * @return $this 420 | */ 421 | public function geoBoundingBox($field, $values, array $parameters = []) 422 | { 423 | $query = new GeoBoundingBoxQuery($field, $values, $parameters); 424 | 425 | $this->append($query); 426 | 427 | return $this; 428 | } 429 | 430 | /** 431 | * Add a geo distance query. 432 | * 433 | * @param string $field 434 | * @param string $distance 435 | * @param mixed $location 436 | * @param array $attributes 437 | * 438 | * @return $this 439 | */ 440 | public function geoDistance($field, $distance, $location, array $attributes = []) 441 | { 442 | $query = new GeoDistanceQuery($field, $distance, $location, $attributes); 443 | 444 | $this->append($query); 445 | 446 | return $this; 447 | } 448 | 449 | /** 450 | * Add a geo distance range query. 451 | * 452 | * @param string $field 453 | * @param $from 454 | * @param $to 455 | * @param mixed $location 456 | * @param array $attributes 457 | * 458 | * @return $this 459 | */ 460 | public function geoDistanceRange($field, $from, $to, array $location, array $attributes = []) 461 | { 462 | $range = compact('from', 'to'); 463 | 464 | $query = new GeoDistanceRangeQuery($field, $range, $location, $attributes); 465 | 466 | $this->append($query); 467 | 468 | return $this; 469 | } 470 | 471 | /** 472 | * Add a geo polygon query. 473 | * 474 | * @param string $field 475 | * @param array $points 476 | * @param array $attributes 477 | * 478 | * @return $this 479 | */ 480 | public function geoPolygon($field, array $points = [], array $attributes = []) 481 | { 482 | $query = new GeoPolygonQuery($field, $points, $attributes); 483 | 484 | $this->append($query); 485 | 486 | return $this; 487 | } 488 | 489 | /** 490 | * Add a geo shape query. 491 | * 492 | * @param string $field 493 | * @param $type 494 | * @param array $coordinates 495 | * @param array $attributes 496 | * 497 | * @return $this 498 | */ 499 | public function geoShape($field, $type, array $coordinates = [], array $attributes = []) 500 | { 501 | $query = new GeoShapeQuery(); 502 | 503 | $query->addShape($field, $type, $coordinates, $attributes); 504 | 505 | $this->append($query); 506 | 507 | return $this; 508 | } 509 | 510 | /** 511 | * Add a prefix query. 512 | * 513 | * @param string $field 514 | * @param string $term 515 | * @param array $attributes 516 | * 517 | * @return $this 518 | */ 519 | public function prefix($field, $term, array $attributes = []) 520 | { 521 | $query = new PrefixQuery($field, $term, $attributes); 522 | 523 | $this->append($query); 524 | 525 | return $this; 526 | } 527 | 528 | /** 529 | * Add a query string query. 530 | * 531 | * @param string $query 532 | * @param array $attributes 533 | * 534 | * @return $this 535 | */ 536 | public function queryString($query, array $attributes = []) 537 | { 538 | $query = new QueryStringQuery($query, $attributes); 539 | 540 | $this->append($query); 541 | 542 | return $this; 543 | } 544 | 545 | /** 546 | * Add a simple query string query. 547 | * 548 | * @param string $query 549 | * @param array $attributes 550 | * 551 | * @return $this 552 | */ 553 | public function simpleQueryString($query, array $attributes = []) 554 | { 555 | $query = new SimpleQueryStringQuery($query, $attributes); 556 | 557 | $this->append($query); 558 | 559 | return $this; 560 | } 561 | 562 | /** 563 | * Add a highlight to result. 564 | * 565 | * @param array $fields 566 | * @param array $parameters 567 | * @param string $preTag 568 | * @param string $postTag 569 | * 570 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html 571 | * 572 | * @return $this 573 | */ 574 | public function highlight($fields = ['_all' => []], $parameters = [], $preTag = '', $postTag = '') 575 | { 576 | $highlight = new Highlight(); 577 | $highlight->setTags([$preTag], [$postTag]); 578 | 579 | foreach ($fields as $field => $fieldParams) { 580 | $highlight->addField($field, $fieldParams); 581 | } 582 | 583 | if ($parameters) { 584 | $highlight->setParameters($parameters); 585 | } 586 | 587 | $this->query->addHighlight($highlight); 588 | 589 | return $this; 590 | } 591 | 592 | /** 593 | * Add a range query. 594 | * 595 | * @param string $field 596 | * @param array $attributes 597 | * 598 | * @return $this 599 | */ 600 | public function range($field, array $attributes = []) 601 | { 602 | $query = new RangeQuery($field, $attributes); 603 | 604 | $this->append($query); 605 | 606 | return $this; 607 | } 608 | 609 | /** 610 | * Add a regexp query. 611 | * 612 | * @param string $field 613 | * @param array $attributes 614 | * 615 | * @return $this 616 | */ 617 | public function regexp($field, $regex, array $attributes = []) 618 | { 619 | $query = new RegexpQuery($field, $regex, $attributes); 620 | 621 | $this->append($query); 622 | 623 | return $this; 624 | } 625 | 626 | /** 627 | * Add a common term query. 628 | * 629 | * @param $field 630 | * @param $term 631 | * @param array $attributes 632 | * 633 | * @return $this 634 | */ 635 | public function commonTerm($field, $term, array $attributes = []) 636 | { 637 | $query = new CommonTermsQuery($field, $term, $attributes); 638 | 639 | $this->append($query); 640 | 641 | return $this; 642 | } 643 | 644 | /** 645 | * Add a fuzzy query. 646 | * 647 | * @param $field 648 | * @param $term 649 | * @param array $attributes 650 | * 651 | * @return $this 652 | */ 653 | public function fuzzy($field, $term, array $attributes = []) 654 | { 655 | $query = new FuzzyQuery($field, $term, $attributes); 656 | 657 | $this->append($query); 658 | 659 | return $this; 660 | } 661 | 662 | /** 663 | * Add a nested query. 664 | * 665 | * @param $field 666 | * @param \Closure $closure 667 | * @param string $score_mode 668 | * 669 | * @return $this 670 | */ 671 | public function nested($field, \Closure $closure, $score_mode = 'avg') 672 | { 673 | $builder = new self($this->connection, new $this->query()); 674 | 675 | $closure($builder); 676 | 677 | $nestedQuery = $builder->query->getQueries(); 678 | 679 | $query = new NestedQuery($field, $nestedQuery, ['score_mode' => $score_mode]); 680 | 681 | $this->append($query); 682 | 683 | return $this; 684 | } 685 | 686 | /** 687 | * Add aggregation. 688 | * 689 | * @param \Closure $closure 690 | * 691 | * @return $this 692 | */ 693 | public function aggregate(\Closure $closure) 694 | { 695 | $builder = new AggregationBuilder($this->query); 696 | 697 | $closure($builder); 698 | 699 | return $this; 700 | } 701 | 702 | /** 703 | * Add function score. 704 | * 705 | * @param \Closure $search 706 | * @param \Closure $closure 707 | * @param array $parameters 708 | * 709 | * @return $this 710 | */ 711 | public function functions(\Closure $search, \Closure $closure, $parameters = []) 712 | { 713 | $builder = new self($this->connection, new $this->query()); 714 | $search($builder); 715 | 716 | $builder = new FunctionScoreBuilder($builder, $parameters); 717 | 718 | $closure($builder); 719 | 720 | $this->append($builder->getQuery()); 721 | 722 | return $this; 723 | } 724 | 725 | /** 726 | * Set the model filler to use after retrieving the results. 727 | * 728 | * @param FillerInterface $filler 729 | */ 730 | public function setModelFiller(FillerInterface $filler) 731 | { 732 | $this->modelFiller = $filler; 733 | } 734 | 735 | /** 736 | * get the model filler to use after retrieving the results. 737 | * 738 | * @return FillerInterface 739 | */ 740 | public function getModelFiller() 741 | { 742 | return $this->modelFiller ? $this->modelFiller : new EloquentFiller(); 743 | } 744 | 745 | /** 746 | * Execute the search query against elastic and return the raw result. 747 | * 748 | * @return array 749 | */ 750 | public function getRaw() 751 | { 752 | $params = [ 753 | 'index' => $this->getIndex(), 754 | 'type' => $this->getType(), 755 | 'body' => $this->toDSL(), 756 | ]; 757 | 758 | return $this->connection->searchStatement($params); 759 | } 760 | 761 | /** 762 | * Execute the search query against elastic and return the raw result if the model is not set. 763 | * 764 | * @return PlasticResult 765 | */ 766 | public function get() 767 | { 768 | $result = $this->getRaw(); 769 | 770 | $result = new PlasticResult($result); 771 | 772 | if ($this->model) { 773 | $this->getModelFiller()->fill($this->model, $result); 774 | } 775 | 776 | return $result; 777 | } 778 | 779 | /** 780 | * Return the current elastic type. 781 | * 782 | * @return string 783 | */ 784 | public function getType() 785 | { 786 | return $this->type; 787 | } 788 | 789 | /** 790 | * Return the current elastic index. 791 | * 792 | * @return string 793 | */ 794 | public function getIndex() 795 | { 796 | return $this->index; 797 | } 798 | 799 | /** 800 | * Return the current plastic connection. 801 | * 802 | * @return Connection 803 | */ 804 | public function getConnection() 805 | { 806 | return $this->connection; 807 | } 808 | 809 | /** 810 | * Return the boolean query state. 811 | * 812 | * @return string 813 | */ 814 | public function getBoolState() 815 | { 816 | return $this->boolState; 817 | } 818 | 819 | /** 820 | * Paginate result hits. 821 | * 822 | * @param int $limit 823 | * @param null|int $current 824 | * 825 | * @return PlasticPaginator 826 | */ 827 | public function paginate($limit = 25, $current = null) 828 | { 829 | $page = $this->getCurrentPage($current); 830 | 831 | $from = $limit * ($page - 1); 832 | $size = $limit; 833 | 834 | $result = $this->from($from)->size($size)->get(); 835 | 836 | return new PlasticPaginator($result, $size, $page); 837 | } 838 | 839 | /** 840 | * Return the DSL query. 841 | * 842 | * @return array 843 | */ 844 | public function toDSL() 845 | { 846 | return $this->query->toArray(); 847 | } 848 | 849 | /** 850 | * Append a query. 851 | * 852 | * @param $query 853 | * 854 | * @return $this 855 | */ 856 | public function append($query) 857 | { 858 | $this->query->addQuery($query, $this->getBoolState()); 859 | 860 | return $this; 861 | } 862 | 863 | /** 864 | * return the current query string value. 865 | * 866 | * @param null|int $current 867 | * 868 | * @return int 869 | */ 870 | protected function getCurrentPage($current) 871 | { 872 | return $current ?: (int) \Request::get('page', 1); 873 | } 874 | } 875 | -------------------------------------------------------------------------------- /src/DSL/SuggestionBuilder.php: -------------------------------------------------------------------------------- 1 | query = $query; 41 | 42 | $this->connection = $connection; 43 | } 44 | 45 | /** 46 | * Set the elastic index to query against. 47 | * 48 | * @param string $index 49 | * 50 | * @return $this 51 | */ 52 | public function index($index) 53 | { 54 | $this->index = $index; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Return the current elastic index. 61 | * 62 | * @return string 63 | */ 64 | public function getIndex() 65 | { 66 | return $this->index; 67 | } 68 | 69 | /** 70 | * Add a completion suggestion. 71 | * 72 | * @param $name 73 | * @param $text 74 | * @param $field 75 | * @param array $parameters 76 | * 77 | * @return $this 78 | * 79 | * @internal param $fields 80 | */ 81 | public function completion($name, $text, $field = 'suggest', $parameters = []) 82 | { 83 | $suggestion = new Suggest($name, 'completion', $text, $field, $parameters); 84 | 85 | $this->append($suggestion); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Add a term suggestion. 92 | * 93 | * @param string $name 94 | * @param string $text 95 | * @param $field 96 | * @param array $parameters 97 | * 98 | * @return $this 99 | */ 100 | public function term($name, $text, $field = '_all', array $parameters = []) 101 | { 102 | $suggestion = new Suggest($name, 'term', $text, $field, $parameters); 103 | 104 | $this->append($suggestion); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Return the DSL query. 111 | * 112 | * @return array 113 | */ 114 | public function toDSL() 115 | { 116 | return $this->query->toArray()['suggest']; 117 | } 118 | 119 | /** 120 | * Execute the suggest query against elastic and return the raw result if model not set. 121 | * 122 | * @return array 123 | */ 124 | public function get() 125 | { 126 | return $this->connection->suggestStatement( 127 | [ 128 | 'index' => $this->getIndex(), 129 | 'body' => $this->toDSL(), 130 | ] 131 | ); 132 | } 133 | 134 | /** 135 | * Returns the connection instance. 136 | * 137 | * @return Connection 138 | */ 139 | public function getConnection() 140 | { 141 | return $this->connection; 142 | } 143 | 144 | /** 145 | * Append a suggestion to query. 146 | * 147 | * @param $suggestion 148 | */ 149 | public function append($suggestion) 150 | { 151 | $this->query->addSuggest($suggestion); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | connection()->getMapBuilder(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Facades/Plastic.php: -------------------------------------------------------------------------------- 1 | hits()->map(function ($hit) use ($model) { 24 | return $this->fillModel($model, $hit); 25 | }); 26 | 27 | $result->setHits($hits); 28 | } 29 | 30 | /** 31 | * New From Hit Builder. 32 | * 33 | * Variation on newFromBuilder. Instead, takes 34 | * 35 | * @param $model 36 | * @param array $hit 37 | * 38 | * @return static 39 | */ 40 | public function fillModel(Model $model, $hit = []) 41 | { 42 | $key_name = $model->getKeyName(); 43 | 44 | $attributes = $hit['_source']; 45 | 46 | if (isset($hit['_id'])) { 47 | $attributes[$key_name] = is_numeric($hit['_id']) ? intval($hit['_id']) : $hit['_id']; 48 | } 49 | 50 | // Add fields to attributes 51 | if (isset($hit['fields'])) { 52 | foreach ($hit['fields'] as $key => $value) { 53 | $attributes[$key] = $value; 54 | } 55 | } 56 | 57 | $instance = $this->newFromBuilderRecursive($model, $attributes); 58 | 59 | // In addition to setting the attributes 60 | // from the index, we will set the score as well. 61 | $instance->documentScore = $hit['_score']; 62 | // This is now a model created 63 | // from an Elasticsearch document. 64 | $instance->isDocument = true; 65 | // Set our document version if it's 66 | if (isset($hit['_version'])) { 67 | $instance->documentVersion = $hit['_version']; 68 | } 69 | 70 | // Set highlighting if present in result and model 71 | if (isset($hit['highlight']) && property_exists($instance, 'highlight')) { 72 | $instance->highlight = $hit['highlight']; 73 | } 74 | 75 | return $instance; 76 | } 77 | 78 | /** 79 | * Fill a model with form an elastic hit. 80 | * 81 | * @param Model $model 82 | * @param array $attributes 83 | * @param Relation $parentRelation 84 | * 85 | * @return mixed 86 | */ 87 | public function newFromBuilderRecursive(Model $model, array $attributes = [], Relation $parentRelation = null) 88 | { 89 | $instance = $model->newInstance([], $exists = true); 90 | 91 | // fill the instance attributes with checking 92 | $instance->unguard(); 93 | $instance->fill($attributes); 94 | $instance->reguard(); 95 | // Load relations recursive 96 | $this->loadRelationsAttributesRecursive($instance); 97 | 98 | // Load pivot 99 | $this->loadPivotAttribute($instance, $parentRelation); 100 | 101 | return $instance; 102 | } 103 | 104 | /** 105 | * Get the relations attributes from a model. 106 | * 107 | * @param \Illuminate\Database\Eloquent\Model $model 108 | */ 109 | protected function loadRelationsAttributesRecursive(Model $model) 110 | { 111 | $attributes = $model->getAttributes(); 112 | 113 | foreach ($attributes as $key => $value) { 114 | if (method_exists($model, $key)) { 115 | $reflection_method = new ReflectionMethod($model, $key); 116 | 117 | if ($reflection_method->class != "Illuminate\Database\Eloquent\Model") { 118 | $relation = $model->$key(); 119 | 120 | if ($relation instanceof Relation) { 121 | 122 | // Get the relation models/model if value is not null 123 | if ($value === null) { 124 | $models = null; 125 | } else { 126 | 127 | // Check if the relation field is single model or collections 128 | if (!$multiLevelRelation = $this->isMultiLevelArray($value)) { 129 | $value = [$value]; 130 | } 131 | 132 | $models = $this->hydrateRecursive($relation->getModel(), $value, $relation); 133 | 134 | if (!$multiLevelRelation) { 135 | $models = $models->first(); 136 | } 137 | } 138 | 139 | // Unset attribute before setting relation 140 | unset($model[$key]); 141 | 142 | // Set the relation value 143 | $model->setRelation($key, $models); 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * Create a collection of models from plain arrays recursive. 152 | * 153 | * @param Model $model 154 | * @param Relation $parentRelation 155 | * @param array $items 156 | * 157 | * @return Collection 158 | */ 159 | protected function hydrateRecursive(Model $model, array $items, Relation $parentRelation = null) 160 | { 161 | $instance = $model; 162 | 163 | $items = array_map(function ($item) use ($instance, $parentRelation) { 164 | return $this->newFromBuilderRecursive($instance, $item, $parentRelation); 165 | }, $items); 166 | 167 | return $instance->newCollection($items); 168 | } 169 | 170 | /** 171 | * Get the pivot attribute from a model. 172 | * 173 | * @param \Illuminate\Database\Eloquent\Model $model 174 | * @param \Illuminate\Database\Eloquent\Relations\Relation $parentRelation 175 | */ 176 | public function loadPivotAttribute(Model $model, Relation $parentRelation = null) 177 | { 178 | $attributes = $model->getAttributes(); 179 | foreach ($attributes as $key => $value) { 180 | if ($key === 'pivot') { 181 | unset($model[$key]); 182 | $pivot = $parentRelation->newExistingPivot($value); 183 | $model->setRelation($key, $pivot); 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Check if an array is multi-level array like [[id], [id], [id]]. 190 | * 191 | * For detect if a relation field is single model or collections. 192 | * 193 | * @param array $array 194 | * 195 | * @return bool 196 | */ 197 | private function isMultiLevelArray(array $array) 198 | { 199 | foreach ($array as $key => $value) { 200 | if (!is_array($value)) { 201 | return false; 202 | } 203 | } 204 | 205 | return true; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Fillers/FillerInterface.php: -------------------------------------------------------------------------------- 1 | registerCommands(); 26 | } 27 | 28 | /** 29 | * Register all needed commands. 30 | */ 31 | protected function registerCommands() 32 | { 33 | $commands = ['Populate']; 34 | 35 | foreach ($commands as $command) { 36 | $this->{'register'.$command.'Command'}(); 37 | } 38 | 39 | $this->commands([ 40 | 'command.index.populate', 41 | ]); 42 | } 43 | 44 | /** 45 | * Register the Install command. 46 | */ 47 | protected function registerPopulateCommand() 48 | { 49 | $this->app->singleton('command.index.populate', function () { 50 | return new Populate(); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Map/Blueprint.php: -------------------------------------------------------------------------------- 1 | type = $type; 47 | 48 | if (!is_null($callback)) { 49 | $callback($this); 50 | } 51 | $this->index = $index; 52 | } 53 | 54 | /** 55 | * Execute the blueprint against the database. 56 | * 57 | * @param Connection $connection 58 | * @param Grammar $grammar 59 | * 60 | * @return array 61 | */ 62 | public function build(Connection $connection, Grammar $grammar) 63 | { 64 | $statement = [ 65 | 'index' => $this->index, 66 | 'type' => $this->type, 67 | 'body' => [ 68 | $this->type => [ 69 | '_source' => [ 70 | 'enabled' => true, 71 | ], 72 | 'properties' => $this->toDSL($grammar), 73 | ], 74 | ], 75 | ]; 76 | 77 | return $connection->mapStatement($statement); 78 | } 79 | 80 | /** 81 | * Indicate that the table needs to be created. 82 | * 83 | * @return \Illuminate\Support\Fluent 84 | */ 85 | public function create() 86 | { 87 | return $this->addCommand('create'); 88 | } 89 | 90 | /** 91 | * Add a string field to the map. 92 | * 93 | * @param string $field 94 | * @param array $attributes 95 | * 96 | * @return Fluent 97 | */ 98 | public function string($field, $attributes = []) 99 | { 100 | return $this->addField('string', $field, $attributes); 101 | } 102 | 103 | /** 104 | * Add a date field to the map. 105 | * 106 | * @param string $field 107 | * @param array $attributes 108 | * 109 | * @return Fluent 110 | */ 111 | public function date($field, $attributes = []) 112 | { 113 | return $this->addField('date', $field, $attributes); 114 | } 115 | 116 | /** 117 | * Add a long numeric field to the map. 118 | * 119 | * @param string $field 120 | * @param array $attributes 121 | * 122 | * @return Fluent 123 | */ 124 | public function long($field, $attributes = []) 125 | { 126 | return $this->addField('long', $field, $attributes); 127 | } 128 | 129 | /** 130 | * Add an integer field to the map. 131 | * 132 | * @param string $field 133 | * @param array $attributes 134 | * 135 | * @return Fluent 136 | */ 137 | public function integer($field, $attributes = []) 138 | { 139 | return $this->addField('integer', $field, $attributes); 140 | } 141 | 142 | /** 143 | * Add a short numeric field to the map. 144 | * 145 | * @param string $field 146 | * @param array $attributes 147 | * 148 | * @return Fluent 149 | */ 150 | public function short($field, $attributes = []) 151 | { 152 | return $this->addField('short', $field, $attributes); 153 | } 154 | 155 | /** 156 | * Add a byte numeric field to the map. 157 | * 158 | * @param string $field 159 | * @param array $attributes 160 | * 161 | * @return Fluent 162 | */ 163 | public function byte($field, $attributes = []) 164 | { 165 | return $this->addField('byte', $field, $attributes); 166 | } 167 | 168 | /** 169 | * Add a double field to the map. 170 | * 171 | * @param string $field 172 | * @param array $attributes 173 | * 174 | * @return Fluent 175 | */ 176 | public function double($field, $attributes = []) 177 | { 178 | return $this->addField('double', $field, $attributes); 179 | } 180 | 181 | /** 182 | * Add a binary field to the map. 183 | * 184 | * @param string $field 185 | * @param array $attributes 186 | * 187 | * @return Fluent 188 | */ 189 | public function binary($field, $attributes = []) 190 | { 191 | return $this->addField('binary', $field, $attributes); 192 | } 193 | 194 | /** 195 | * Add a float field to the map. 196 | * 197 | * @param string $field 198 | * @param array $attributes 199 | * 200 | * @return Fluent 201 | */ 202 | public function float($field, $attributes = []) 203 | { 204 | return $this->addField('float', $field, $attributes); 205 | } 206 | 207 | /** 208 | * Add a boolean field to the map. 209 | * 210 | * @param string $field 211 | * @param array $attributes 212 | * 213 | * @return Fluent 214 | */ 215 | public function boolean($field, $attributes = []) 216 | { 217 | return $this->addField('boolean', $field, $attributes); 218 | } 219 | 220 | /** 221 | * Add a geo point field to the map. 222 | * 223 | * @param string $field 224 | * @param array $attributes 225 | * 226 | * @return Fluent 227 | */ 228 | public function point($field, $attributes = []) 229 | { 230 | return $this->addField('point', $field, $attributes); 231 | } 232 | 233 | /** 234 | * Add a geo shape field to the map. 235 | * 236 | * @param string $field 237 | * @param array $attributes 238 | * 239 | * @return Fluent 240 | */ 241 | public function shape($field, $attributes = []) 242 | { 243 | return $this->addField('shape', $field, $attributes); 244 | } 245 | 246 | /** 247 | * Add an IPv4 field to the map. 248 | * 249 | * @param string $field 250 | * @param array $attributes 251 | * 252 | * @return Fluent 253 | */ 254 | public function ip($field, $attributes = []) 255 | { 256 | return $this->addField('ip', $field, $attributes); 257 | } 258 | 259 | /** 260 | * Add a completion field to the map. 261 | * 262 | * @param string $field 263 | * @param array $attributes 264 | * 265 | * @return Fluent 266 | */ 267 | public function completion($field, $attributes = []) 268 | { 269 | return $this->addField('completion', $field, $attributes); 270 | } 271 | 272 | /** 273 | * Add a completion field to the map. 274 | * 275 | * @param string $field 276 | * @param array $attributes 277 | * 278 | * @return Fluent 279 | */ 280 | public function tokenCount($field, $attributes = []) 281 | { 282 | return $this->addField('token_count', $field, $attributes); 283 | } 284 | 285 | /** 286 | * Add a nested map. 287 | * 288 | * @param $field 289 | * @param Closure $callback 290 | * 291 | * @return Fluent 292 | */ 293 | public function nested($field, Closure $callback) 294 | { 295 | return $this->addField('nested', $field, ['callback' => $callback]); 296 | } 297 | 298 | /** 299 | * Add a object map. 300 | * 301 | * @param $field 302 | * @param Closure $callback 303 | * 304 | * @return Fluent 305 | */ 306 | public function object($field, Closure $callback) 307 | { 308 | return $this->addField('object', $field, ['callback' => $callback]); 309 | } 310 | 311 | /** 312 | * Add a new field to the blueprint. 313 | * 314 | * @param string $type 315 | * @param string $name 316 | * @param array $attributes 317 | * 318 | * @return Fluent 319 | */ 320 | public function addField($type, $name, array $attributes = []) 321 | { 322 | $attributes = array_merge(compact('type', 'name'), $attributes); 323 | 324 | $this->fields[] = $field = new Fluent($attributes); 325 | 326 | return $field; 327 | } 328 | 329 | /** 330 | * Get the registered fields. 331 | * 332 | * @return array 333 | */ 334 | public function getFields() 335 | { 336 | return $this->fields; 337 | } 338 | 339 | /** 340 | * Get the command fields. 341 | * 342 | * @return array 343 | */ 344 | public function getCommands() 345 | { 346 | return $this->fields; 347 | } 348 | 349 | /** 350 | * Add a new command to the blueprint. 351 | * 352 | * @param string $name 353 | * @param array $parameters 354 | * 355 | * @return \Illuminate\Support\Fluent 356 | */ 357 | protected function addCommand($name, array $parameters = []) 358 | { 359 | $this->commands[] = $command = $this->createCommand($name, $parameters); 360 | 361 | return $command; 362 | } 363 | 364 | /** 365 | * Create a new Fluent command. 366 | * 367 | * @param string $name 368 | * @param array $parameters 369 | * 370 | * @return \Illuminate\Support\Fluent 371 | */ 372 | protected function createCommand($name, array $parameters = []) 373 | { 374 | return new Fluent(array_merge(compact('name'), $parameters)); 375 | } 376 | 377 | /** 378 | * Get the raw DSL statements for the blueprint. 379 | * 380 | * @param Grammar $grammar 381 | * 382 | * @return array 383 | */ 384 | public function toDSL(Grammar $grammar) 385 | { 386 | $statements = []; 387 | 388 | // Each type of command has a corresponding compiler function on the schema 389 | // grammar which is used to build the necessary DSL statements to build 390 | // the blueprint element, so we'll just call that compilers function. 391 | foreach ($this->commands as $command) { 392 | $method = 'compile'.ucfirst($command->name); 393 | 394 | if (method_exists($grammar, $method)) { 395 | if (!is_null($dsl = $grammar->$method($this, $command))) { 396 | $statements = array_merge($statements, (array) $dsl); 397 | } 398 | } 399 | } 400 | 401 | return $statements; 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/Map/Builder.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 40 | $this->grammar = $connection->getMapGrammar(); 41 | } 42 | 43 | /** 44 | * Create a map on your elasticsearch index. 45 | * 46 | * @param string $type 47 | * @param string $index 48 | * @param Closure $callback 49 | */ 50 | public function create($type, Closure $callback, $index = null) 51 | { 52 | if (!is_string($type)) { 53 | throw new InvalidArgumentException('type should be a string'); 54 | } 55 | 56 | if ($index and !is_string($index)) { 57 | throw new InvalidArgumentException('index should be a string'); 58 | } 59 | 60 | $blueprint = $this->createBlueprint($type, $closure = null, $index); 61 | 62 | $blueprint->create(); 63 | 64 | $callback($blueprint); 65 | 66 | $this->build($blueprint); 67 | } 68 | 69 | /** 70 | * Execute the blueprint to build. 71 | * 72 | * @param Blueprint $blueprint 73 | */ 74 | protected function build(Blueprint $blueprint) 75 | { 76 | $blueprint->build($this->connection, $this->grammar); 77 | } 78 | 79 | /** 80 | * Create a new command set with a Closure. 81 | * 82 | * @param string $type 83 | * @param Closure|null $callback 84 | * @param null $index 85 | * 86 | * @return mixed|Blueprint 87 | */ 88 | protected function createBlueprint($type, Closure $callback = null, $index = null) 89 | { 90 | if (isset($this->resolver)) { 91 | return call_user_func($this->resolver, $type, $callback, $index); 92 | } 93 | 94 | return new Blueprint($type, $callback, $index); 95 | } 96 | 97 | /** 98 | * Set the Schema Blueprint resolver callback. 99 | * 100 | * @param \Closure $resolver 101 | * 102 | * @return void 103 | */ 104 | public function blueprintResolver(Closure $resolver) 105 | { 106 | $this->resolver = $resolver; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Map/Grammar.php: -------------------------------------------------------------------------------- 1 | getFields(); 20 | 21 | $statement = $this->compileFields($fields); 22 | 23 | return $statement; 24 | } 25 | 26 | /** 27 | * Compile an integer map. 28 | * 29 | * @param Fluent $fluent 30 | * 31 | * @return array 32 | */ 33 | public function compileInteger(Fluent $fluent) 34 | { 35 | return $this->compileNumeric($fluent); 36 | } 37 | 38 | /** 39 | * Compile a long map. 40 | * 41 | * @param Fluent $fluent 42 | * 43 | * @return array 44 | */ 45 | public function compileLong(Fluent $fluent) 46 | { 47 | return $this->compileNumeric($fluent); 48 | } 49 | 50 | /** 51 | * Compile a short map. 52 | * 53 | * @param Fluent $fluent 54 | * 55 | * @return array 56 | */ 57 | public function compileShort(Fluent $fluent) 58 | { 59 | return $this->compileNumeric($fluent); 60 | } 61 | 62 | /** 63 | * Compile a byte map. 64 | * 65 | * @param Fluent $fluent 66 | * 67 | * @return array 68 | */ 69 | public function compileByte(Fluent $fluent) 70 | { 71 | return $this->compileNumeric($fluent); 72 | } 73 | 74 | /** 75 | * Compile a double map. 76 | * 77 | * @param Fluent $fluent 78 | * 79 | * @return array 80 | */ 81 | public function compileDouble(Fluent $fluent) 82 | { 83 | return $this->compileNumeric($fluent); 84 | } 85 | 86 | /** 87 | * Compile a binary map. 88 | * 89 | * @param Fluent $fluent 90 | * 91 | * @return array 92 | */ 93 | public function compileBinary(Fluent $fluent) 94 | { 95 | $map = [ 96 | 'type' => 'binary', 97 | 'doc_values' => $fluent->doc_values, 98 | 'store' => $fluent->store, 99 | ]; 100 | 101 | return $this->formatMap($map); 102 | } 103 | 104 | /** 105 | * Compile float map. 106 | * 107 | * @param Fluent $fluent 108 | * 109 | * @return array 110 | */ 111 | public function compileFloat(Fluent $fluent) 112 | { 113 | return $this->compileNumeric($fluent); 114 | } 115 | 116 | /** 117 | * Compile a date map. 118 | * 119 | * @param Fluent $fluent 120 | * 121 | * @return array 122 | */ 123 | public function compileDate(Fluent $fluent) 124 | { 125 | $map = [ 126 | 'type' => 'date', 127 | 'boost' => $fluent->boost, 128 | 'doc_values' => $fluent->doc_values, 129 | 'format' => $fluent->format, 130 | 'ignore_malformed' => $fluent->ignore_malformed, 131 | 'include_in_all' => $fluent->include_in_all, 132 | 'index' => $fluent->index, 133 | 'null_value' => $fluent->null_value, 134 | 'precision_step' => $fluent->precision_step, 135 | 'store' => $fluent->store, 136 | ]; 137 | 138 | return $this->formatMap($map); 139 | } 140 | 141 | /** 142 | * Compile a boolean map. 143 | * 144 | * @param Fluent $fluent 145 | * 146 | * @return array 147 | */ 148 | public function compileBoolean(Fluent $fluent) 149 | { 150 | $map = [ 151 | 'type' => 'boolean', 152 | 'boost' => $fluent->boost, 153 | 'doc_values' => $fluent->doc_values, 154 | 'index' => $fluent->index, 155 | 'null_value' => $fluent->null_value, 156 | 'store' => $fluent->store, 157 | ]; 158 | 159 | return $this->formatMap($map); 160 | } 161 | 162 | /** 163 | * Compile a geo point map. 164 | * 165 | * @param Fluent $fluent 166 | * 167 | * @return array 168 | */ 169 | public function compilePoint(Fluent $fluent) 170 | { 171 | $map = [ 172 | 'type' => 'geo_point', 173 | 'geohash' => $fluent->geohash, 174 | 'geohash_precision' => $fluent->geohash_precision, 175 | 'geohash_prefix' => $fluent->geohash_prefix, 176 | 'ignore_malformed' => $fluent->ignore_malformed, 177 | 'lat_lon' => $fluent->lat_lon, 178 | 'precision_step' => $fluent->precision_step, 179 | ]; 180 | 181 | return $this->formatMap($map); 182 | } 183 | 184 | /** 185 | * Compile a geo shape map. 186 | * 187 | * @param Fluent $fluent 188 | * 189 | * @return array 190 | */ 191 | public function compileShape(Fluent $fluent) 192 | { 193 | $map = [ 194 | 'type' => 'geo_shape', 195 | 'tree' => $fluent->tree, 196 | 'precision' => $fluent->precision, 197 | 'tree_levels' => $fluent->tree_levels, 198 | 'strategy' => $fluent->strategy, 199 | 'distance_error_pct' => $fluent->distance_error_pct, 200 | 'orientation' => $fluent->orientation, 201 | 'points_only' => $fluent->points_only, 202 | ]; 203 | 204 | return $this->formatMap($map); 205 | } 206 | 207 | /** 208 | * Compile an ip map. 209 | * 210 | * @param Fluent $fluent 211 | * 212 | * @return array 213 | */ 214 | public function compileIp(Fluent $fluent) 215 | { 216 | $map = [ 217 | 'type' => $fluent->type, 218 | 'boost' => $fluent->boost, 219 | 'doc_values' => $fluent->doc_values, 220 | 'include_in_all' => $fluent->include_in_all, 221 | 'index' => $fluent->index, 222 | 'null_value' => $fluent->null_value, 223 | 'precision_step' => $fluent->precision_step, 224 | 'store' => $fluent->store, 225 | ]; 226 | 227 | return $this->formatMap($map); 228 | } 229 | 230 | /** 231 | * Compile a completion map. 232 | * 233 | * @param Fluent $fluent 234 | * 235 | * @return array 236 | */ 237 | public function compileCompletion(Fluent $fluent) 238 | { 239 | $map = [ 240 | 'type' => 'completion', 241 | 'analyzer' => $fluent->analyzer, 242 | 'search_analyzer' => $fluent->search_analyzer, 243 | 'payloads' => $fluent->payloads, 244 | 'preserve_separators' => $fluent->preserve_separators, 245 | 'max_input_length' => $fluent->max_input_length, 246 | ]; 247 | 248 | return $this->formatMap($map); 249 | } 250 | 251 | /** 252 | * Compile a completion map. 253 | * 254 | * @param Fluent $fluent 255 | * 256 | * @return array 257 | */ 258 | public function compileToken_count(Fluent $fluent) 259 | { 260 | $map = [ 261 | 'type' => 'token_count', 262 | 'boost' => $fluent->boost, 263 | 'doc_values' => $fluent->doc_values, 264 | 'include_in_all' => $fluent->include_in_all, 265 | 'index' => $fluent->index, 266 | 'null_value' => $fluent->null_value, 267 | 'precision_step' => $fluent->precision_step, 268 | 'store' => $fluent->store, 269 | ]; 270 | 271 | return $this->formatMap($map); 272 | } 273 | 274 | /** 275 | * Compile a string map. 276 | * 277 | * @param Fluent $fluent 278 | * 279 | * @return array 280 | */ 281 | public function compileString(Fluent $fluent) 282 | { 283 | $map = [ 284 | 'type' => 'string', 285 | 'analyzer' => $fluent->analyzer, 286 | 'boost' => $fluent->boost, 287 | 'doc_values' => $fluent->doc_values, 288 | 'fielddata' => $fluent->fielddata, 289 | 'fields' => $fluent->fields, 290 | 'ignore_above' => $fluent->ignore_above, 291 | 'include_in_all' => $fluent->include_in_all, 292 | 'index' => $fluent->index, 293 | 'index_options' => $fluent->index_options, 294 | 'norms' => $fluent->norms, 295 | 'position_increment_gap' => $fluent->position_increment_gap, 296 | 'store' => $fluent->store, 297 | 'search_analyzer' => $fluent->search_analyzer, 298 | 'search_quote_analyzer' => $fluent->search_quote_analyzer, 299 | 'similarity' => $fluent->similarity, 300 | 'term_vector' => $fluent->term_vector, 301 | 'copy_to' => $fluent->copy_to, 302 | ]; 303 | 304 | return $this->formatMap($map); 305 | } 306 | 307 | /** 308 | * Compile a numeric map. 309 | * 310 | * @param Fluent $fluent 311 | * 312 | * @return array 313 | */ 314 | public function compileNumeric(Fluent $fluent) 315 | { 316 | $map = [ 317 | 'type' => $fluent->type, 318 | 'coerce' => $fluent->coerce, 319 | 'boost' => $fluent->boost, 320 | 'doc_values' => $fluent->doc_values, 321 | 'ignore_malformed' => $fluent->ignore_malformed, 322 | 'include_in_all' => $fluent->include_in_all, 323 | 'index' => $fluent->index, 324 | 'null_value' => $fluent->null_value, 325 | 'precision_step' => $fluent->precision_step, 326 | ]; 327 | 328 | return $this->formatMap($map); 329 | } 330 | 331 | /** 332 | * Compile a nested map. 333 | * 334 | * @param Fluent $fluent 335 | * 336 | * @return array 337 | */ 338 | public function compileNested(Fluent $fluent) 339 | { 340 | $blueprint = new Blueprint($fluent->type); 341 | 342 | /* @var \Closure $callback */ 343 | $callback = $fluent->callback; 344 | 345 | if (is_callable($callback)) { 346 | $callback($blueprint); 347 | } 348 | 349 | return [ 350 | 'type' => 'nested', 351 | 'properties' => $this->compileFields($blueprint->getFields()), 352 | ]; 353 | } 354 | 355 | /** 356 | * Compile a object map. 357 | * 358 | * @param Fluent $fluent 359 | * 360 | * @return array 361 | */ 362 | public function compileObject(Fluent $fluent) 363 | { 364 | $blueprint = new Blueprint($fluent->type); 365 | 366 | /* @var \Closure $callback */ 367 | $callback = $fluent->callback; 368 | 369 | if (is_callable($callback)) { 370 | $callback($blueprint); 371 | } 372 | 373 | return [ 374 | 'properties' => $this->compileFields($blueprint->getFields()), 375 | ]; 376 | } 377 | 378 | /** 379 | * Format the map array for submission. 380 | * 381 | * @param array $map 382 | * 383 | * @return array 384 | */ 385 | protected function formatMap(array $map) 386 | { 387 | return array_filter($map); 388 | } 389 | 390 | /** 391 | * Compile an array of fluent fields. 392 | * 393 | * @param $fields 394 | * 395 | * @return array 396 | */ 397 | public function compileFields($fields) 398 | { 399 | $statement = []; 400 | 401 | foreach ($fields as $field) { 402 | $method = 'compile'.ucfirst($field->type); 403 | 404 | if (method_exists($this, $method)) { 405 | if (!empty($map = $this->$method($field))) { 406 | $statement[$field->name] = $map; 407 | } 408 | } 409 | } 410 | 411 | return $statement; 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/MappingServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRepository(); 35 | 36 | $this->registerMapper(); 37 | 38 | $this->registerCreator(); 39 | 40 | $this->registerCommands(); 41 | 42 | $this->registerAlias(); 43 | } 44 | 45 | /** 46 | * Register the mapping repository service. 47 | */ 48 | protected function registerRepository() 49 | { 50 | $this->app->singleton('mapping.repository', function ($app) { 51 | $table = $app['config']['plastic.mappings']; 52 | 53 | return new Mappings($app['db'], $table); 54 | }); 55 | } 56 | 57 | /** 58 | * Register the mapping creator service. 59 | */ 60 | protected function registerCreator() 61 | { 62 | $this->app->singleton('mapping.creator', function ($app) { 63 | return new Creator($app['files']); 64 | }); 65 | } 66 | 67 | /** 68 | * Register the mapper service. 69 | */ 70 | protected function registerMapper() 71 | { 72 | $this->app->singleton('mapping.mapper', function ($app) { 73 | return new Mapper($app['mapping.repository'], $app['files']); 74 | }); 75 | } 76 | 77 | /** 78 | * Register all needed commands. 79 | */ 80 | protected function registerCommands() 81 | { 82 | $commands = ['Install', 'Reset', 'Make', 'Run', 'ReRun']; 83 | 84 | foreach ($commands as $command) { 85 | $this->{'register'.$command.'Command'}(); 86 | } 87 | 88 | $this->commands([ 89 | 'command.mapping.install', 90 | 'command.mapping.reset', 91 | 'command.mapping.make', 92 | 'command.mapping.run', 93 | 'command.mapping.rerun', 94 | ]); 95 | } 96 | 97 | /** 98 | * Register the Install command. 99 | */ 100 | protected function registerInstallCommand() 101 | { 102 | $this->app->singleton('command.mapping.install', function ($app) { 103 | return new Install($app['mapping.repository']); 104 | }); 105 | } 106 | 107 | /** 108 | * Register the Install command. 109 | */ 110 | protected function registerRunCommand() 111 | { 112 | $this->app->singleton('command.mapping.run', function ($app) { 113 | return new Run($app['mapping.mapper']); 114 | }); 115 | } 116 | 117 | /** 118 | * Register the Install command. 119 | */ 120 | protected function registerReRunCommand() 121 | { 122 | $this->app->singleton('command.mapping.rerun', function ($app) { 123 | return new ReRun(); 124 | }); 125 | } 126 | 127 | /** 128 | * Register the reset command. 129 | */ 130 | protected function registerResetCommand() 131 | { 132 | $this->app->singleton('command.mapping.reset', function ($app) { 133 | return new Reset($app['mapping.repository']); 134 | }); 135 | } 136 | 137 | /** 138 | * Register the make command. 139 | */ 140 | protected function registerMakeCommand() 141 | { 142 | $this->app->singleton('command.mapping.make', function ($app) { 143 | return new Make($app['mapping.creator'], $app['composer']); 144 | }); 145 | } 146 | 147 | /** 148 | * Register the Map alias. 149 | */ 150 | protected function registerAlias() 151 | { 152 | AliasLoader::getInstance()->alias('Map', Map::class); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Mappings/Creator.php: -------------------------------------------------------------------------------- 1 | files = $filesystem; 33 | } 34 | 35 | /** 36 | * Create a mapping file. 37 | * 38 | * @param $model 39 | * @param $path 40 | * 41 | * @return string 42 | */ 43 | public function create($model, $path) 44 | { 45 | $path = $this->getPath($model, $path); 46 | 47 | $stub = $this->getStub(); 48 | 49 | $this->files->put($path, $this->populateStub($model, $stub)); 50 | 51 | $this->firePostCreateHooks(); 52 | 53 | return $path; 54 | } 55 | 56 | /** 57 | * Get the path to the stubs. 58 | * 59 | * @return string 60 | */ 61 | public function getStubPath() 62 | { 63 | return __DIR__.'/stubs'; 64 | } 65 | 66 | /** 67 | * Get the mapping stub template. 68 | * 69 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 70 | * 71 | * @return string 72 | */ 73 | protected function getStub() 74 | { 75 | return $this->files->get($this->getStubPath().'/default.stub'); 76 | } 77 | 78 | /** 79 | * Populate the place-holders in the mapping stub. 80 | * 81 | * @param string $model 82 | * @param string $stub 83 | * 84 | * @return string 85 | */ 86 | protected function populateStub($model, $stub) 87 | { 88 | $stub = str_replace('DummyClass', $this->getClassName($model), $stub); 89 | 90 | $stub = str_replace('DummyModel', $model, $stub); 91 | 92 | return $stub; 93 | } 94 | 95 | /** 96 | * Get the class name of a migration name. 97 | * 98 | * @param $model 99 | * 100 | * @return string 101 | */ 102 | protected function getClassName($model) 103 | { 104 | return Str::studly(str_replace('\\', '', $model)); 105 | } 106 | 107 | /** 108 | * Fire the registered post create hooks. 109 | * 110 | * @return void 111 | */ 112 | protected function firePostCreateHooks() 113 | { 114 | foreach ($this->postCreate as $callback) { 115 | call_user_func($callback); 116 | } 117 | } 118 | 119 | /** 120 | * Register a post migration create hook. 121 | * 122 | * @param Closure $callback 123 | * 124 | * @return void 125 | */ 126 | public function afterCreate(Closure $callback) 127 | { 128 | $this->postCreate[] = $callback; 129 | } 130 | 131 | /** 132 | * Return the filesystem instance. 133 | * 134 | * @return Filesystem 135 | */ 136 | public function getFilesystem() 137 | { 138 | return $this->files; 139 | } 140 | 141 | /** 142 | * Get the full path name to the mapping. 143 | * 144 | * @param string $model 145 | * @param string $path 146 | * 147 | * @return string 148 | */ 149 | protected function getPath($model, $path) 150 | { 151 | $name = Str::lower(str_replace('\\', '_', $model)); 152 | 153 | return $path.'/'.$name.'.php'; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Mappings/Mapper.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 37 | $this->files = $files; 38 | } 39 | 40 | public function run($path, array $options = []) 41 | { 42 | $files = $this->getMappingFiles($path); 43 | 44 | $ran = $this->repository->getRan(); 45 | 46 | $mappings = array_diff($files, $ran); 47 | 48 | $this->requireFiles($path, $mappings); 49 | 50 | $this->runMappingList($mappings, $options); 51 | } 52 | 53 | /** 54 | * Run an array of mappings. 55 | * 56 | * @param array $mappings 57 | * @param array $options 58 | */ 59 | public function runMappingList(array $mappings, array $options = []) 60 | { 61 | // First we will just make sure that there are any migrations to run. If there 62 | // aren't, we will just make a note of it to the developer. 63 | if (count($mappings) == 0) { 64 | $this->note('Nothing to map'); 65 | 66 | return; 67 | } 68 | 69 | $batch = $this->repository->getNextBatchNumber(); 70 | 71 | $step = Arr::get($options, 'step', false); 72 | $index = Arr::get($options, 'index'); 73 | 74 | foreach ($mappings as $file) { 75 | $this->runMap($file, $batch, $index); 76 | 77 | if ($step) { 78 | $batch++; 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Run the given mapping file. 85 | * 86 | * @param $file 87 | * @param $batch 88 | * @param string|null $index The index on which to run the mappings instead of the default one. 89 | */ 90 | public function runMap($file, $batch, $index = null) 91 | { 92 | $mapping = $this->resolve($file); 93 | 94 | $mapping->setIndex($index); 95 | 96 | $mapping->map(); 97 | 98 | $this->repository->log($file, $batch); 99 | 100 | $this->note('Mapped: '.$file); 101 | } 102 | 103 | /** 104 | * Resolve mapping file from. 105 | * 106 | * @param $file 107 | * 108 | * @return Mapping 109 | */ 110 | public function resolve($file) 111 | { 112 | $class = Str::studly($file); 113 | 114 | return new $class(); 115 | } 116 | 117 | /** 118 | * Get all of the migration files in a given path. 119 | * 120 | * @param $path 121 | * 122 | * @return array 123 | */ 124 | public function getMappingFiles($path) 125 | { 126 | $files = $this->files->glob($path.'/*_*.php'); 127 | 128 | if ($files === false) { 129 | return []; 130 | } 131 | 132 | $files = array_map(function ($file) { 133 | return str_replace('.php', '', basename($file)); 134 | }, $files); 135 | 136 | return $files; 137 | } 138 | 139 | /** 140 | * Require All migration files in a given path. 141 | * 142 | * @param $path 143 | * @param array $files 144 | */ 145 | public function requireFiles($path, array $files) 146 | { 147 | foreach ($files as $file) { 148 | $this->files->requireOnce($path.'/'.$file.'.php'); 149 | } 150 | } 151 | 152 | /** 153 | * Check if the mappings repository exists. 154 | * 155 | * @return mixed 156 | */ 157 | public function repositoryExists() 158 | { 159 | return $this->repository->exits(); 160 | } 161 | 162 | /** 163 | * Set the default connection name. 164 | * 165 | * @param string $name 166 | * 167 | * @return void 168 | */ 169 | public function setConnection($name) 170 | { 171 | $this->repository->setSource($name); 172 | } 173 | 174 | /** 175 | * Return the registered notes. 176 | * 177 | * @return array 178 | */ 179 | public function getNotes() 180 | { 181 | return $this->notes; 182 | } 183 | 184 | /** 185 | * Return a filesystem instance. 186 | * 187 | * @return Filesystem 188 | */ 189 | public function getRepository() 190 | { 191 | return $this->repository; 192 | } 193 | 194 | /** 195 | * Return a filesystem instance. 196 | * 197 | * @return Filesystem 198 | */ 199 | public function getFilesystem() 200 | { 201 | return $this->files; 202 | } 203 | 204 | /** 205 | * Add a message to the note array. 206 | * 207 | * @param $message 208 | */ 209 | protected function note($message) 210 | { 211 | $this->notes[] = $message; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Mappings/Mapping.php: -------------------------------------------------------------------------------- 1 | prepareModel(); 32 | } 33 | 34 | /** 35 | * Gets the index name. 36 | * 37 | * @return string|null 38 | */ 39 | public function index() 40 | { 41 | return $this->index; 42 | } 43 | 44 | /** 45 | * Sets the index name. 46 | * 47 | * @param string|null $index 48 | */ 49 | public function setIndex($index) 50 | { 51 | $this->index = $index; 52 | } 53 | 54 | /** 55 | * Validate the given model and create a new instance. 56 | */ 57 | protected function prepareModel() 58 | { 59 | if (!$this->model) { 60 | throw new MissingArgumentException('model property should be filled'); 61 | } 62 | 63 | $this->model = new $this->model(); 64 | 65 | $traits = class_uses_recursive(get_class($this->model)); 66 | 67 | if (!isset($traits[Searchable::class])) { 68 | throw new InvalidArgumentException(get_class($this->model).' does not use the searchable trait'); 69 | } 70 | } 71 | 72 | /** 73 | * Get the model elastic type. 74 | * 75 | * @return mixed 76 | */ 77 | public function getModelType() 78 | { 79 | return $this->model->getDocumentType(); 80 | } 81 | 82 | /** 83 | * Get the model elastic index. 84 | * 85 | * @return mixed 86 | */ 87 | public function getModelIndex() 88 | { 89 | return $this->index() ?: $this->model->getDocumentIndex(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Mappings/Mappings.php: -------------------------------------------------------------------------------- 1 | table = $table; 42 | $this->resolver = $resolver; 43 | } 44 | 45 | /** 46 | * Get the ran mappings. 47 | * 48 | * @return array 49 | */ 50 | public function getRan() 51 | { 52 | $result = $this->table() 53 | ->orderBy('batch', 'asc') 54 | ->orderBy('mapping', 'asc') 55 | ->pluck('mapping'); 56 | 57 | if (is_array($result)) { 58 | return $result; 59 | } 60 | 61 | return $result->toArray(); 62 | } 63 | 64 | /** 65 | * Get the last mapping batch. 66 | * 67 | * @return array 68 | */ 69 | public function getLast() 70 | { 71 | $result = $this->table() 72 | ->where('batch', $this->getLastBatchNumber()) 73 | ->orderBy('mapping', 'desc') 74 | ->get(); 75 | 76 | if (is_array($result)) { 77 | return $result; 78 | } 79 | 80 | return $result->toArray(); 81 | } 82 | 83 | /** 84 | * Log that a mapping was run. 85 | * 86 | * @param $file 87 | * @param $batch 88 | */ 89 | public function log($file, $batch) 90 | { 91 | $record = ['mapping' => $file, 'batch' => $batch]; 92 | 93 | $this->table()->insert($record); 94 | } 95 | 96 | /** 97 | * Remove mapping from the log. 98 | * 99 | * @param object $mapping 100 | */ 101 | public function delete($mapping) 102 | { 103 | $this->table()->where('mapping', $mapping->mapping)->delete(); 104 | } 105 | 106 | /** 107 | * Remove all mapping logs from the repository. 108 | */ 109 | public function reset() 110 | { 111 | $this->table()->truncate(); 112 | } 113 | 114 | /* 115 | * Get the next mapping batch number 116 | * 117 | * @return float|int 118 | */ 119 | public function getNextBatchNumber() 120 | { 121 | return $this->getLastBatchNumber() + 1; 122 | } 123 | 124 | /** 125 | * Get the next mapping batch number. 126 | * 127 | * @return float|int 128 | */ 129 | public function getLastBatchNumber() 130 | { 131 | return $this->table()->max('batch'); 132 | } 133 | 134 | /** 135 | * Create the mapping repository data store. 136 | */ 137 | public function createRepository() 138 | { 139 | $this->schema()->create($this->table, function ($table) { 140 | 141 | // The mappings table is responsible for keeping track of which of the 142 | // mappings have actually run for the application. We'll create the 143 | // table to hold the mapping file's path as well as the batch ID. 144 | $table->increments('id'); 145 | 146 | $table->string('mapping'); 147 | 148 | $table->integer('batch'); 149 | }); 150 | } 151 | 152 | /** 153 | * Check it the repository table exits. 154 | * 155 | * @return mixed 156 | */ 157 | public function exits() 158 | { 159 | return $this->schema()->hasTable($this->table); 160 | } 161 | 162 | /** 163 | * get a schema builder instance. 164 | * 165 | * @return \Illuminate\Database\Schema\Builder 166 | */ 167 | protected function schema() 168 | { 169 | return $this->getConnection()->getSchemaBuilder(); 170 | } 171 | 172 | /** 173 | * Get a query builder for the mapping table. 174 | * 175 | * @return \Illuminate\Database\Query\Builder 176 | */ 177 | protected function table() 178 | { 179 | return $this->getConnection()->table($this->table); 180 | } 181 | 182 | /** 183 | * Resolve the database connection instance. 184 | * 185 | * @return \Illuminate\Database\ConnectionInterface 186 | */ 187 | public function getConnection() 188 | { 189 | return $this->resolver->connection($this->connection); 190 | } 191 | 192 | /** 193 | * Get the connection resolver instance. 194 | * 195 | * @return \Illuminate\Database\ConnectionResolverInterface 196 | */ 197 | public function getConnectionResolver() 198 | { 199 | return $this->resolver; 200 | } 201 | 202 | /** 203 | * Set the information source to gather data. 204 | * 205 | * @param string $name 206 | * 207 | * @return void 208 | */ 209 | public function setSource($name) 210 | { 211 | $this->connection = $name; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Mappings/stubs/default.stub: -------------------------------------------------------------------------------- 1 | getModelType(),function(Blueprint $map){ 23 | // 24 | },$this->getModelIndex()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Persistence/EloquentPersistence.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 32 | } 33 | 34 | /** 35 | * Get the model to persist. 36 | * 37 | * @return Model 38 | */ 39 | public function getModel() 40 | { 41 | return $this->model; 42 | } 43 | 44 | /** 45 | * Set the model to persist. 46 | * 47 | * @param Model $model 48 | * 49 | * @throws InvalidArgumentException 50 | * 51 | * @return $this 52 | */ 53 | public function model(Model $model) 54 | { 55 | // Check if the model is searchable before setting the query builder model 56 | $traits = class_uses_recursive(get_class($model)); 57 | 58 | if (!isset($traits[Searchable::class])) { 59 | throw new InvalidArgumentException(get_class($model).' does not use the searchable trait'); 60 | } 61 | 62 | $this->model = $model; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Save a model instance. 69 | * 70 | * @throws \Exception 71 | * 72 | * @return mixed 73 | */ 74 | public function save() 75 | { 76 | $this->exitIfModelNotSet(); 77 | 78 | if (!$this->model->exists) { 79 | throw new \Exception('Model not persisted yet'); 80 | } 81 | $document = $this->model->getDocumentData(); 82 | 83 | $params = [ 84 | 'id' => $this->model->getKey(), 85 | 'type' => $this->model->getDocumentType(), 86 | 'index' => $this->model->getDocumentIndex(), 87 | 'body' => $document, 88 | ]; 89 | 90 | return $this->connection->indexStatement($params); 91 | } 92 | 93 | /** 94 | * Update a model document. 95 | * 96 | * @throws \Exception 97 | * 98 | * @return mixed 99 | */ 100 | public function update() 101 | { 102 | $this->exitIfModelNotSet(); 103 | 104 | if (!$this->model->exists) { 105 | throw new \Exception('Model not persisted yet'); 106 | } 107 | 108 | $document = $this->model->getDocumentData(); 109 | 110 | $params = [ 111 | 'id' => $this->model->getKey(), 112 | 'type' => $this->model->getDocumentType(), 113 | 'index' => $this->model->getDocumentIndex(), 114 | 'body' => [ 115 | 'doc' => $document, 116 | ], 117 | ]; 118 | 119 | return $this->connection->updateStatement($params); 120 | } 121 | 122 | /** 123 | * Delete a model document. 124 | * 125 | * @return mixed 126 | */ 127 | public function delete() 128 | { 129 | $this->exitIfModelNotSet(); 130 | 131 | $params = [ 132 | 'id' => $this->model->getKey(), 133 | 'type' => $this->model->getDocumentType(), 134 | 'index' => $this->model->getDocumentIndex(), 135 | ]; 136 | 137 | // check if the document exists before deleting 138 | if ($this->connection->existsStatement($params)) { 139 | return $this->connection->deleteStatement($params); 140 | } 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * Bulk save a collection Models. 147 | * 148 | * @param array|Collection $collection 149 | * 150 | * @return mixed 151 | */ 152 | public function bulkSave($collection = []) 153 | { 154 | $params = []; 155 | 156 | $defaultIndex = $this->connection->getDefaultIndex(); 157 | 158 | foreach ($collection as $item) { 159 | $modelIndex = $item->getDocumentIndex(); 160 | 161 | $params['body'][] = [ 162 | 'index' => [ 163 | '_id' => $item->getKey(), 164 | '_type' => $item->getDocumentType(), 165 | '_index' => $modelIndex ? $modelIndex : $defaultIndex, 166 | ], 167 | ]; 168 | $params['body'][] = $item->getDocumentData(); 169 | } 170 | 171 | return $this->connection->bulkStatement($params); 172 | } 173 | 174 | /** 175 | * Bulk Delete a collection of Models. 176 | * 177 | * @param array|collection $collection 178 | * 179 | * @return mixed 180 | */ 181 | public function bulkDelete($collection = []) 182 | { 183 | $params = []; 184 | 185 | $defaultIndex = $this->connection->getDefaultIndex(); 186 | 187 | foreach ($collection as $item) { 188 | $modelIndex = $item->getDocumentIndex(); 189 | 190 | $params['body'][] = [ 191 | 'delete' => [ 192 | '_id' => $item->getKey(), 193 | '_type' => $item->getDocumentType(), 194 | '_index' => $modelIndex ? $modelIndex : $defaultIndex, 195 | ], 196 | ]; 197 | } 198 | 199 | return $this->connection->bulkStatement($params); 200 | } 201 | 202 | /** 203 | * Reindex a collection of Models. 204 | * 205 | * @param array|Collection $collection 206 | * 207 | * @return mixed 208 | */ 209 | public function reindex($collection = []) 210 | { 211 | $this->bulkDelete($collection); 212 | 213 | return $this->bulkSave($collection); 214 | } 215 | 216 | /** 217 | * Function called when the model value is a required. 218 | */ 219 | private function exitIfModelNotSet() 220 | { 221 | if (!$this->model) { 222 | throw new MissingArgumentException('you should set the model first'); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/PlasticManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 27 | } 28 | 29 | /** 30 | * Get an elastic search connection instance. 31 | */ 32 | public function connection() 33 | { 34 | if (!$this->connection) { 35 | $config = $this->app['config']['plastic']; 36 | 37 | $this->connection = new Connection($config); 38 | } 39 | 40 | return $this->connection; 41 | } 42 | 43 | /** 44 | * Dynamically pass methods to the default connection. 45 | * 46 | * @param string $method 47 | * @param array $parameters 48 | * 49 | * @return mixed 50 | */ 51 | public function __call($method, $parameters) 52 | { 53 | return call_user_func_array([$this->connection(), $method], $parameters); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/PlasticPaginator.php: -------------------------------------------------------------------------------- 1 | result = $result; 24 | 25 | parent::__construct( 26 | $result->hits(), 27 | $result->totalHits(), 28 | $limit, 29 | $page, 30 | ['path' => LengthAwarePaginator::resolveCurrentPath()] 31 | ); 32 | 33 | $hitsReference = &$this->items; 34 | 35 | $result->setHits($hitsReference); 36 | } 37 | 38 | /** 39 | * Access the plastic result object. 40 | * 41 | * @return PlasticResult 42 | */ 43 | public function result() 44 | { 45 | return $this->result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/PlasticResult.php: -------------------------------------------------------------------------------- 1 | took = $results['took']; 64 | 65 | $this->timed_out = $results['timed_out']; 66 | 67 | $this->shards = $results['_shards']; 68 | 69 | $this->hits = new Collection($results['hits']['hits']); 70 | 71 | $this->totalHits = $results['hits']['total']; 72 | 73 | $this->maxScore = $results['hits']['max_score']; 74 | 75 | $this->aggregations = isset($results['aggregations']) ? $results['aggregations'] : []; 76 | } 77 | 78 | /** 79 | * Total Hits. 80 | * 81 | * @return int 82 | */ 83 | public function totalHits() 84 | { 85 | return $this->totalHits; 86 | } 87 | 88 | /** 89 | * Max Score. 90 | * 91 | * @return float 92 | */ 93 | public function maxScore() 94 | { 95 | return $this->maxScore; 96 | } 97 | 98 | /** 99 | * Get Shards. 100 | * 101 | * @return array 102 | */ 103 | public function shards() 104 | { 105 | return $this->shards; 106 | } 107 | 108 | /** 109 | * Took. 110 | * 111 | * @return string 112 | */ 113 | public function took() 114 | { 115 | return $this->took; 116 | } 117 | 118 | /** 119 | * Timed Out. 120 | * 121 | * @return bool 122 | */ 123 | public function timedOut() 124 | { 125 | return (bool) $this->timed_out; 126 | } 127 | 128 | /** 129 | * Get Hits. 130 | * 131 | * Get the hits from Elasticsearch 132 | * results as a Collection. 133 | * 134 | * @return Collection 135 | */ 136 | public function hits() 137 | { 138 | return $this->hits; 139 | } 140 | 141 | /** 142 | * Set the hits value. 143 | * 144 | * @param $values 145 | */ 146 | public function setHits($values) 147 | { 148 | $this->hits = $values; 149 | } 150 | 151 | /** 152 | * Get aggregations. 153 | * 154 | * Get the raw hits array from 155 | * Elasticsearch results. 156 | * 157 | * @return array 158 | */ 159 | public function aggregations() 160 | { 161 | return $this->aggregations; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/PlasticServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 23 | __DIR__.'/Resources/config.php' => config_path('plastic.php'), 24 | ]); 25 | 26 | // Create the mapping folder 27 | $this->publishes([ 28 | __DIR__.'/Resources/database' => database_path(), 29 | ], 'database'); 30 | } 31 | 32 | /** 33 | * Register the service provider. 34 | * 35 | * @return void 36 | */ 37 | public function register() 38 | { 39 | $this->registerManager(); 40 | 41 | $this->registerProviders(); 42 | 43 | $this->registerAlias(); 44 | } 45 | 46 | /** 47 | * Register plastic's Manager and connection. 48 | */ 49 | protected function registerManager() 50 | { 51 | $this->app->singleton('plastic', function ($app) { 52 | return new PlasticManager($app); 53 | }); 54 | 55 | $this->app->singleton('plastic.connection', function ($app) { 56 | return $app['plastic']->connection(); 57 | }); 58 | } 59 | 60 | /** 61 | * Register the service providers. 62 | */ 63 | protected function registerProviders() 64 | { 65 | // Register the index service provider. 66 | $this->app->register(IndexServiceProvider::class); 67 | 68 | // Register the mappings service provider. 69 | $this->app->register(MappingServiceProvider::class); 70 | } 71 | 72 | /** 73 | * Register the Plastic alias. 74 | */ 75 | protected function registerAlias() 76 | { 77 | AliasLoader::getInstance()->alias('Plastic', Plastic::class); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Resources/config.php: -------------------------------------------------------------------------------- 1 | env('PLASTIC_INDEX', 'plastic'), 14 | 15 | /* 16 | * Connection settings 17 | */ 18 | 'connection' => [ 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Hosts 23 | |-------------------------------------------------------------------------- 24 | | 25 | | The most common configuration is telling the client about your cluster: how many nodes, their addresses and ports. 26 | | If no hosts are specified, the client will attempt to connect to localhost:9200. 27 | | 28 | */ 29 | 'hosts' => [ 30 | env('PLASTIC_HOST', '127.0.0.1:9200'), 31 | ], 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Reties 36 | |-------------------------------------------------------------------------- 37 | | 38 | | By default, the client will retry n times, where n = number of nodes in your cluster. 39 | | A retry is only performed if the operation results in a "hard" exception. 40 | | 41 | */ 42 | 'retries' => env('PLASTIC_RETRIES', 3), 43 | 44 | /* 45 | |------------------------------------------------------------------ 46 | | Logging 47 | |------------------------------------------------------------------ 48 | | 49 | | Logging is disabled by default for performance reasons. The recommended logger is Monolog (used by Laravel), 50 | | but any logger that implements the PSR/Log interface will work. 51 | | 52 | | @more https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#enabling_logger 53 | | 54 | */ 55 | 'logging' => [ 56 | 'enabled' => env('PLASTIC_LOG', false), 57 | 'path' => storage_path(env('PLASTIC_LOG_PATH', 'logs/plastic.log')), 58 | 'level' => env('PLASTIC_LOG_LEVEL', 200), 59 | ], 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Mapping repository table 65 | |-------------------------------------------------------------------------- 66 | | 67 | | The sql table to store the mappings logs 68 | | 69 | */ 70 | 'mappings' => env('PLASTIC_MAPPINGS', 'mappings'), 71 | 72 | /* 73 | |------------------------------------------------------------------ 74 | | Populate settings 75 | |------------------------------------------------------------------ 76 | | 77 | | The settings for populating an index. 78 | | 79 | */ 80 | 'populate' => [ 81 | 82 | /* 83 | |------------------------------------------------------------------ 84 | | Models 85 | |------------------------------------------------------------------ 86 | | 87 | | The list of models, per index, from which to recreate the documents when running the console command to populate an index. 88 | | 89 | */ 90 | 'models' => [ 91 | 92 | // The models for the default index 93 | env('PLASTIC_INDEX', 'plastic') => [], 94 | ], 95 | 96 | /* 97 | |------------------------------------------------------------------ 98 | | Chunk size 99 | |------------------------------------------------------------------ 100 | | 101 | | The size of documents chunks to index per model 102 | | 103 | */ 104 | 'chunk_size' => 1000, 105 | ], 106 | 107 | ]; 108 | -------------------------------------------------------------------------------- /src/Resources/database/mappings/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleimanx2/plastic/ef40b41a8ae8c7ef58ffaeaa730868e765a1b1f6/src/Resources/database/mappings/.gitkeep -------------------------------------------------------------------------------- /src/Searchable.php: -------------------------------------------------------------------------------- 1 | shouldSyncDocument()) { 51 | $model->document()->save(); 52 | } 53 | }); 54 | 55 | static::deleted(function ($model) { 56 | if ($model->shouldSyncDocument()) { 57 | $model->document()->delete(); 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Start an elastic persistence query builder. 64 | * 65 | * @return EloquentPersistence 66 | */ 67 | public function document() 68 | { 69 | return Plastic::persist()->model($this); 70 | } 71 | 72 | /** 73 | * Get the model elastic type. 74 | * 75 | * @return string 76 | */ 77 | public function getDocumentType() 78 | { 79 | // if the type is defined use it else return the table name 80 | if (isset($this->documentType) and !empty($this->documentType)) { 81 | return $this->documentType; 82 | } 83 | 84 | return $this->getTable(); 85 | } 86 | 87 | /** 88 | * Get the model elastic index if available. 89 | * 90 | * @return mixed 91 | */ 92 | public function getDocumentIndex() 93 | { 94 | // if a custom index is defined use it else return null 95 | if (isset($this->documentIndex) and !empty($this->documentIndex)) { 96 | return $this->documentIndex; 97 | } 98 | } 99 | 100 | /** 101 | * Build the document data with the appropriate method. 102 | * 103 | * @return array 104 | */ 105 | public function getDocumentData() 106 | { 107 | // If the model contain a buildDocument function 108 | // use it to build the document 109 | if (method_exists($this, 'buildDocument')) { 110 | $document = $this->buildDocument(); 111 | 112 | return $document; 113 | } 114 | // If a searchable array is provided build 115 | // the document from the given array 116 | elseif (is_array($this->searchable)) { 117 | $document = $this->buildDocumentFromArray($this->searchable); 118 | 119 | return $document; 120 | } else { 121 | $document = $this->toArray(); 122 | 123 | return $document; 124 | } 125 | } 126 | 127 | /** 128 | * Build the document from a searchable array. 129 | * 130 | * @param array $searchable 131 | * 132 | * @return array 133 | */ 134 | protected function buildDocumentFromArray(array $searchable) 135 | { 136 | $document = []; 137 | 138 | foreach ($searchable as $value) { 139 | $result = $this->$value; 140 | 141 | if ($result instanceof Collection) { 142 | $result = $result->toArray(); 143 | } elseif ($result instanceof Carbon) { 144 | $result = $result->toDateTimeString(); 145 | } else { 146 | $result = $this->$value; 147 | } 148 | 149 | $document[$value] = $result; 150 | } 151 | 152 | return $document; 153 | } 154 | 155 | /** 156 | * Checks if the model content should be auto synced with elastic. 157 | * 158 | * @return boolean; 159 | */ 160 | public function shouldSyncDocument() 161 | { 162 | if (property_exists($this, 'syncDocument')) { 163 | return $this->syncDocument; 164 | } 165 | 166 | return true; 167 | } 168 | 169 | /** 170 | * Handle dynamic method calls into the model. 171 | * 172 | * @param string $method 173 | * @param array $parameters 174 | * 175 | * @return mixed 176 | */ 177 | public function __call($method, $parameters) 178 | { 179 | if ($method == 'search') { 180 | //Start an elastic dsl search query builder 181 | return Plastic::search()->model($this); 182 | } 183 | 184 | if ($method == 'suggest') { 185 | //Start an elastic dsl suggest query builder 186 | return Plastic::suggest()->index($this->getDocumentIndex()); 187 | } 188 | 189 | return parent::__call($method, $parameters); 190 | } 191 | } 192 | --------------------------------------------------------------------------------