Posted at

Laravel ScoutでElasticsearchを使うためにカスタムエンジンを自作した

More than 1 year has passed since last update.

Laravel + Elasticsearchで全文検索を試してみようと思って調べたところ、Laravel Scoutが見つかったので使ってみることにしました。

https://laravel.com/docs/5.6/scout

ただし、Laravel ScoutはデフォルトのドライバがAlgoliaになっていて、Elasticsearchを組み込めないようになっています。

そのため、カスタムエンジンを用意する必要があるのですが、自分は公式を見てもカスタムエンジンの実装方法がイマイチわかりませんでした。

ErickTamayo/laravel-scout-elastic

そんな時に上記のリポジトリを発見・参考にしてカスタムエンジンを実装できたのですが、自分の備忘録兼同じように困っている方向けに、自分が作成したカスタムエンジンを下記に記載します。

参考にして頂ければ幸いです。

また、カスタムエンジンの使用例については下記の記事で紹介していますので、ご参考下さい。

【Laravel】Laravel Scout + Elasticsearchを使った全文検索の実装方法


環境


  • Laravel: 5.6.33

  • laravel/scout: v5.0.3


カスタムエンジン - Elasticsearch

performSearch()でElasticsearchに実行させたいクエリを組み立てています。

Elasticsearchのクエリを自分のアプリケーション用にチューニングする際には、このメソッドを変更することになります。

<?php

namespace App\Scout;

use Elasticsearch\Client as Elastic;
use Laravel\Scout\Builder;
use Laravel\Scout\Engines\Engine;

class ElasticsearchEngine extends Engine
{

/**
* @var string
*/

protected $index;

/**
* @var Elastic
*/

protected $elastic;

/**
* ElasticsearchEngine constructor.
*
* @param string $index
* @param \Elasticsearch\Client $elastic
*/

public function __construct($index, Elastic $elastic)
{
$this->index = $index;
$this->elastic = $elastic;
}

/**
* Update the given model in the index.
*
* @param \Illuminate\Database\Eloquent\Collection $models
* @return void
*/

public function update($models)
{
$params['body'] = [];
$models->each(function ($model) use (&$params) {
$params['body'][] = [
'update' => [
'_id' => $model->getKey(),
'_index' => $this->index,
'_type' => $model->searchableAs(),
]
];
$params['body'][] = [
'doc' => $model->toSearchableArray(),
'doc_as_upsert' => true
];
});
$this->elastic->bulk($params);
}

/**
* Remove the given model from the index.
*
* @param \Illuminate\Database\Eloquent\Collection $models
* @return void
*/

public function delete($models)
{
$params['body'] = [];

$models->each(function ($model) use (&$params) {
$params['body'][] = [
'delete' => [
'_id' => $model->getKey(),
'_index' => $this->index,
'_type' => $model->searchableAs(),
]
];
});
$this->elastic->bulk($params);
}

/**
* Perform the given search on the engine.
*
* @param \Laravel\Scout\Builder $builder
* @return mixed
*/

public function search(Builder $builder)
{
return $this->performSearch($builder, array_filter([
'filters' => $this->filters($builder),
'limit' => $builder->limit,
]));
}

/**
* Perform the given search on the engine.
*
* @param \Laravel\Scout\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),
'limit' => $perPage,
]);

$result['nbPages'] = $result['hits']['total'] / $perPage;

return $result;
}

/**
* 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 \Laravel\Scout\Builder $builder
* @param mixed $results
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Collection
*/

public function map(Builder $builder, $results, $model)
{
if ($results['hits']['total'] === 0) {
return collect();
}

$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) {
return isset($models[$hit['_id']]) ? $models[$hit['_id']] : null;
})->filter()->values();
}

/**
* 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'];
}

/**
* @param \Laravel\Scout\Builder $builder
* @param array $options
* @return array|mixed
*/

protected function performSearch(Builder $builder, $options = [])
{
$params = [
'index' => $this->index,
'type' => $builder->index ?: $builder->model->searchableAs(),
'body' => [
'query' => [
'bool' => [
'must' => [
'term' => [
'title' => "{$builder->query}",
]
],
],
],
]
];

if ($sort = $this->sort($builder)) {
$params['body']['sort'] = $sort;
}

if (isset($options['filters']) && count($options['filters'])) {
$params['body']['query']['bool']['filter'] = $options['filters'];
}

if ($builder->callback) {
return call_user_func(
$builder->callback,
$this->elastic,
$builder->query,
$params
);
}

return $this->elastic->search($params);
}

public function filters(Builder $builder)
{
return collect($builder->wheres)->map(function ($value, $key) {
return [
'term' => [
$key => $value
]
];
})->values()->all();
}

protected function sort(Builder $builder)
{
if (count($builder->orders) == 0) {
return null;
}

return collect($builder->orders)->map(function ($order) {
return [$order['column'] => $order['direction']];
})->toArray();
}
}


参考

https://laravel.com/docs/5.6/scout

https://github.com/ErickTamayo/laravel-scout-elastic

https://public-constructor.com/laravel-scout-with-elasticsearch/