├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── ElasticsearchEngine.php └── ElasticsearchProvider.php └── tests └── ElasticsearchEngineTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.env 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | Thumbs.db 7 | /phpunit.xml 8 | /build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.0 4 | - 5.6 5 | - 5.5 6 | - hhvm 7 | 8 | before_script: 9 | - travis_retry composer self-update 10 | - travis_retry composer install --prefer-source --no-interaction --dev 11 | 12 | script: vendor/phpunit/phpunit/phpunit --verbose -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Erick Tamayo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Scout Elasticsearch Driver 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 4 | 5 | This package makes is the [Elasticsearch](https://www.elastic.co/products/elasticsearch) driver for Laravel Scout. 6 | 7 | ## Contents 8 | 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Credits](#credits) 12 | - [License](#license) 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ``` bash 19 | composer require tamayo/laravel-scout-elastic 20 | ``` 21 | 22 | You must install the service provider: 23 | 24 | ```php 25 | // config/app.php 26 | 'providers' => [ 27 | ... 28 | ScoutEngines\Elasticsearch\ElasticsearchProvider::class, 29 | ], 30 | ``` 31 | 32 | ### Setting up Elasticsearch configuration 33 | You must have a Elasticsearch server up and running with the index you want to use created 34 | 35 | If you need help with this please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) 36 | 37 | After you've published the Laravel Scout package configuration: 38 | 39 | ```php 40 | // config/scout.php 41 | // Set your driver to elasticsearch 42 | 'driver' => env('SCOUT_DRIVER', 'elasticsearch'), 43 | 44 | ... 45 | 'elasticsearch' => [ 46 | 'index' => env('ELASTICSEARCH_INDEX', 'laravel'), 47 | 'hosts' => [ 48 | env('ELASTICSEARCH_HOST', 'http://localhost'), 49 | ], 50 | ], 51 | ... 52 | ``` 53 | 54 | ## Usage 55 | 56 | Now you can use Laravel Scout as described [here](https://laravel-news.com/2016/08/laravel-scout-is-now-open-for-developer-testing/) 57 | ## Credits 58 | 59 | - [Erick Tamayo](https://github.com/ericktamayo) 60 | - [All Contributors](../../contributors) 61 | 62 | ## License 63 | 64 | The MIT License (MIT). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jeylabs/laravel-scout-elastic", 3 | "description": "Elastic Driver for Laravel Scout", 4 | "keywords": ["laravel", "scout", "elasticsearch", "elastic"], 5 | "require": { 6 | "php": ">=5.6.4", 7 | "laravel/scout": "^1.0", 8 | "illuminate/support": "^5.3", 9 | "illuminate/database": "^5.3", 10 | "elasticsearch/elasticsearch": "^2.2" 11 | }, 12 | "require-dev": { 13 | "fzaninotto/faker": "~1.4", 14 | "mockery/mockery": "0.9.*", 15 | "phpunit/phpunit": "~5.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "ScoutEngines\\Elasticsearch\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "ScoutEngines\\Elasticsearch\\Test\\": "tests" 25 | } 26 | }, 27 | "minimum-stability": "stable", 28 | "prefer-stable": true 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ElasticsearchEngine.php: -------------------------------------------------------------------------------- 1 | elastic = $elastic; 29 | $this->index = $index; 30 | } 31 | 32 | /** 33 | * Update the given model in the index. 34 | * 35 | * @param Collection $models 36 | * @return void 37 | */ 38 | public function update($models) 39 | { 40 | $params['body'] = $models->map(function ($model) { 41 | return [ 42 | [ 43 | 'update' => [ 44 | '_id' => $model->getKey(), 45 | '_index' => $this->index, 46 | '_type' => $model->searchableAs(), 47 | ] 48 | ], 49 | [ 50 | 'doc' => $model->toSearchableArray(), 51 | 'doc_as_upsert' => true 52 | ] 53 | ]; 54 | })->flatten(1) 55 | ->toArray(); 56 | 57 | $this->elastic->bulk($params); 58 | } 59 | 60 | /** 61 | * Remove the given model from the index. 62 | * 63 | * @param Collection $models 64 | * @return void 65 | */ 66 | public function delete($models) 67 | { 68 | $params['body'] = $models->map(function ($model) { 69 | return [ 70 | 'delete' => [ 71 | '_id' => $model->getKey(), 72 | '_index' => $this->index, 73 | '_type' => $model->searchableAs(), 74 | ] 75 | ]; 76 | })->toArray(); 77 | 78 | $this->elastic->bulk($params); 79 | } 80 | 81 | /** 82 | * Perform the given search on the engine. 83 | * 84 | * @param Builder $builder 85 | * @return mixed 86 | */ 87 | public function search(Builder $builder) 88 | { 89 | return $this->performSearch($builder, array_filter([ 90 | 'numericFilters' => $this->filters($builder), 91 | 'size' => $builder->limit, 92 | ])); 93 | } 94 | 95 | /** 96 | * Perform the given search on the engine. 97 | * 98 | * @param Builder $builder 99 | * @param int $perPage 100 | * @param int $page 101 | * @return mixed 102 | */ 103 | public function paginate(Builder $builder, $perPage, $page) 104 | { 105 | $result = $this->performSearch($builder, [ 106 | 'numericFilters' => $this->filters($builder), 107 | 'from' => (($page * $perPage) - $perPage), 108 | 'size' => $perPage, 109 | ]); 110 | 111 | $result['nbPages'] = $result['hits']['total']/$perPage; 112 | 113 | return $result; 114 | } 115 | 116 | /** 117 | * Perform the given search on the engine. 118 | * 119 | * @param Builder $builder 120 | * @param array $options 121 | * @return mixed 122 | */ 123 | protected function performSearch(Builder $builder, array $options = []) 124 | { 125 | $params = [ 126 | 'index' => $this->index, 127 | 'type' => $builder->model->searchableAs(), 128 | 'body' => [ 129 | 'query' => [ 130 | 'bool' => [ 131 | 'must' => [['query_string' => [ 'query' => "*{$builder->query}*"]]] 132 | ] 133 | ] 134 | ] 135 | ]; 136 | 137 | if (isset($options['from'])) { 138 | $params['body']['from'] = $options['from']; 139 | } 140 | 141 | if (isset($options['size'])) { 142 | $params['body']['size'] = $options['size']; 143 | } 144 | 145 | if (count($options['numericFilters'])) { 146 | $params['body']['query']['bool']['must'] = array_merge($params['body']['query']['bool']['must'], 147 | $options['numericFilters']); 148 | } 149 | 150 | return $this->elastic->search($params); 151 | } 152 | 153 | /** 154 | * Get the filter array for the query. 155 | * 156 | * @param Builder $builder 157 | * @return array 158 | */ 159 | protected function filters(Builder $builder) 160 | { 161 | return collect($builder->wheres)->map(function ($value, $key) { 162 | return ['match_phrase' => [$key => $value]]; 163 | })->values()->all(); 164 | } 165 | 166 | /** 167 | * Map the given results to instances of the given model. 168 | * 169 | * @param mixed $results 170 | * @param \Illuminate\Database\Eloquent\Model $model 171 | * @return Collection 172 | */ 173 | public function map($results, $model) 174 | { 175 | if (count($results['hits']['total']) === 0) { 176 | return Collection::make(); 177 | } 178 | 179 | $keys = collect($results['hits']['hits']) 180 | ->pluck('_id')->values()->all(); 181 | 182 | $models = $model->whereIn( 183 | $model->getKeyName(), $keys 184 | )->get()->keyBy($model->getKeyName()); 185 | 186 | return collect($results['hits']['hits'])->map(function ($hit) use ($model, $models) { 187 | return $models[$hit['_id']]; 188 | }); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/ElasticsearchProvider.php: -------------------------------------------------------------------------------- 1 | app[EngineManager::class]->extend('elasticsearch', function($app){ 17 | return new ElasticsearchEngine(ElasticBuilder::create() 18 | ->setHosts(config('scout.elastic.hosts')) 19 | ->build(), 20 | config('scout.elasticsearch.index') 21 | ); 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /tests/ElasticsearchEngineTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('bulk')->with([ 17 | 'body' => [ 18 | [ 19 | 'update' => [ 20 | '_id' => 1, 21 | '_index' => 'scout', 22 | '_type' => 'table', 23 | ] 24 | ], 25 | [ 26 | 'doc' => ['id' => 1 ], 27 | 'doc_as_upsert' => true 28 | ] 29 | ] 30 | ]); 31 | 32 | $engine = new ElasticsearchEngine($client, 'scout'); 33 | $engine->update(Collection::make([new ElasticsearchEngineTestModel])); 34 | } 35 | 36 | public function test_delete_removes_objects_to_index() 37 | { 38 | $client = Mockery::mock('Elasticsearch\Client'); 39 | $client->shouldReceive('bulk')->with([ 40 | 'body' => [ 41 | [ 42 | 'delete' => [ 43 | '_id' => 1, 44 | '_index' => 'scout', 45 | '_type' => 'table', 46 | ] 47 | ], 48 | ] 49 | ]); 50 | 51 | $engine = new ElasticsearchEngine($client, 'scout'); 52 | $engine->delete(Collection::make([new ElasticsearchEngineTestModel])); 53 | } 54 | 55 | public function test_search_sends_correct_parameters_to_elasticsearch() 56 | { 57 | $client = Mockery::mock('Elasticsearch\Client'); 58 | $client->shouldReceive('search')->with([ 59 | 'index' => 'scout', 60 | 'type' => 'table', 61 | 'body' => [ 62 | 'query' => [ 63 | 'bool' => [ 64 | 'must' => [ 65 | ['query_string' => ['query' => '*zonda*']], 66 | ['match_phrase' => ['foo' => 1]] 67 | ] 68 | ] 69 | ] 70 | ] 71 | ]); 72 | 73 | $engine = new ElasticsearchEngine($client, 'scout'); 74 | $builder = new Laravel\Scout\Builder(new ElasticsearchEngineTestModel, 'zonda'); 75 | $builder->where('foo', 1); 76 | $engine->search($builder); 77 | } 78 | 79 | public function test_map_correctly_maps_results_to_models() 80 | { 81 | $client = Mockery::mock('Elasticsearch\Client'); 82 | $engine = new ElasticsearchEngine($client, 'scout'); 83 | 84 | $model = Mockery::mock('Illuminate\Database\Eloquent\Model'); 85 | $model->shouldReceive('getKeyName')->andReturn('key'); 86 | $model->shouldReceive('whereIn')->once()->with('key', ['1'])->andReturn($model); 87 | $model->shouldReceive('get')->once()->andReturn(Collection::make([new ElasticsearchEngineTestModel])); 88 | 89 | $results = $engine->map([ 90 | 'hits' => [ 91 | 'total' => '1', 92 | 'hits' => [ 93 | [ 94 | '_id' => '1' 95 | ] 96 | ] 97 | ] 98 | ], $model); 99 | 100 | $this->assertEquals(1, count($results)); 101 | } 102 | } 103 | 104 | class ElasticsearchEngineTestModel 105 | { 106 | public function searchableAs() 107 | { 108 | return 'table'; 109 | } 110 | 111 | public function getKey() 112 | { 113 | return '1'; 114 | } 115 | 116 | public function toSearchableArray() 117 | { 118 | return ['id' => 1]; 119 | } 120 | } 121 | --------------------------------------------------------------------------------