Last active
April 19, 2024 09:23
-
-
Save tillkruss/580352267fd404bfe975cbbb3efcb5f5 to your computer and use it in GitHub Desktop.
ElasticSearch engine for Laravel Scout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
resolve(EngineManager::class)->extend('elasticsearch', function ($app) { | |
return new ElasticsearchEngine( | |
ElasticBuilder::create()->setHosts(config('scout.elasticsearch.hosts'))->build() | |
); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Support; | |
use ReflectionClass; | |
use Laravel\Scout\Builder; | |
use Laravel\Scout\Engines\Engine; | |
use Elasticsearch\Client as Elastic; | |
use Illuminate\Support\Str; | |
use Illuminate\Database\Eloquent\Collection; | |
class ElasticSearchEngine extends Engine | |
{ | |
/** | |
* Elasticsearch client. | |
* | |
* @var \Elasticsearch\Client | |
*/ | |
public $elastic; | |
/** | |
* Create a new engine instance. | |
* | |
* @param \Elasticsearch\Client $elastic | |
* @return void | |
*/ | |
public function __construct(Elastic $elastic) | |
{ | |
$this->elastic = $elastic; | |
} | |
/** | |
* Update the given model in the index. | |
* | |
* @param Collection $models | |
* @return void | |
*/ | |
public function update($models) | |
{ | |
$params['body'] = []; | |
$models->each(function ($model) use (&$params) { | |
$params['body'][] = [ | |
'update' => [ | |
'_id' => $model->getKey(), | |
'_index' => $model->searchableAs(), | |
'_type' => Str::lower((new ReflectionClass($model))->getShortName()), | |
], | |
]; | |
$params['body'][] = [ | |
'doc' => $model->toSearchableArray(), | |
'doc_as_upsert' => true, | |
]; | |
}); | |
$this->elastic->bulk($params); | |
} | |
/** | |
* Remove the given model from the index. | |
* | |
* @param Collection $models | |
* @return void | |
*/ | |
public function delete($models) | |
{ | |
$params['body'] = []; | |
$models->each(function ($model) use (&$params) { | |
$params['body'][] = [ | |
'delete' => [ | |
'_id' => $model->getKey(), | |
'_index' => $model->searchableAs(), | |
'_type' => Str::lower((new ReflectionClass($model))->getShortName()), | |
], | |
]; | |
}); | |
$this->elastic->bulk($params); | |
} | |
/** | |
* Perform the given search on the engine. | |
* | |
* @param Builder $builder | |
* @return mixed | |
*/ | |
public function search(Builder $builder) | |
{ | |
return $this->performSearch($builder, array_filter([ | |
'filters' => $this->filters($builder), | |
'size' => $builder->limit, | |
])); | |
} | |
/** | |
* Perform the given search on the engine. | |
* | |
* @param Builder $builder | |
* @param int $perPage | |
* @param int $page | |
* @return mixed | |
*/ | |
public function paginate(Builder $builder, $perPage, $page) | |
{ | |
$result = $this->performSearch($builder, [ | |
'filters' => $this->filters($builder), | |
'from' => (($page * $perPage) - $perPage), | |
'size' => $perPage, | |
]); | |
$result['nbPages'] = $result['hits']['total'] / $perPage; | |
return $result; | |
} | |
/** | |
* Perform the given search on the engine. | |
* | |
* @param Builder $builder | |
* @param array $options | |
* @return mixed | |
*/ | |
protected function performSearch(Builder $builder, array $options = []) | |
{ | |
$params = [ | |
'index' => $builder->index ?: $builder->model->searchableAs(), | |
'body' => ['query' => []], | |
]; | |
if (! empty($builder->query)) { | |
$params['body']['query']['bool']['must']['match']['_all'] = [ | |
'query' => $builder->query, | |
'operator' => 'and', | |
'fuzziness' => 2, | |
]; | |
} | |
if (isset($options['from'])) { | |
$params['body']['from'] = $options['from']; | |
} | |
if (isset($options['size'])) { | |
$params['body']['size'] = $options['size']; | |
} | |
if (! empty($options['filters'])) { | |
$params['body']['query']['bool']['filter'] = [$options['filters']]; | |
} | |
if ($builder->orders) { | |
foreach ($builder->orders as $order) { | |
$params['body']['sort'][] = [$order['column'] => ['order' => $order['direction']]]; | |
} | |
} | |
if ($builder->callback) { | |
return call_user_func( | |
$builder->callback, | |
$this->elastic, | |
$builder->query, | |
$params | |
); | |
} | |
return $this->elastic->search($params); | |
} | |
/** | |
* Get the filter array for the query. | |
* | |
* @param Builder $builder | |
* @return array | |
*/ | |
protected function filters(Builder $builder) | |
{ | |
return collect($builder->wheres)->map(function ($value, $key) { | |
return ['term' => [$key => $value]]; | |
})->values()->all(); | |
} | |
/** | |
* Pluck and return the primary keys of the given results. | |
* | |
* @param mixed $results | |
* @return \Illuminate\Support\Collection | |
*/ | |
public function mapIds($results) | |
{ | |
return collect($results['hits']['hits'])->pluck('_id')->values(); | |
} | |
/** | |
* Map the given results to instances of the given model. | |
* | |
* @param mixed $results | |
* @param \Illuminate\Database\Eloquent\Model $model | |
* @return Collection | |
*/ | |
public function map($results, $model) | |
{ | |
if (count($results['hits']['total']) === 0) { | |
return Collection::make(); | |
} | |
$keys = collect($results['hits']['hits'])->pluck('_id')->values()->all(); | |
$models = $model->whereIn($model->getKeyName(), $keys)->get()->keyBy($model->getKeyName()); | |
return collect($results['hits']['hits'])->map(function ($hit) use ($model, $models) { | |
if (isset($models[$hit['_id']])) { | |
return $models[$hit['_id']]; | |
} | |
})->filter(); | |
} | |
/** | |
* Get the total count from a raw result returned by the engine. | |
* | |
* @param mixed $results | |
* @return int | |
*/ | |
public function getTotalCount($results) | |
{ | |
return $results['hits']['total']; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Console\Commands; | |
use Illuminate\Console\Command; | |
use Laravel\Scout\EngineManager; | |
use Elasticsearch\Common\Exceptions\Missing404Exception; | |
class SetupElasticSearch extends Command | |
{ | |
/** | |
* The name and signature of the console command. | |
* | |
* @var string | |
*/ | |
protected $signature = 'scout:elastic'; | |
/** | |
* The console command description. | |
* | |
* @var string | |
*/ | |
protected $description = 'Setup ElasticSearch indexes, types and mappings for Laravel Scout'; | |
/** | |
* Execute the console command. | |
* | |
* @return mixed | |
*/ | |
public function handle(EngineManager $engine) | |
{ | |
$client = $engine->engine('elasticsearch')->elastic; | |
foreach (['posts', 'products'] as $index) { | |
try { | |
$client->indices()->delete(['index' => config('scout.prefix') . $index]); | |
$this->line("<info>Deleted index:</info> {$index}"); | |
} catch (Missing404Exception $exception) { | |
// | |
} | |
} | |
$client->indices()->create([ | |
'index' => config('scout.prefix') . 'posts', | |
'body' => [ | |
'settings' => [ | |
'number_of_shards' => 1, | |
'number_of_replicas' => 1, | |
], | |
'mappings' => [ | |
'_default_' => [ | |
'properties' => [ | |
'userId' => ['type' => 'keyword'], | |
'state' => ['type' => 'keyword', 'include_in_all' => false], | |
'author' => ['type' => 'text'], | |
'site' => ['type' => 'text'], | |
'slug' => ['type' => 'text'], | |
'title' => ['type' => 'text'], | |
'content' => ['type' => 'text'], | |
], | |
], | |
], | |
], | |
]); | |
$this->line('<info>Created index:</info> posts'); | |
$client->indices()->create([ | |
'index' => config('scout.prefix') . 'products', | |
'body' => [ | |
'settings' => [ | |
'number_of_shards' => 1, | |
'number_of_replicas' => 1, | |
], | |
'mappings' => [ | |
'_default_' => [ | |
'properties' => [ | |
'type' => ['type' => 'keyword', 'include_in_all' => false], | |
'state' => ['type' => 'keyword', 'include_in_all' => false], | |
'visibility' => ['type' => 'keyword', 'include_in_all' => false], | |
'rating' => ['type' => 'integer', 'include_in_all' => false], | |
'trend' => ['type' => 'integer', 'include_in_all' => false], | |
'createdAt' => ['type' => 'date', 'include_in_all' => false], | |
'slug' => ['type' => 'text'], | |
'title' => ['type' => 'text'], | |
'description' => ['type' => 'text'], | |
'tags' => ['type' => 'text'], | |
], | |
], | |
], | |
], | |
]); | |
$this->line('<info>Created index:</info> products'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment