├── src ├── Events │ └── ModelsImported.php ├── ScoutServiceProvider.php ├── Jobs │ └── MakeSearchable.php ├── Console │ └── ImportCommand.php ├── SearchableScope.php ├── EngineManager.php ├── Engines │ ├── NullEngine.php │ ├── Engine.php │ ├── AlgoliaEngine.php │ └── ElasticsearchEngine.php ├── ModelObserver.php ├── Builder.php └── Searchable.php ├── readme.md ├── LICENSE.txt ├── composer.json └── config └── scout.php /src/Events/ModelsImported.php: -------------------------------------------------------------------------------- 1 | models = $models; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ScoutServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(EngineManager::class, function ($app) { 18 | return new EngineManager($app); 19 | }); 20 | 21 | if ($this->app->runningInConsole()) { 22 | $this->commands([ 23 | ImportCommand::class, 24 | ]); 25 | 26 | $this->publishes([ 27 | __DIR__.'/../config/scout.php' => config_path('scout.php'), 28 | ]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Jobs/MakeSearchable.php: -------------------------------------------------------------------------------- 1 | models = $models; 29 | } 30 | 31 | /** 32 | * Handle the job. 33 | * 34 | * @return void 35 | */ 36 | public function handle() 37 | { 38 | $this->models->first()->searchableUsing()->update($this->models); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Scout 2 | 3 | [![Build Status](https://travis-ci.org/laravel/scout.svg)](https://travis-ci.org/laravel/scout) 4 | [![Total Downloads](https://poser.pugx.org/laravel/scout/d/total.svg)](https://packagist.org/packages/laravel/scout) 5 | [![Latest Stable Version](https://poser.pugx.org/laravel/scout/v/stable.svg)](https://packagist.org/packages/laravel/scout) 6 | [![Latest Unstable Version](https://poser.pugx.org/laravel/scout/v/unstable.svg)](https://packagist.org/packages/laravel/scout) 7 | [![License](https://poser.pugx.org/laravel/scout/license.svg)](https://packagist.org/packages/laravel/scout) 8 | 9 | ## Introduction 10 | 11 | Laravel Scout provides a simple, driver-based solution for adding full-text search to your Eloquent models. 12 | 13 | ## Official Documentation 14 | 15 | Documentation for Scout can be found on the [Laravel website](http://laravel.com/docs/master/scout). 16 | 17 | ## License 18 | 19 | Laravel Scout is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Console/ImportCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 34 | 35 | $model = new $class; 36 | 37 | $events->listen(ModelsImported::class, function ($event) use ($class) { 38 | $key = $event->models->last()->getKey(); 39 | 40 | $this->line('Imported ['.$class.'] models up to ID: '.$key); 41 | }); 42 | 43 | $model::makeAllSearchable(); 44 | 45 | $this->info('All ['.$class.'] records have been imported.'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SearchableScope.php: -------------------------------------------------------------------------------- 1 | macro('searchable', function (EloquentBuilder $builder) { 33 | $builder->chunk(100, function ($models) use ($builder) { 34 | $models->searchable(); 35 | 36 | event(new ModelsImported($models)); 37 | }); 38 | }); 39 | 40 | $builder->macro('unsearchable', function (EloquentBuilder $builder) { 41 | $builder->chunk(100, function ($models) use ($builder) { 42 | $models->unsearchable(); 43 | }); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/scout", 3 | "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.", 4 | "keywords": [ 5 | "algolia", 6 | "laravel", 7 | "search" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.6.4", 18 | "illuminate/bus": "~5.3", 19 | "illuminate/contracts": "~5.3", 20 | "illuminate/database": "~5.3", 21 | "illuminate/pagination": "~5.3", 22 | "illuminate/queue": "~5.3", 23 | "illuminate/support": "~5.3" 24 | }, 25 | "require-dev": { 26 | "algolia/algoliasearch-client-php": "^1.10", 27 | "elasticsearch/elasticsearch": "^2.2", 28 | "mockery/mockery": "~0.9", 29 | "phpunit/phpunit": "~5.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Laravel\\Scout\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Tests\\": "tests/" 39 | } 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "1.0-dev" 44 | } 45 | }, 46 | "suggest": { 47 | "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^1.10).", 48 | "elasticsearch/elasticsearch": "Required to use the Elasticsearch engine (^2.2)." 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /src/EngineManager.php: -------------------------------------------------------------------------------- 1 | driver($name); 23 | } 24 | 25 | /** 26 | * Create an Algolia engine instance. 27 | * 28 | * @return \Laravel\Scout\Engines\AlgoliaEngine 29 | */ 30 | public function createAlgoliaDriver() 31 | { 32 | return new AlgoliaEngine(new Algolia( 33 | config('scout.algolia.id'), config('scout.algolia.secret') 34 | )); 35 | } 36 | 37 | /** 38 | * Create an Elasticsearch engine instance. 39 | * 40 | * @return \Laravel\Scout\Engines\ElasticsearchEngine 41 | */ 42 | public function createElasticsearchDriver() 43 | { 44 | return new ElasticsearchEngine( 45 | Elasticsearch::fromConfig(config('scout.elasticsearch.config')), 46 | config('scout.elasticsearch.index') 47 | ); 48 | } 49 | 50 | /** 51 | * Create a Null engine instance. 52 | * 53 | * @return \Laravel\Scout\Engines\NullEngine 54 | */ 55 | public function createNullDriver() 56 | { 57 | return new NullEngine; 58 | } 59 | 60 | /** 61 | * Get the default session driver name. 62 | * 63 | * @return string 64 | */ 65 | public function getDefaultDriver() 66 | { 67 | return $this->app['config']['scout.driver']; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Engines/NullEngine.php: -------------------------------------------------------------------------------- 1 | map( 70 | $this->search($builder), $builder->model 71 | )); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ModelObserver.php: -------------------------------------------------------------------------------- 1 | searchable(); 62 | } 63 | 64 | /** 65 | * Handle the updated event for the model. 66 | * 67 | * @param \Illuminate\Database\Eloquent\Model $model 68 | * @return void 69 | */ 70 | public function updated($model) 71 | { 72 | $this->created($model); 73 | } 74 | 75 | /** 76 | * Handle the updated event for the model. 77 | * 78 | * @param \Illuminate\Database\Eloquent\Model $model 79 | * @return void 80 | */ 81 | public function deleted($model) 82 | { 83 | if (static::syncingDisabledFor($model)) { 84 | return; 85 | } 86 | 87 | $model->unsearchable(); 88 | } 89 | 90 | /** 91 | * Handle the restored event for the model. 92 | * 93 | * @param \Illuminate\Database\Eloquent\Model $model 94 | * @return void 95 | */ 96 | public function restored($model) 97 | { 98 | $this->created($model); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /config/scout.php: -------------------------------------------------------------------------------- 1 | env('SCOUT_DRIVER', 'algolia'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Index Prefix 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify a prefix that will be applied to all search index 26 | | names used by Scout. This prefix may be useful if you have multiple 27 | | "tenants" or applications sharing the same search infrastructure. 28 | | 29 | */ 30 | 31 | 'prefix' => env('SCOUT_PREFIX', ''), 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Queue Data Syncing 36 | |-------------------------------------------------------------------------- 37 | | 38 | | This option allows you to control if the operations that sync your data 39 | | with your search engines are queued. When this is set to "true" then 40 | | all automatic data syncing will get queued for better performance. 41 | | 42 | */ 43 | 44 | 'queue' => false, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Algolia Configuration 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Here you may configure your Algolia settings. Algolia is a cloud hosted 52 | | search engine which works great with Scout out of the box. Just plug 53 | | in your application ID and admin API key to get started searching. 54 | | 55 | */ 56 | 57 | 'algolia' => [ 58 | 'id' => env('ALGOLIA_APP_ID', ''), 59 | 'secret' => env('ALGOLIA_SECRET', ''), 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Elasticsearch Configuration 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here you may configure your settings for Elasticsearch, which is a 68 | | distributed, open source search and analytics engine. Feel free 69 | | to add as many Elasticsearch servers as required by your app. 70 | | 71 | */ 72 | 73 | 'elasticsearch' => [ 74 | 'index' => env('ELASTICSEARCH_INDEX', 'laravel'), 75 | 76 | 'config' => [ 77 | 'hosts' => [ 78 | env('ELASTICSEARCH_HOST'), 79 | ], 80 | ], 81 | ], 82 | 83 | ]; 84 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | model = $model; 56 | $this->query = $query; 57 | } 58 | 59 | /** 60 | * Specify a custom index to perform this search on. 61 | * 62 | * @param string $index 63 | * @return $this 64 | */ 65 | public function within($index) 66 | { 67 | $this->index = $index; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Add a constraint to the search query. 74 | * 75 | * @param string $field 76 | * @param mixed $value 77 | * @return $this 78 | */ 79 | public function where($field, $value) 80 | { 81 | $this->wheres[$field] = $value; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Set the "limit" for the search query. 88 | * 89 | * @param int $limit 90 | * @return $this 91 | */ 92 | public function take($limit) 93 | { 94 | $this->limit = $limit; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Get the first result from the search. 101 | * 102 | * @return \Illuminate\Database\Eloquent\Model 103 | */ 104 | public function first() 105 | { 106 | return $this->get()->first(); 107 | } 108 | 109 | /** 110 | * Get the results of the search. 111 | * 112 | * @return \Illuminate\Database\Eloquent\Collection 113 | */ 114 | public function get() 115 | { 116 | return $this->engine()->get($this); 117 | } 118 | 119 | /** 120 | * Paginate the given query into a simple paginator. 121 | * 122 | * @param int $perPage 123 | * @param string $pageName 124 | * @param int|null $page 125 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 126 | */ 127 | public function paginate($perPage = 15, $pageName = 'page', $page = null) 128 | { 129 | $engine = $this->engine(); 130 | 131 | $page = $page ?: Paginator::resolveCurrentPage($pageName); 132 | 133 | $results = Collection::make($engine->map( 134 | $rawResults = $engine->paginate($this, $perPage, $page), $this->model 135 | )); 136 | 137 | $paginator = (new LengthAwarePaginator($results, $engine->getTotalCount($rawResults), $perPage, $page, [ 138 | 'path' => Paginator::resolveCurrentPath(), 139 | 'pageName' => $pageName, 140 | ])); 141 | 142 | return $paginator->appends('query', $this->query); 143 | } 144 | 145 | /** 146 | * Get the engine that should handle the query. 147 | * 148 | * @return mixed 149 | */ 150 | protected function engine() 151 | { 152 | return $this->model->searchableUsing(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Engines/AlgoliaEngine.php: -------------------------------------------------------------------------------- 1 | algolia = $algolia; 27 | } 28 | 29 | /** 30 | * Update the given model in the index. 31 | * 32 | * @param \Illuminate\Database\Eloquent\Collection $models 33 | * @throws \AlgoliaSearch\AlgoliaException 34 | * @return void 35 | */ 36 | public function update($models) 37 | { 38 | $index = $this->algolia->initIndex($models->first()->searchableAs()); 39 | 40 | $index->addObjects($models->map(function ($model) { 41 | $array = $model->toSearchableArray(); 42 | 43 | if (empty($array)) { 44 | return; 45 | } 46 | 47 | return array_merge(['objectID' => $model->getKey()], $array); 48 | })->filter()->values()->all()); 49 | } 50 | 51 | /** 52 | * Remove the given model from the index. 53 | * 54 | * @param \Illuminate\Database\Eloquent\Collection $models 55 | * @return void 56 | */ 57 | public function delete($models) 58 | { 59 | $index = $this->algolia->initIndex($models->first()->searchableAs()); 60 | 61 | $index->deleteObjects( 62 | $models->map(function ($model) { 63 | return $model->getKey(); 64 | })->values()->all() 65 | ); 66 | } 67 | 68 | /** 69 | * Perform the given search on the engine. 70 | * 71 | * @param \Laravel\Scout\Builder $builder 72 | * @return mixed 73 | */ 74 | public function search(Builder $builder) 75 | { 76 | return $this->performSearch($builder, array_filter([ 77 | 'numericFilters' => $this->filters($builder), 78 | 'hitsPerPage' => $builder->limit, 79 | ])); 80 | } 81 | 82 | /** 83 | * Perform the given search on the engine. 84 | * 85 | * @param \Laravel\Scout\Builder $builder 86 | * @param int $perPage 87 | * @param int $page 88 | * @return mixed 89 | */ 90 | public function paginate(Builder $builder, $perPage, $page) 91 | { 92 | return $this->performSearch($builder, [ 93 | 'numericFilters' => $this->filters($builder), 94 | 'hitsPerPage' => $perPage, 95 | 'page' => $page - 1, 96 | ]); 97 | } 98 | 99 | /** 100 | * Perform the given search on the engine. 101 | * 102 | * @param \Laravel\Scout\Builder $builder 103 | * @param array $options 104 | * @return mixed 105 | */ 106 | protected function performSearch(Builder $builder, array $options = []) 107 | { 108 | return $this->algolia->initIndex( 109 | $builder->index ?: $builder->model->searchableAs() 110 | )->search($builder->query, $options); 111 | } 112 | 113 | /** 114 | * Get the filter array for the query. 115 | * 116 | * @param \Laravel\Scout\Builder $builder 117 | * @return array 118 | */ 119 | protected function filters(Builder $builder) 120 | { 121 | return collect($builder->wheres)->map(function ($value, $key) { 122 | return $key.'='.$value; 123 | })->values()->all(); 124 | } 125 | 126 | /** 127 | * Map the given results to instances of the given model. 128 | * 129 | * @param mixed $results 130 | * @param \Illuminate\Database\Eloquent\Model $model 131 | * @return \Illuminate\Database\Eloquent\Collection 132 | */ 133 | public function map($results, $model) 134 | { 135 | if (count($results['hits']) === 0) { 136 | return Collection::make(); 137 | } 138 | 139 | $keys = collect($results['hits']) 140 | ->pluck('objectID')->values()->all(); 141 | 142 | $models = $model->whereIn( 143 | $model->getKeyName(), $keys 144 | )->get()->keyBy($model->getKeyName()); 145 | 146 | return Collection::make($results['hits'])->map(function ($hit) use ($model, $models) { 147 | $key = $hit[$model->getKeyName()]; 148 | 149 | if (isset($models[$key])) { 150 | return $models[$key]; 151 | } 152 | })->filter(); 153 | } 154 | 155 | /** 156 | * Get the total count from a raw result returned by the engine. 157 | * 158 | * @param mixed $results 159 | * @return int 160 | */ 161 | public function getTotalCount($results) 162 | { 163 | return $results['nbHits']; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Searchable.php: -------------------------------------------------------------------------------- 1 | registerSearchableMacros(); 23 | } 24 | 25 | /** 26 | * Register the searchable macros. 27 | * 28 | * @return void 29 | */ 30 | public function registerSearchableMacros() 31 | { 32 | $self = $this; 33 | 34 | BaseCollection::macro('searchable', function () use ($self) { 35 | $self->queueMakeSearchable($this); 36 | }); 37 | 38 | BaseCollection::macro('unsearchable', function () use ($self) { 39 | $self->queueRemoveFromSearch($this); 40 | }); 41 | } 42 | 43 | /** 44 | * Dispatch the job to make the given models searchable. 45 | * 46 | * @param \Illuminate\Database\Eloquent\Collection $models 47 | * @return void 48 | */ 49 | public function queueMakeSearchable($models) 50 | { 51 | if (! config('scout.queue')) { 52 | return $models->first()->searchableUsing()->update($models); 53 | } 54 | 55 | dispatch((new MakeSearchable($models)) 56 | ->onConnection($models->first()->syncWithSearchUsing())); 57 | } 58 | 59 | /** 60 | * Dispatch the job to make the given models unsearchable. 61 | * 62 | * @param \Illuminate\Database\Eloquent\Collection $models 63 | * @return void 64 | */ 65 | public function queueRemoveFromSearch($models) 66 | { 67 | return $models->first()->searchableUsing()->delete($models); 68 | } 69 | 70 | /** 71 | * Perform a search against the model's indexed data. 72 | * 73 | * @param string $query 74 | * @return \Laravel\Scout\Builder 75 | */ 76 | public static function search($query) 77 | { 78 | return new Builder(new static, $query); 79 | } 80 | 81 | /** 82 | * Make all instances of the model searchable. 83 | * 84 | * @return void 85 | */ 86 | public static function makeAllSearchable() 87 | { 88 | (new static)->newQuery()->searchable(); 89 | } 90 | 91 | /** 92 | * Make the given model instance searchable. 93 | * 94 | * @return void 95 | */ 96 | public function searchable() 97 | { 98 | Collection::make([$this])->searchable(); 99 | } 100 | 101 | /** 102 | * Remove all instances of the model from the search index. 103 | * 104 | * @return void 105 | */ 106 | public static function removeAllFromSearch() 107 | { 108 | (new static)->newQuery()->unsearchable(); 109 | } 110 | 111 | /** 112 | * Remove the given model instance from the search index. 113 | * 114 | * @return void 115 | */ 116 | public function unsearchable() 117 | { 118 | Collection::make([$this])->unsearchable(); 119 | } 120 | 121 | /** 122 | * Enable search syncing for this model. 123 | * 124 | * @return void 125 | */ 126 | public static function enableSearchSyncing() 127 | { 128 | ModelObserver::enableSyncingFor(get_called_class()); 129 | } 130 | 131 | /** 132 | * Disable search syncing for this model. 133 | * 134 | * @return void 135 | */ 136 | public static function disableSearchSyncing() 137 | { 138 | ModelObserver::disableSyncingFor(get_called_class()); 139 | } 140 | 141 | /** 142 | * Temporarily disable search syncing for the given callback. 143 | * 144 | * @param callable $callback 145 | * @return void 146 | */ 147 | public static function withoutSyncingToSearch($callback) 148 | { 149 | static::disableSearchSyncing(); 150 | 151 | $callback(); 152 | 153 | static::enableSearchSyncing(); 154 | } 155 | 156 | /** 157 | * Get the index name for the model. 158 | * 159 | * @return string 160 | */ 161 | public function searchableAs() 162 | { 163 | return config('scout.prefix').$this->getTable(); 164 | } 165 | 166 | /** 167 | * Get the indexable data array for the model. 168 | * 169 | * @return array 170 | */ 171 | public function toSearchableArray() 172 | { 173 | return $this->toArray(); 174 | } 175 | 176 | /** 177 | * Get the Scout engine for the model. 178 | * 179 | * @return mixed 180 | */ 181 | public function searchableUsing() 182 | { 183 | return app(EngineManager::class)->engine(); 184 | } 185 | 186 | /** 187 | * Get the queue connection that should be used when syncing. 188 | * 189 | * @return string 190 | */ 191 | public function syncWithSearchUsing() 192 | { 193 | return config('queue.default'); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Engines/ElasticsearchEngine.php: -------------------------------------------------------------------------------- 1 | elasticsearch = $elasticsearch; 35 | 36 | $this->index = $index; 37 | } 38 | 39 | /** 40 | * Update the given model in the index. 41 | * 42 | * @param Collection $models 43 | * @return void 44 | */ 45 | public function update($models) 46 | { 47 | $body = new BaseCollection(); 48 | 49 | $models->each(function ($model) use ($body) { 50 | $array = $model->toSearchableArray(); 51 | 52 | if (empty($array)) { 53 | return; 54 | } 55 | 56 | $body->push([ 57 | 'index' => [ 58 | '_index' => $this->index, 59 | '_type' => $model->searchableAs(), 60 | '_id' => $model->getKey(), 61 | ], 62 | ]); 63 | 64 | $body->push($array); 65 | }); 66 | 67 | $this->elasticsearch->bulk([ 68 | 'refresh' => true, 69 | 'body' => $body->all(), 70 | ]); 71 | } 72 | 73 | /** 74 | * Remove the given model from the index. 75 | * 76 | * @param Collection $models 77 | * @return void 78 | */ 79 | public function delete($models) 80 | { 81 | $body = new BaseCollection(); 82 | 83 | $models->each(function ($model) use ($body) { 84 | $body->push([ 85 | 'delete' => [ 86 | '_index' => $this->index, 87 | '_type' => $model->searchableAs(), 88 | '_id' => $model->getKey(), 89 | ], 90 | ]); 91 | }); 92 | 93 | $this->elasticsearch->bulk([ 94 | 'refresh' => true, 95 | 'body' => $body->all(), 96 | ]); 97 | } 98 | 99 | /** 100 | * Perform the given search on the engine. 101 | * 102 | * @param Builder $query 103 | * @return mixed 104 | */ 105 | public function search(Builder $query) 106 | { 107 | return $this->performSearch($query, [ 108 | 'filters' => $this->filters($query), 109 | 'size' => $query->limit ?: 10000, 110 | ]); 111 | } 112 | 113 | /** 114 | * Perform the given search on the engine. 115 | * 116 | * @param Builder $query 117 | * @param int $perPage 118 | * @param int $page 119 | * @return mixed 120 | */ 121 | public function paginate(Builder $query, $perPage, $page) 122 | { 123 | $result = $this->performSearch($query, [ 124 | 'filters' => $this->filters($query), 125 | 'size' => $perPage, 126 | 'from' => (($page * $perPage) - $perPage), 127 | ]); 128 | 129 | $result['nbPages'] = (int) ceil($result['hits']['total'] / $perPage); 130 | 131 | return $result; 132 | } 133 | 134 | /** 135 | * Perform the given search on the engine. 136 | * 137 | * @param Builder $query 138 | * @param array $options 139 | * @return mixed 140 | */ 141 | protected function performSearch(Builder $query, array $options = []) 142 | { 143 | $termFilters = []; 144 | 145 | $matchQueries[] = [ 146 | 'match' => [ 147 | '_all' => [ 148 | 'query' => $query->query, 149 | 'fuzziness' => 1 150 | ] 151 | ] 152 | ]; 153 | 154 | if (array_key_exists('filters', $options) && $options['filters']) { 155 | foreach ($options['filters'] as $field => $value) { 156 | 157 | if(is_numeric($value)) { 158 | $termFilters[] = [ 159 | 'term' => [ 160 | $field => $value, 161 | ], 162 | ]; 163 | } elseif(is_string($value)) { 164 | $matchQueries[] = [ 165 | 'match' => [ 166 | $field => [ 167 | 'query' => $value, 168 | 'operator' => 'and' 169 | ] 170 | ] 171 | ]; 172 | } 173 | 174 | } 175 | } 176 | 177 | $searchQuery = [ 178 | 'index' => $this->index, 179 | 'type' => $query->model->searchableAs(), 180 | 'body' => [ 181 | 'query' => [ 182 | 'filtered' => [ 183 | 'filter' => $termFilters, 184 | 'query' => [ 185 | 'bool' => [ 186 | 'must' => $matchQueries 187 | ] 188 | ], 189 | ], 190 | ], 191 | ], 192 | ]; 193 | 194 | if (array_key_exists('size', $options)) { 195 | $searchQuery = array_merge($searchQuery, [ 196 | 'size' => $options['size'], 197 | ]); 198 | } 199 | 200 | if (array_key_exists('from', $options)) { 201 | $searchQuery = array_merge($searchQuery, [ 202 | 'from' => $options['from'], 203 | ]); 204 | } 205 | 206 | return $this->elasticsearch->search($searchQuery); 207 | } 208 | 209 | /** 210 | * Get the filter array for the query. 211 | * 212 | * @param Builder $query 213 | * @return array 214 | */ 215 | protected function filters(Builder $query) 216 | { 217 | return $query->wheres; 218 | } 219 | 220 | /** 221 | * Map the given results to instances of the given model. 222 | * 223 | * @param mixed $results 224 | * @param \Illuminate\Database\Eloquent\Model $model 225 | * @return Collection 226 | */ 227 | public function map($results, $model) 228 | { 229 | if (count($results['hits']) === 0) { 230 | return Collection::make(); 231 | } 232 | 233 | $keys = collect($results['hits']['hits']) 234 | ->pluck('_id') 235 | ->values() 236 | ->all(); 237 | 238 | $models = $model->whereIn( 239 | $model->getKeyName(), $keys 240 | )->get()->keyBy($model->getKeyName()); 241 | 242 | return Collection::make($results['hits']['hits'])->map(function ($hit) use ($model, $models) { 243 | return $models[$hit['_source'][$model->getKeyName()]]; 244 | }); 245 | } 246 | 247 | /** 248 | * Get the total count from a raw result returned by the engine. 249 | * 250 | * @param mixed $results 251 | * @return int 252 | */ 253 | public function getTotalCount($results) 254 | { 255 | return $results['hits']['total']; 256 | } 257 | } 258 | --------------------------------------------------------------------------------