はじめに
Elasticserchを導入しているプロジェクトがあって、どんな感じなのかイメージ掴むため、下記の参考記事にそって実装をすすめてみる
とりあえずLaravel Scout+Elasticsearchを動かしてみる
開発環境
Os:Mac
PHP:version 8.3.2
Laravel:version 10.45.1
Composer:version 2.2.4
Elasticsearch:5.6.16
導入
プロジェクトを作成
$ composer create-project --prefer-dist laravel/laravel Laravel-elasticserch
ライブラリインストール
$ composer require laravel/scout
$ composer require elasticsearch/elasticsearch
Scoutの設定ファイル生成して設定する
// config配下にscout.phpを生成
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
config/scout.php
<?php
return [
// ~~~[略]~~~
/*
|--------------------------------------------------------------------------
| Elasticsearch Configuration
|--------------------------------------------------------------------------
*/
'elasticsearch' => [
'index' => env('ELASTICSEARCH_INDEX', 'scout'),
'hosts' => [
env('ELASTICSEARCH_HOST', 'http://localhost'),
],
],
];
プロバイダー作成
$ php artisan make:provider ElasticsearchServiceProvider
ElasticsearchServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Scout\EngineManager;
use App\Scout\ElasticsearchEngine;
use Elastic\Elasticsearch\ClientBuilder;
class ElasticsearchServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
// ScoutのEngineManagerを拡張してElasticsearchをサポートす
resolve(EngineManager::class)->extend('elasticsearch', function ($app) {
// 適切なElasticsearchクライアントを使用してElasticsearchEngineの新しいインスタンスを作成
return new ElasticsearchEngine(
// Scoutの設定で指定されたインデックス名を使用
config('scout.elasticsearch.index'),
// Scoutの設定からElasticsearchのホスト情報を使用してElasticsearchクライアントを構築
ClientBuilder::create()
->setHosts(config('scout.elasticsearch.hosts'))
->build()
);
});
}
}
プロバイダー登録
config/app.php
'providers' => [
// 中略
/*
* Package Service Providers...
*/
App\Providers\ElasticsearchServiceProvider::class,
],
カスタムエンジンを作成
app\Scout\ElasticSearchEngine.php.php
<?php
namespace App\Scout;
use Elasticsearch\Client as Elastic;
use Laravel\Scout\Builder;
use Laravel\Scout\Engines\Engine;
class ElasticsearchEngine extends Engine
{
protected $index;
protected $elastic;
public function __construct($index, $elastic)
{
$this->index = $index;
$this->elastic = $elastic;
}
public function flush($model)
{
//
}
public function lazyMap(Builder $builder, $results, $model)
{
// Implement lazyMap method
}
public function createIndex($index, array $options = [])
{
// Implement createIndex method
}
public function deleteIndex($index)
{
// Implement deleteIndex method
}
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);
}
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);
}
public function search(Builder $builder)
{
// Implement search method
}
public function paginate(Builder $builder, $perPage, $page)
{
// Implement paginate method
}
public function mapIds($results)
{
// Implement mapIds method
}
public function map(Builder $builder, $results, $model)
{
// Implement map method
}
public function getTotalCount($results)
{
// Implement getTotalCount method
}
protected function performSearch(Builder $builder, $options = [])
{
// Implement performSearch method
}
public function filters(Builder $builder)
{
// Implement filters method
}
protected function sort(Builder $builder)
{
// Implement sort method
}
}
DB作成
$ mysql -u root
MariaDB> CREATE DATABASE `findhome`
MariaDB> CHARACTER SET utf8mb4
MariaDB> COLLATE utf8mb4_unicode_ci;
$ php artisan make:migration create_shops_table
$ php artisan migrate
モデル作成
$ php artisan make:model Models/Shop
Models/Shop.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Shop extends Model
{
use Searchable;
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'building_type',
'phone_number',
'address',
'latitude',
'longitude',
'nearest_station',
'open',
'close',
'wifi',
];
}
Elasticsearchの起動
ルート配下にdocker-compose.ymlを作成
docker-compose.yml
version: '2'
services:
elasticsearch:
image: elasticsearch:5.6
volumes:
- ./elasticsearch-data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
DockerでElasticsearchコンテナを起動
// コンテナ起動
$ docker-compose up -d
// 起動しているか確認
$ docker ps
- 起動できていることを確認
Elasticsearchの動作確認
- curlを使用してデータを追加する
$ curl -X PUT http://localhost:9200/test_index/httpd_access/1 -d '{"host": "localhost", "response": "200", "request": "/"}'
- 追加したデータの取得
$ curl -X GET http://localhost:9200/test_index/_search -d '{"query": { "match_all": {} } }'
- ステータス200で期待されている結果どおり取得できている
{
"took": 120,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1.0,
"hits": [
{
"_index": "test_index",
"_type": "httpd_access",
"_id": "1",
"_score": 1.0,
"_source": {
"host": "localhost",
"response": "200",
"request": "/"
}
}
]
}
}
テスト用データの作成
Elasticsearchに登録するテスト用のデータを生成する
$ php artisan make:factory ShopFactory
database/factories/ShopFactory.phpを追加
database/factories/ShopFactory.php
<?php
namespace Database\Factories;
use App\Models\Shop;
use Illuminate\Database\Eloquent\Factories\Factory;
class ShopFactory extends Factory
{
protected $model = Shop::class;
public function definition()
{
return [
'name' => $this->faker->streetName . $this->faker->randomElement(['マンション', 'ハウス', 'レーベン', 'レジデンス']),
'phone_number' => $this->faker->phoneNumber,
'address' => $this->faker->address,
'nearest_station' => $this->faker->randomElement(['東京', '表参道', '錦糸町']),
'open' => $this->faker->randomElement(['9:00', '10:00', '11:00', '12:00']),
'close' => $this->faker->randomElement(['19:00', '20:00', '21:00', '22:00', '23:00']),
'wifi' => $this->faker->randomElement(['あり', 'なし']),
];
}
}
- tinker使用せず、通常通りseederで作成してしまった。データが取得できているか確認
curl -X GET "localhost:9200/_cat/indices?v"
curl -X GET http://localhost:9200/scout/_search -d '{"query": { "match_all": {} } }'
- データみにくいけど、取得できている
- 検索してみる
curl -X GET http://localhost:9200/scout/_search -d '{
"query": {
"match": {
"nearest_station": "表参道"
}
}
}' | jq
- 取得できている
- 下記のエラーになる場合は、jqコマンドを使用できるようにインストールする
zsh: command not found: jq
# Macの方
$ brew install jq
まとめ
なんとなくElasticsearch触る時は、こなんな感じか〜ってイメージ掴むために実装すすめてみましたが、よく理解できていない。