首页 专题 文章 代码 归档

Laravel+Scout+Xunsearch最佳实践

1. 前言

先说一下啊,首先Laravel和Xunsearch各自都有各种坑,各种达不到很完美的效果;

使用这个方法不知道怎么回事,添加索引总是有重复的,每条数据都有两条索引;

有两条索引确实比较麻烦,等我以后研究下;

2. 步骤

以下步骤基本参考自:https://xueyuanjun.com/post/9485.html

但不得不说,不知道scout版本原因,还是什么原因,原文有坑;

2.1. 安装xunsearch服务端

安装不多言,在Linux上安装就行,如果这都不会,还是真的得研究下Linux;

官方文档:http://www.xunsearch.com/doc/php/guide/start.installation

2.2. 安装各自扩展包

xunsearch扩展包:

composer require hightman/xunsearch

Scout扩展包:

composer require laravel/scout

小坑1:截止本文发布时,scout是v8.0.0版本,这个版本仅支持Laravel^6.0|^7.0

如果你还是5.8.x,请安装:

composer require laravel/scout "v7.2.1"

2.3. 配置

基本配置

将配置文件 scout.php 发布到 config 目录下:

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

config/scout.php 中新增 xunsearch 相关配置:

'xunsearch' => [
    'host' => env('XUNSEARCH_HOST', '127.0.0.1'),
]

接下来需要修改 .env 中的相关配置:

SCOUT_DRIVER=xunsearch
XUNSEARCH_HOST=迅搜服务端IP地址
SCOUT_PREFIX=misiai_
SCOUT_QUEUE=true

在模型中,使用:

use Searchable;

如:

class Disk extends Model
{
    use Searchable;
    //....
}

索引配置文件

config/目录下新建一个xs_disk.ini的配置文件:

project.name = vlzpan_disk
project.default_charset = utf-8
server.index = xunsearch服务端IP:8383  // 不配置的话默认为127.0.0.1:8383 
server.search = xunsearch服务端IP:8384  // 不配置的话默认为127.0.0.1:8384

[id]
type = id

[filename]
type = title

[share_user]
type = mixed

该配置文件和官方一致,所以更多配置请到官网查看:http://www.xunsearch.com/doc/php/guide/ini.guide

2.4. 编写迅搜 Scout 扩展类

要实现基于迅搜驱动的搜索功能,还需要为其编写 Scout 扩展 XunSearchEnginge

该文件位于App\Services(没有则新建)

<?php


namespace App\Services;


use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Scout\Builder;

class SearchEngine
{
    protected $xs;

    public function __construct(\XS $xs)
    {
        $this->xs = $xs;
    }

    /**
     * 更新给定模型索引
     *
     * @param \Illuminate\Database\Eloquent\Collection $models
     * @return void
     * @throws \XSException
     */
    public function update($models)
    {
        if ($models->isEmpty()) {
            return;
        }

        // if ($this->usesSoftDelete($models->first()) && config('scout.soft_delete', false)) {
        //     $models->each->pushSoftDeleteMetadata();
        // }
        Log::info('Update Index');
        $index = $this->xs->index;
        $models->map(function ($model) use ($index) {
            $array = $model->toSearchableArray();
            if (empty($array)) {
                return;
            }
            $doc = new \XSDocument;
            $data = [
                'id' => $model->id,
                'filename' => $model->filename,
                'share_user' => $model->share_user,
            ];
            $doc->setFields($data);
            $index->update($doc);
        });
        $index->flushIndex();
    }

    /**
     * 从索引中移除给定模型
     *
     * @param \Illuminate\Database\Eloquent\Collection $models
     * @return void
     * @throws \XSException
     */
    public function delete($models)
    {
        $index = $this->xs->index;
        $models->map(function ($model) use ($index) {
            Log::info('Deleted:' . $model->getKey());
            $index->del($model->getKey());
        });
        $index->flushIndex();
    }


    /**
     * 通过迅搜引擎执行搜索
     *
     * @param \Laravel\Scout\Builder $builder
     * @return mixed
     */
    public function search(Builder $builder)
    {
        return $this->performSearch($builder, array_filter(['hitsPerPage' => $builder->limit]));
    }

    /**
     * 分页实现
     *
     * @param \Laravel\Scout\Builder $builder
     * @param int $perPage
     * @param int $page
     * @return mixed
     */
    public function paginate(Builder $builder, $perPage, $page)
    {
        return $this->performSearch($builder, [
            'hitsPerPage' => $perPage,
            'page' => $page - 1,
        ]);
    }

    /**
     * 返回给定搜索结果的主键
     *
     * @param mixed $results
     * @return \Illuminate\Support\Collection
     */
    public function mapIds($results)
    {
        return collect($results)
            ->pluck('id')->values();
    }


    /**
     * 将搜索结果和模型实例映射起来
     *
     * @param Builder $builder
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param mixed $results
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function map(Builder $builder, $results, $model)
    {
        if (count($results) === 0) {
            return Collection::make();
        }

        $keys = collect($results)
            ->pluck('id')->values()->unique()->all();
        $models = $model->getScoutModelsByIds($builder, $keys)->keyBy($model->getKeyName());
        return Collection::make($results)->map(function ($hit) use ($model, $models) {
            $key = $hit['id'];
            if (isset($models[$key])) {
                return $models[$key];
            }
        })->filter();
    }

    /**
     * 返回搜索结果总数
     *
     * @param mixed $results
     * @return int
     */
    public function getTotalCount($results)
    {
        return ceil($this->xs->search->getLastCount() / 2);
    }

    // protected function usesSoftDelete($model)
    // {
    //     return in_array(SoftDeletes::class, class_uses_recursive($model));
    // }
    // 执行搜索功能
    protected function performSearch(Builder $builder, array $options = [])
    {
        $search = $this->xs->search;

        if ($builder->callback) {
            return call_user_func(
                $builder->callback,
                $search,
                $builder->query,
                $options
            );
        }

        $search->setFuzzy()->setQuery($builder->query);
        collect($builder->wheres)->map(function ($value, $key) use ($search) {
            $search->addRange($key, $value, $value);
        });

        $offset = 0;
        $perPage = $options['hitsPerPage'];

        if (!empty($options['page'])) {
            $offset = $perPage * $options['page'];
        }
        return $search->setLimit($perPage, $offset)->search();
    }

    /**
     * 获取中文分词
     * @param $text
     * @return array
     */
    public function getScwsWords($text)
    {
        $tokenizer = new \XSTokenizerScws();
        return $tokenizer->getResult($text);
    }
}

说明:

你需要修改

$data = [
    'id' => $model->id,
    'filename' => $model->filename,
    'share_user' => $model->share_user,
];

中的字段为你自己的字段,当然,这还得和你之前ini文件中的字段保持一致;

这里有个中坑:

public function map(Builder $builder, $results, $model)
{
    if (count($results) === 0) {
        return Collection::make();
    }

    $keys = collect($results)
        ->pluck('id')->values()->all();
    $models = $model->getScoutModelsByIds($builder, $keys)->keyBy($model->getKeyName());
    return Collection::make($results)->map(function ($hit) use ($model, $models) {
        $key = $hit['id'];
        if (isset($models[$key])) {
            return $models[$key];
        }
    })->filter();
}

Map方法和原文的不一致,我修改过了;你可以看一下原文,原文只传了2个参数,但实际要传3个参数!

以上代码包含搜索、索引构建、删除、分页等所有功能,接下来需要做的就是将其绑定到 Scout 扩展中,我们可以通过在 AppServiceProviderboot 方法中添加以下代码来实现:

// 注册新的搜索引擎
resolve(EngineManager::class)->extend('xunsearch', function ($app) {
    $xs = new \XS(config_path('xs_disk.ini'));
    return new SearchEngine($xs);
});

3. 后言

目前暂时就这样,我遇到的问题是:

1、每条数据都有两条索引(搜索出来的结果每一条都有重复的一条)

2、上面写完了后,你使用:

$disks = Disk::search($query)->paginate(20);

即可搜索,返回的是模型对象的集合,也正如此,所以建议在ini配置文件中,只填写需要搜索的字段;

因为如果你仔细看SearchEngine类,你会发现所得到的仅仅是搜索结果的id,然后Laravel(Scout)再封装成每个模型(相当于应该还要从数据库中查询)

3、还值得给小白说的是,.env配置文件里面的

SCOUT_DRIVER=xunsearch
XUNSEARCH_HOST=迅搜服务端IP地址
SCOUT_PREFIX=misiai_
SCOUT_QUEUE=true

我们使用了SCOUT_QUEUE=true代表使用了队列,如果你不懂队列,那么将其改为false也行;使用队列不过是想提高web的响应速度罢了;

此文阅读完毕,您可以:分享
二维码图片 扫描关注我们哟