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

实现步骤

  1. 在laradock/.env中修改ELK版本

    # ~/laradock/.env
    ...
    ELK_VERSION=9.1.2
    ...
    
  2. 启动Elasticsearch和Kibana容器

    docker compose up -d elasticsearch kibana
    
  3. 开放对应端口

    sudo ufw allow 9200
    sudo ufw allow 9300
    sudo ufw allow 5601
    
  4. 验证服务启动成功
    4.1 浏览器输入:localhost:9200,结果如下在这里插入图片描述
    4.2 浏览器输入localhost:5601,结果如下
    在这里插入图片描述

  5. 在Laravel项目中安装Elasticsearch扩展

    composer require elasticsearch/elasticsearch
    
  6. 修改ELK配置

    # ~project/.env
    ...
    ELK_HOST_IP='你的IP地址:9200'
    ...
    
    # ~project/config/services.php
    ...
    'elk' => [
    	'host_ip' => env('ELK_HOST_IP')
    ];
    ...
    
  7. 创建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();
            }
        }
    }
    
  8. 创建测试模型和迁移

    cd ~/laradock
    docker compose exec workspace bash
    cd project
    php artisan make:model Article -m -f
    
  9. 定义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');
        }
    };
    
  10. 编辑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());
            });
        }
    }
    
  11. 编辑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),
            ];
        }
    }
    
  12. 创建命令类,向数据库中填充数据

    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();
        }
    }
    
  13. 定义schedule console

    # ~/project/routes/console.php
    ...
    // 每五分钟向数据库中插入1w条数据
    Schedule::command('app:create-articles')->everyFiveMinutes();
    ...
    
  14. 执行schedule命令,填充数据的同时会在Elasticsearch服务中创建索引(index)和映射(mapping),并将数据(doc)同步到ES中

    php artisan schedule:work
    
  15. 数据填充完毕(数据量越大越好,我本次测试插入了十几万条)后,在Kibana管理面板中可以看到创建的索引
    在这里插入图片描述
    点击索引名称进入详情,可以查看设置和映射等详细信息:
    在这里插入图片描述

  16. 创建控制器

    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;
            ...搜索代码见19return $service->search($this->index, $query);
    	}
    }
    
  17. 创建路由

    # ~/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']);
    });
    
  18. 访问localhost/es/test-connection,返回200表示连接ES服务成功

  19. 搜索
    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的功能远不止于此,如果将来有更深入的学习,本文会继续更新,敬请期待~

参考资料

Elasticsearch官方文档
Elasticsearch PHP扩展

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐