Laravel接入Elasticsearch实战教程
是一个开源的分布式搜索和分析引擎,专为速度、扩展和 AI 应用而打造。作为一个检索平台,它可以实时存储结构化、非结构化和向量数据,提供快速的混合和向量搜索,支持可观测性与安全分析,并以高性能、高准确性和高相关性实现 AI 驱动的应用。
Elasticsearch 是一个开源的分布式搜索和分析引擎,专为速度、扩展和 AI 应用而打造。作为一个检索平台,它可以实时存储结构化、非结构化和向量数据,提供快速的混合和向量搜索,支持可观测性与安全分析,并以高性能、高准确性和高相关性实现 AI 驱动的应用。
系统版本
Ubuntu 24.04
开发环境
PHP 8.4
MySQL 9.0
Docker 28.3.3
Elasticsearch 9.1.2
Kibana 9.1.2
Laravel 12
Laradock
Nginx
实现步骤
-
在laradock/.env中修改ELK版本
# ~/laradock/.env ... ELK_VERSION=9.1.2 ...
-
启动Elasticsearch和Kibana容器
docker compose up -d elasticsearch kibana
-
开放对应端口
sudo ufw allow 9200 sudo ufw allow 9300 sudo ufw allow 5601
-
验证服务启动成功
4.1 浏览器输入:localhost:9200,结果如下
4.2 浏览器输入localhost:5601,结果如下 -
在Laravel项目中安装Elasticsearch扩展
composer require elasticsearch/elasticsearch
-
修改ELK配置
# ~project/.env ... ELK_HOST_IP='你的IP地址:9200' ... # ~project/config/services.php ... 'elk' => [ 'host_ip' => env('ELK_HOST_IP') ]; ...
-
创建Elasticsearch服务类
<?php namespace App\Services; use Elastic\Elasticsearch\ClientBuilder; use Elastic\Elasticsearch\Exception\AuthenticationException; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Elasticsearch\Exception\MissingParameterException; use Elastic\Elasticsearch\Exception\ServerResponseException; class ElasticSearchService { protected $client; public function __construct() { try { $this->client = ClientBuilder::create() ->setHosts([config('services.elk.host_ip')]) ->setSSLVerification(false) ->build(); } catch (AuthenticationException $e) { throw $e; } } /** * 测试Elasticsearch连接 * * 尝试连接到Elasticsearch服务器并获取服务器信息。 * 如果连接成功,返回HTTP状态码;如果连接失败,返回错误信息。 * * @return int|string HTTP状态码(连接成功时)或错误信息(连接失败时) */ public function testConnection(): int|string { try { $result = $this->client->info(); return $result->getStatusCode(); } catch (\Exception $e) { return $e->getMessage(); } } /** * 检查Elasticsearch索引是否存在 * * @param string $index 要检查的索引名称 * @return mixed 索引存在时返回true,不存在时返回false,出错时返回错误信息 */ public function existIndex(string $index): mixed { try { return $this->client->indices()->exists(['index' => $index]); } catch (ClientResponseException|MissingParameterException|ServerResponseException $e) { return $e->getMessage(); } } /** * 创建Elasticsearch索引 * * @param string $index 索引名称 * @param array $settings 索引设置参数 * @param array $mappings 索引映射配置 * @return mixed 成功时返回创建索引的结果,失败时返回错误信息 */ public function createIndex(string $index, array $settings = [], array $mappings = []): mixed { try { $params = [ 'index' => $index, 'body' => [ 'settings' => $settings, 'mappings' => $mappings, ], ]; return $this->client->indices()->create($params); } catch (\Exception $e) { return $e->getMessage(); } } /** * 删除Elasticsearch索引 * * @param string $index 要删除的索引名称 * @return mixed 成功时返回删除索引的结果,失败时返回错误信息 */ public function deleteIndex(string $index): mixed { $params = [ 'index' => $index, ]; try { return $this->client->indices()->delete($params); } catch (ClientResponseException|MissingParameterException|ServerResponseException $e) { return $e->getMessage(); } } /** * 索引单个文档到Elasticsearch * * @param string $index 索引名称 * @param array $body 文档内容 * @return mixed 成功时返回索引操作结果,失败时返回错误信息 */ public function indexSingleDoc(string $index, array $body): mixed { $params = [ 'index' => $index, 'body' => $body, ]; try { return $this->client->index($params); } catch (ClientResponseException|MissingParameterException|ServerResponseException $e) { return $e->getMessage(); } } /** * 批量索引文档到Elasticsearch * * @param string $index 索引名称 * @param array $body 包含多个文档的数组,每个文档必须包含'id'字段 * @return mixed 成功时返回批量操作的结果,失败时返回错误信息 */ public function indexBulkDoc(string $index, array $body): mixed { $params = [ 'body' => [], ]; // 构建批量索引请求参数 foreach ($body as $item) { $params['body'][] = [ 'index' => [ '_index' => $index, '_id' => $item['id'], ], ]; $params['body'][] = $item; } try { return $this->client->bulk($params); } catch (ClientResponseException|ServerResponseException $e) { return $e->getMessage(); } } /** * 从Elasticsearch索引中删除指定文档 * * @param string $index 索引名称 * @param string $id 要删除的文档ID * @return mixed 成功时返回删除操作的结果,失败时返回错误信息 */ public function deleteDoc(string $index, string $id): mixed { $params = [ 'index' => $index, 'id' => $id, ]; try { return $this->client->delete($params); } catch (ClientResponseException|MissingParameterException|ServerResponseException $e) { return $e->getMessage(); } } /** * 更新Elasticsearch索引设置 * * @param string $index 索引名称 * @param array $settings 索引设置参数 * @return mixed 成功时返回更新设置的结果,失败时返回错误信息 */ public function putSettings(string $index, array $settings = []): mixed { $params = [ 'index' => $index, 'body' => [ 'settings' => $settings, ], ]; try { return $this->client->indices()->putSettings($params); } catch (ClientResponseException|ServerResponseException $e) { return $e->getMessage(); } } /** * 更新Elasticsearch索引映射 * * @param string $index 索引名称 * @param array $mappings 索引映射配置 * @return mixed 成功时返回更新映射的结果,失败时返回错误信息 */ public function putMappings(string $index, array $mappings = []): mixed { $params = [ 'index' => $index, 'body' => [ '_source' => [ 'enabled' => true, ], 'properties' => $mappings, ], ]; try { return $this->client->indices()->putMapping($params); } catch (ClientResponseException|MissingParameterException|ServerResponseException $e) { return $e->getMessage(); } } /** * 在指定索引中执行搜索查询 * * 使用提供的查询参数在Elasticsearch中搜索文档。 * 如果搜索成功,返回搜索结果;如果出现客户端或服务器响应异常,返回错误信息。 * * @param string $index 要搜索的索引名称 * @param array $query 查询参数数组 * @return mixed 搜索成功时返回结果数组,失败时返回错误信息 */ public function search(string $index, array $query): mixed { $params = [ 'index' => $index, 'body' => [ 'query' => $query ], ]; try { return $this->client->search($params); } catch (ClientResponseException|ServerResponseException $e) { return $e->getMessage(); } } }
-
创建测试模型和迁移
cd ~/laradock docker compose exec workspace bash cd project php artisan make:model Article -m -f
-
定义articles表字段
# ~/project/database/migrations/xxx_create_articles_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('articles', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->foreignId('author_id'); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('articles'); } };
-
编辑Article Model
# ~/project/app/Models/Article.php <?php namespace App\Models; use App\Services\ElasticSearchService; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Article extends Model { use HasFactory; protected $fillable = [ 'title', 'content', 'author_id', ]; protected static function booted() { static::created(function ($article) { $elasticsearchService = app(ElasticSearchService::class); $elasticsearchService->indexSingleDoc('articles', $article->toArray()); }); static::updated(function ($article) { $elasticsearchService = app(ElasticSearchService::class); $elasticsearchService->indexSingleDoc('articles', $article->toArray()); }); static::deleted(function ($article) { $elasticsearchService = app(ElasticSearchService::class); $elasticsearchService->deleteDoc('articles', $article->id->toArray()); }); } }
-
编辑Article Factory
# ~/project/database/factories/ArticleFactory.php <?php namespace Database\Factories; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article> */ class ArticleFactory extends Factory { /** * Define the model's default state. * * @return array<string, mixed> */ public function definition(): array { $users = User::all()->pluck('id')->toArray(); return [ 'title' => fake()->sentence(), 'content' => fake()->text, 'author_id' => fake()->randomElement($users), ]; } }
-
创建命令类,向数据库中填充数据
php artisan make:command CreateArticles
# ~/project/app/Console/Commands/CreateArticles.php <?php namespace App\Console\Commands; use App\Models\Article; use Illuminate\Console\Command; class CreateArticles extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'app:create-articles'; /** * The console command description. * * @var string */ protected $description = 'Create articles'; /** * Execute the console command. */ public function handle() { Article::factory()->count(10000)->create(); } }
-
定义schedule console
# ~/project/routes/console.php ... // 每五分钟向数据库中插入1w条数据 Schedule::command('app:create-articles')->everyFiveMinutes(); ...
-
执行schedule命令,填充数据的同时会在Elasticsearch服务中创建索引(index)和映射(mapping),并将数据(doc)同步到ES中
php artisan schedule:work
-
数据填充完毕(数据量越大越好,我本次测试插入了十几万条)后,在Kibana管理面板中可以看到创建的索引
点击索引名称进入详情,可以查看设置和映射等详细信息: -
创建控制器
php artisan make:controller ArticleEsController
# ~/project app/Http/Controllers/AritcleEsController <?php namespace App\Http\Controllers; use App\Services\ElasticSearchService; use Illuminate\Http\Request; class ArticleEsController extends Controller { protected ElasticSearchService $elasticSearchService; protected string $index = 'articles'; public function __construct(ElasticSearchService $elasticSearchService) { $this->elasticSearchService = $elasticSearchService; } public function search(Request $request) { $keywords = $request->input('keywords'); $service = $this->elasticSearchService; ...搜索代码见19步 return $service->search($this->index, $query); } }
-
创建路由
# ~/project/routes/web.php Route::prefix('es')->group(function () { Route::get('test-connection', function () { $service = new ElasticSearchService; return $service->testConnection(); }); Route::get('search', [ArticleEsController::class, 'search']); });
-
访问localhost/es/test-connection,返回200表示连接ES服务成功
-
搜索
16.1 默认匹配:查询title包含‘voluptas’或‘quas’的数据// match $query = [ 'match' => [ 'title' => [ 'query' => $keywords ] ] ];
16.2 查询content中包含‘voluptas’和‘quas’的数据
// match all $query = [ 'match' => [ 'content' => [ 'query' => $keywords, 'operator' => 'and' ] ] ];
16.3 指定至少匹配的约束,查询content中至少包含‘fuga nihil sequi’中两个单词的数据// match minimum $query = [ 'match' => [ 'content' => [ 'query' => $keywords, 'minimum_should_match' => 2 ] ] ];
16.4 同时搜索多个字段
// multi match $query = [ 'multi_match' => [ 'query' => $keywords, 'fields' => ['title', 'content'] ] ];
16.5 搜索多个字段时指定不同的权重
// multi match with different importance $query = [ 'multi_match' => [ 'query' => $keywords, 'fields' => [ 'title', 'content^2' ] ] ];
16.6 筛选
// filter $query = [ 'bool' => [ 'filter' => [ 'term' => [ 'title.keyword' => $keywords ] ] ] ];
16.7 范围搜索
// range $query = [ 'range' => [ 'author_id' => [ 'gte' => '2025-08-15', 'lte' => '2025-08-16' ] ] ];
16.8 精准搜索
// term $query = [ 'term' => [ 'title' => $keywords ] ];
16.9 组合搜索
// combine multiple search $query = [ 'bool' => [ 'should' => [ 'match' => [ 'content' => [ 'query' => $keywords ] ] ], 'must' => [ 'term' => [ 'title' => $keywords ] ], 'filter' => [ 'term' => [ 'author_id' => '1' ] ] ] ];
小结
本文描述了在Laravel中接入Elasticsearch的实现方法,希望能对感兴趣的小伙伴们有所帮助,当然ES的功能远不止于此,如果将来有更深入的学习,本文会继续更新,敬请期待~
参考资料
更多推荐
所有评论(0)