在 Laravel 中使用 Elasticsearch的基本配置
2019年3月28日1.引入composer扩展包
1 |
$ composer require elasticsearch/elasticsearch '~6.0' |
2.配置
config/database.php
1 2 3 4 5 6 7 |
. . . 'elasticsearch' => [ // Elasticsearch 支持多台服务器负载均衡,因此这里是一个数组 'hosts' => explode(',', env('ES_HOSTS')), ], |
.env
1 2 3 4 |
. . . ES_HOSTS=localhost |
.env.example
1 2 3 4 |
. . . ES_HOSTS=localhost |
3.服务注册初始化 Elasticsearch 对象
app/Providers/AppServiceProvider.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
use Elasticsearch\ClientBuilder as ESClientBuilder; . . . public function register() { . . . #注册一个名为 es 的单例 $this->app->singleton('es', function () { #从配置文件读取 Elasticsearch 服务器列表 $builder = ESClientBuilder::create()->setHosts(config('database.elasticsearch.hosts')); #如果是开发环境 if (app()->environment() === 'local') { #配置日志,Elasticsearch 的请求和返回数据将打印到日志文件中,方便我们调试 $builder->setLogger(app('log')->driver()); } return $builder->build(); }); } |
4.创建索引
1 |
$ curl -XPUT http://localhost:9200/products?pretty |
由于查询的不支持嵌套,注意看 skus.title
字段的定义里加入了 copy_to
参数,值是 skus_title
,Elasticsearch 就会把这个字段值复制到 skus_title
字段里, skus_title
这个字段不需要自己手动创建,这样就可以在 multi_match
的 fields
里通过 skus_title
来匹配。skus.description
和 properties.name
同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ curl -H'Content-Type: application/json' -XPUT http://localhost:9200/products/_mapping/_doc?pretty -d'{ "properties": { "type": { "type": "keyword" } , "title": { "type": "text", "analyzer": "ik_smart" }, "long_title": { "type": "text", "analyzer": "ik_smart" }, "category_id": { "type": "integer" }, "category": { "type": "keyword" }, "category_path": { "type": "keyword" }, "description": { "type": "text", "analyzer": "ik_smart" }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "on_sale": { "type": "boolean" }, "rating": { "type": "float" }, "sold_count": { "type": "integer" }, "review_count": { "type": "integer" }, "skus": { "type": "nested", "properties": { "title": { "type": "text", "analyzer": "ik_smart", "copy_to": "skus_title" }, "description": { "type": "text", "analyzer": "ik_smart", "copy_to": "skus_description" }, "price": { "type": "scaled_float", "scaling_factor": 100 } } }, "properties": { "type": "nested", "properties": { "name": { "type": "keyword" }, "value": { "type": "keyword", "copy_to": "properties_value" }, "search_value": { "type": "keyword" } } } } }' |
创建成功返回
1 2 3 |
{ "acknowledged" : true } |
5.测试
1 2 |
$ php artisan tinker >>> app('es')->info() |
6.将数据转化为数组,准备填充到索引当中
填充格式↓
1 |
app('es')->index(['id' => $arr['id'], 'index' => 'products', 'type' => '_doc', 'body' => $arr]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/** * 将数据 转化未数组 * @return array */ public function toESArray() { #只取出需要的字段 $arr = array_only($this->toArray(), [ 'id', 'type', 'title', 'category_id', 'long_title', 'on_sale', 'rating', 'sold_count', 'review_count', 'price', ]); $arr['category'] = $this->category ? explode(' - ', $this->category->full_name) : ''; #如果商品有类目,则 category 字段为类目名数组,否则为空字符串 $arr['category_path'] = $this->category ? $this->category->path : ''; #类目的 path 字段 $arr['description'] = strip_tags($this->description); #strip_tags 函数可以将 html 标签去除 $arr['skus'] = $this->skus->map(function (ProductSku $sku) { #该map方法遍历集合并将每个值传递给给定的回调。回调可以自由修改项目并返回它,从而形成一个新的修改项目集合 return array_only($sku->toArray(), ['title', 'description', 'price']); }); $arr['properties'] = $this->properties->map(function (ProductProperty $property) { #该map方法遍历集合并将每个值传递给给定的回调。回调可以自由修改项目并返回它,从而形成一个新的修改项目集合 // 对应地增加一个 search_value 字段,用符号 : 将属性名和属性值拼接起来 return array_merge(array_only($property->toArray(), ['name', 'value']), [ 'search_value' => $property->name.':'.$property->value, ]); }); return $arr; } |
7.创建命令调用toESArray方法,一键填充到Elasticsearch
1 |
$ php artisan make:command Elasticsearch/SyncProducts |
app/Console/Commands/Elasticsearch/SyncProducts.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<?php namespace App\Console\Commands\Elasticsearch; use App\Models\Product; use Illuminate\Console\Command; class SyncProducts extends Command { /** * 命令名称 * @var string */ protected $signature = 'es:sync-products'; /** * 命令介绍 * @var string */ protected $description = '将商品数据同步到 Elasticsearch'; /** * 构造方法 * SyncProducts constructor. */ public function __construct() { parent::__construct(); } /** * 同步数据的方法 */ public function handle() { #获取 Elasticsearch 对象 $es = app('es'); Product::query() ->with(['skus', 'properties']) #使用 chunkById 避免一次性加载过多数据 ->chunkById(100, function ($products) use ($es) { $this->info(sprintf('正在同步 ID 范围为 %s 至 %s 的商品', $products->first()->id, $products->last()->id)); #初始化请求体 $req = ['body' => []]; #遍历商品 foreach ($products as $product) { #将商品模型转为 Elasticsearch 所用的数组 $data = $product->toESArray(); $req['body'][] = [ 'index' => [ '_index' => 'products', '_type' => '_doc', '_id' => $data['id'], ], ]; $req['body'][] = $data; } try { #使用 bulk 方法批量创建 $es->bulk($req); } catch (\Exception $e) { $this->error($e->getMessage()); } }); $this->info('同步完成'); } } |
执行命令
1 |
$ php artisan es:sync-products |
查看导入的数据
1 |
curl http://localhost:9200/products/_doc/_count?pretty |
8.创建队列时时更新数据
1 |
$ php artisan make:job SyncOneProductToES |
app/Jobs/SyncOneProductToES.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php namespace App\Jobs; use App\Models\Product; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; class SyncOneProductToES implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $product; public function __construct(Product $product) { $this->product = $product; } public function handle() { $data = $this->product->toESArray(); app('es')->index([ 'index' => 'products', 'type' => '_doc', 'id' => $data['id'], 'body' => $data, ]); } } |
在更改数据的地方使用队列
app/Admin/Controllers/CommonProductsController.php
1 2 3 4 5 6 |
use App\Jobs\SyncOneProductToES; $form->saved(function (Form $form) { $product = $form->model(); #获取刚才保存的数据 $this->dispatch(new SyncOneProductToES($product)); #执行队列 }); |
执行队列更新商品数据,测试一下
1 |
php artisan queue:work |