Laravel Scout 为 Eloquent 模型全文搜索实现提供了简单的、基于驱动的解决方案。通过使用模型观察者,Scout 会自动同步更新模型记录的索引,非常方便,易于上手,学院的文章搜索功能正好可以通过它来实现。
Laravel Scout 基于模型 + 底层搜索驱动扩展包来实现模型的全文搜索,目前,Scout 默认通过 Algolia 驱动提供搜索功能,不过,编写自定义驱动很简单,我们可以很轻松地通过自己的搜索实现来扩展 Scout。Algolia 毕竟是收费 API,而且是国外的服务,国内访问速度和可用性上不能保证,所以很自然被略过,接下来的选择就是自己搭建搜索引擎了,中文搜索有多种解决方案,比如轻量级的迅搜(xunsearch)、coreseek(sphinx变种,支持中文搜索),适用于中小型应用,还有适用于大型应用的 Elasticsearch。
因为之前项目用到过ElasticSearch所以一时激动直接就安装上了,无奈忽略了服务器内存,一但启动,整个服务器负载瞬间挤满,所以碍于模糊搜索不准确以及服务器不够给力(主要原因当然是穷 5 5 5~ ~ ~)我们改用xunsearch完成相关功能。坑蛮多的,这边也寻找到了不少轻量化的全文搜索,大家可以选个一样替换 1.
meilisearch
官网 2.sonic
仓库地址 3.zinc
仓库地址 4.redisSearch
仓库 5.TntSearch
相关导航
wget http://www.xunsearch.com/download/xunsearch-full-latest.tar.bz2
tar -xjf xunsearch-full-latest.tar.bz2 xunsearch
cd xunsearch/
sudo sh setup.sh
到这里我们就安装完了迅搜
cd /usr/local/xunsearch
bin/xs-ctl.sh restart
出现以下提示则代表已经完成启动
xunsearch
扩展包composer require hightman/xunsearch
Scout
扩展包(因为项目laravel版本为5.7最新的scout
要laravel8以上所以我这里需要在后面追加 "*")composer require laravel/scout
config
下php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
'xunsearch' => [
'host' => env('XUNSEARCH_HOST', '127.0.0.1'),
]
如图所示
.env
中的相关配置: (这里通过 Laravel Horizon 实现队列系统,关于这方面的内容请移步对应文档查看,这里不再单独介绍。)SCOUT_DRIVER=xunsearch
XUNSEARCH_HOST=迅搜服务端IP地址
SCOUT_PREFIX=academy_
SCOUT_QUEUE=true
php artisan config:clear
php artisan config:cache
use Searchable;
由于只对文章搜索,所以只要为其定义相应的索引配置文件即可,在
config
目录下创建xs_articles.ini
:
project.name = env('APP_NAME')
project.default_charset = utf-8
; 索引服务端配置,默认值为 8383
server.index = 8383
; 搜索服务端配置,默认值为 8384
server.search = 8384
[id]
type = id
[title]
type = title
tokenizer = scws([3])
[content]
type = body
关于字段索引的相关配置参考官方网站->迅搜官方文档
要实现基于迅搜驱动的搜索功能,还需要为其编写 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下的内容为你的索引配置项
//需要修改的内容
$data = [
'id' => $model->id,
'filename' => $model->filename,
'share_user' => $model->share_user,
];
以上代码包含搜索、索引构建、删除、分页等所有功能,接下来需要做的就是将其绑定到 Scout 扩展中,我们可以通过在 AppServiceProvider(app/Providers) 的 boot
方法中添加以下代码来实现:
// 注册新的搜索引擎
resolve(EngineManager::class)->extend('xunsearch', function ($app) {
$xs = new \XS(config_path('xs_disk.ini'));
return new SearchEngine($xs);
});
$disks = Disk::search($request->keyword)->paginate(20);
xs_disk.ini
复制到了xunsearch的php目录(具体路径为/usr/local/xunsearch/sdk/php/util)然后通过以下语句导入/usr/local/xunsearch/sdk/php/util/index.php --rebuild --source=mysql://数据库用户名:数据库密码@数据库地址(我的在本地所以默认localhost)/数据库名 --sql="select * from 表名" --project=配置文件名(我的是xs_disk.ini)