Skip to content

Instantly share code, notes, and snippets.

@tillkruss
Last active April 19, 2024 09:23
Show Gist options
  • Save tillkruss/580352267fd404bfe975cbbb3efcb5f5 to your computer and use it in GitHub Desktop.
Save tillkruss/580352267fd404bfe975cbbb3efcb5f5 to your computer and use it in GitHub Desktop.
ElasticSearch engine for Laravel Scout
<?php
resolve(EngineManager::class)->extend('elasticsearch', function ($app) {
return new ElasticsearchEngine(
ElasticBuilder::create()->setHosts(config('scout.elasticsearch.hosts'))->build()
);
});
<?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'];
}
}
<?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