美文网首页
laravel 接入elasticsearch

laravel 接入elasticsearch

作者: Newzer | 来源:发表于2022-06-07 14:32 被阅读0次

在laravel中使用elasticsearch

  1. 引入composer包 Elasticsearch 官方提供了 Composer 包,在引入时需要注意要指定版本,因为不同版本的 Elasticsearch 的 API 略有不同,我们用的是 7.x,因此需使用 ~7.0 来指定包版本。
composer require elasticsearch/elasticsearch '7.6.1'

为什么不用 Scout?

熟悉 Laravel 的同学,应该会有此疑问。Scout 是 Laravel 官方出的一个让 Eloquent 模型支持全文搜索的包,这个包封装好一批方法,通过这些方法就可以将数据索引到全文搜索引擎中、以及使用关键字从搜索引擎搜索文档。这个包适用于一些简单的搜索场景,比如博客文章搜索,但无法发挥出全文搜索引擎全部威力,像 Elasticsearch 这种重量级的搜索引擎有无数种查询语句,例如 我们将会学到的 should 语句、模糊查询、分片查询等,根本不是 Scout 几个简单的方法能够覆盖的,也就无法满足电商系统搜索模块的需求。
  1. 配置 Elasticsearch 的配置很简单,我们只需要 Elasticsearch 服务器的 IP 和端口即可:

config/database.php

<?php
'elasticsearch' => [
    // Elasticsearch 支持多台服务器负载均衡,因此这里是一个数组
    'hosts' => explode(',', env('ES_HOSTS')),
]
?>

我们本地环境的 Elasticsearch 的 IP 和端口是 localhost:9200,如果端口是 9200 则可以忽略不写

.env

ES_HOSTS=localhost
  1. 初始化elasticsearch对象

接下来我们将初始化 Elasticsearch 对象,并注入到 Laravel 容器中: app/Providers/AppServiceProvider.php

<?php
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();
        });
    }
.
.
.
?>
  1. 测试

接下来我们来测试一下能否正常工作,首先进入 tinker:

php artisan tinker

然后输入:

>>>app('es')->info()

1.2 elasticsearch同步商品表数据

1.2.1 创建商品索引与定义商品字段

  1. 创建索引

现在我们需要重新创建一个 Elasticsearch 索引用于保存商品数据,名为 products:

curl -XPUT http://localhost:9200/products?pretty   或者   curl -XPUT http://你的服务器ip:9200/products?pretty
  1. 定义商品字段
PUT /products/_mapping?pretty
{
    "properties": {
    "type": { "type": "keyword" } ,
    "product_core": { "type": "text"},
    "title": { "type": "text", "analyzer": "ik_smart" },
    "long_title":{"type": "text","analyzer": "ik_smart"},
    "bar_code": { "type": "text"},
    "category": { "type": "keyword" },
    "category_path":{"type": "keyword"},
    "status": { "type": "boolean" },
    "audit_status": { "type": "integer" },
    "shop_name": { "type": "text","analyzer": "ik_smart" },
    "description": { "type": "text", "analyzer": "ik_smart" },
    "rating": { "type": "float" },
    "sold_count": { "type": "integer" },
    "review_count": { "type": "integer" },
    "price":{"type": "scaled_float", "scaling_factor": 100},
    "image":{"type":"text"},
    "skus": {
      "type": "nested",
      "properties": {
        "title": { "type": "text", "analyzer": "ik_smart" },
        "description": { "type": "text", "analyzer": "ik_smart" },
        "price": { "type": "scaled_float", "scaling_factor": 100 }
      }
    },
    "properties": {
      "type": "nested",
      "properties": {
        "name": { "type": "keyword" },
        "value": { "type": "keyword" }
      }
    },
    "images": {
      "type":"nested",
      "properties": {
        "image_url": {"type": "text"}
      }
    }
  }
}

"analyzer": "ik_smart" 代表这个字段需要使用 IK 中文分词器分词,在前面的章节也介绍过了。

还有有一些字段的类型是 keyword,这是字符串类型的一种,这种类型是告诉 Elasticsearch 不需要对这个字段做分词,通常用于邮箱、标签、属性等字段。

scaled_float 代表一个小数位固定的浮点型字段,与 Mysql 的 decimal 类型类似,后面的 scaling_factor 用来指定小数位精度,100 就代表精确到小数点后两位。

skus 和 properties 的字段类型是 nested,代表这个字段是一个复杂对象,由下一级的 properties 字段定义这个对象的字段。有同学可能会问,我们的『商品 SKU』和『商品属性』明明是对象数组,为什么这里可以定义成对象?这是 Elasticsearch 的另外一个特性,每个字段都可以保存多个值,这也是 Elasticsearch 的类型没有数组的原因,因为不需要,每个字段都可以是数组。

请确保 Elasticsearch 返回了 "acknowledged" : true,否则就要检查提交的内容是否有问题。

  1. 转化商品模型为数组,为之后的同步代码提供数据

接下来我们试着将一条商品数据写入到 Elasticsearch,需要写一个方法把商品模型转成符合上述字段格式的数组:

app\Models\Product

<?php
use Illuminate\Support\Arr;
.
.
.
    public function toESArray()
    {
      // 只取出需要的字段
      $arr = Arr::only($this->toArray(), [
        'id',
        'product_core',
        'title',
        'long_title',
        'bar_code',
        'status',
        'audit_status',
        'rating',
        'sold_count',
        'review_count',
        'price',
        'image'
      ]);

      // 如果商品有类目,则 category 字段为类目名数组,否则为空字符串
      $arr['category'] = $this->category ? explode(' - ', $this->category->full_name) : '';
      // 类目的 path 字段
      $arr['category_path'] = $this->category ? $this->category->path : '';
      // strip_tags 函数可以将 html 标签去除
      $arr['description'] = strip_tags($this->productdescriptions["description"]);
      // 只取出需要的 SKU 字段
      $arr['skus'] = $this->skus->map(function (ProductSku $sku) {
          return Arr::only($sku->toArray(), ['title', 'description', 'price']);
      });
      $arr['shop_name'] = $this->shop->name;
      // 只取出需要的商品属性字段
      $arr['properties'] = $this->properties->map(function (ProductProperty $property) {
          return Arr::only($property->toArray(), ['name', 'value']);
      });

      $arr['images'] = $this->images->map(function (ProductImage $productimage){
        return Arr::only($productimage->toArray(),['image_url']);
      });

      return $arr;
    }
.
.
.
?>

在上面代码中 category 字段的值可能是数组也可能是字符串,Elasticsearch 都支持,这就是我们上面说的,任何一个字段都可以存多个值。我们这里把商品的类目及对应的祖先类目都存到了这个字段中,这样当用户搜索任意一个祖先类目时都可以匹配到这个商品。

  1. 测试-将商品加入elasticsearch
    接下来我们试着读取一下,看看是否正确:

可以看到 category、skus 和 properties 返回的都是数组,与我们写入时的数据一致。

  1. 同步数据

首先我们需要把系统中已有的商品数据同步到 Elasticsearch 中,可以创建一个 Artisan 命令来实现:

php artisan make:command Elasticsearch/SyncProducts
<?php
namespace App\Console\Commands\Elasticsearch;

use App\Models\Product;
use Illuminate\Console\Command;

class SyncProducts extends Command
{
    protected $signature = 'es:sync-products';

    protected $description = '将商品数据同步到 Elasticsearch';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        // 获取 Elasticsearch 对象
        $es = app('es');

        Product::query()
            // 预加载 SKU 和 商品属性数据,避免 N + 1 问题
            ->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('同步完成');
    }
}
?>

这里使用了 chunkById() 方法避免一次性加载过多的商品数据,具体原理在之前的章节已经给大家介绍过了,这里不再多说。

我们在写入商品数据的时候用的是 bulk() 方法,这是 Elasticsearch 提供的一个批量操作接口。设想一下假如我们系统里有数百万条商品,如果每条商品都单独请求一次 Elasticsearch 的 API,那就是数百万次的请求,性能肯定是很差的,而 bulk() 方法可以让我们用一次 API 请求完成一批操作,从而减少请求次数的数量级,提高整体性能。

bulk() 方法的参数是一个数组,数组的第一行描述了我们要做的操作,第二行则代表这个操作所需要的数据,第三行操作描述,第四行数据,依次类推,当然如果是删除操作则没有数据行。我们这个代码里只有创建数据,因此都是每两行一组操作。

  1. 测试数据同步

现在我们来执行一下这个命令:

 php artisan es:sync-products

现在我们来看看 Elasticsearch 索引中有多少条数据,可以通过 /{index_name}/doc/count 这个接口来查询:

curl http://localhost:9200/products/_doc/_count?pretty

3. 商品提交redis队列同步变更数据到elasticsearch

接下来我们要实现新增或者修改商品时,能够同步到 Elasticsearch。

我们可以用一个异步任务来实现这个需求:

php artisan make:job SyncOneProductToES

这个异步任务的代码和我们上一节在 tinker 中执行的差不多:

app/Jobs/SyncOneProductToES.php

<?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;

// ShouldQueue 代表异步
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,
        ]);
    }
}
?>

接下来我们需要在合适的地方触发这个异步任务。

我们可以在 Laravel-Admin 的表单回调中来触发,当运营人员提交商品表单时,Laravel-Admin 就会触发表单回调,我们之前计算商品价格时用的是表单的 saving 事件,这次我们需要用 saved 事件,由于我们希望所有类型的商品在被创建或者被修改时都能同步到 Elasticsearch,所有我们把这个逻辑写在 ProductsController 中:

app/Admin/Controllers/ProductsController.php

<?php
use App\Jobs\SyncOneProductToES;
.
.
.
    protected function form()
    {
        .
        .
        .
        $form->saved(function (Form $form) {
            $product = $form->model();
            dispatch(new SyncOneProductToES($product));
        });

        return $form;
    }
.
.
.
?>

启动队列处理器:

php artisan queue:work

相关文章

网友评论

      本文标题:laravel 接入elasticsearch

      本文链接:https://www.haomeiwen.com/subject/zvdemrtx.html