php 搜索多模型,基于迅搜(xunsearch) + Laravel Scout 实现 Laravel 学院全文搜索功能(支持多模型搜索)...

基于迅搜(xunsearch) + Laravel Scout 实现 Laravel 学院全文搜索功能(支持多模型搜索)

由 学院君 创建于2年前, 最后更新于 5个月前

版本号 #2

13460 views

11 likes

1 collects

概述

Laravel Scout 为 Eloquent 模型全文搜索实现提供了简单的、基于驱动的解决方案。通过使用模型观察者,Scout 会自动同步更新模型记录的索引,非常方便,易于上手,学院的文章搜索功能正好可以通过它来实现。

Laravel Scout 基于模型 + 底层搜索驱动扩展包来实现模型的全文搜索,目前,Scout 默认通过 Algolia 驱动提供搜索功能,不过,编写自定义驱动很简单,我们可以很轻松地通过自己的搜索实现来扩展 Scout。Algolia 毕竟是收费 API,而且是国外的服务,国内访问速度和可用性上不能保证,所以很自然被略过,接下来的选择就是自己搭建搜索引擎了,中文搜索有多种解决方案,比如轻量级的迅搜(xunsearch)、coreseek(sphinx变种,支持中文搜索),适用于中小型应用,还有适用于大型应用的 Elasticsearch。

对于学院的规模来说使用迅搜就够了,简单易上手,只需少许步骤就可以快速搭建其自己的搜索引擎,而且它们的客户中就有国内著名的编程社区 segmentfault,有这样的背书也可以让我们放心使用。

安装迅搜服务端

在服务器上安装迅搜很简单,只需以下几步即可:

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

安装完成后,通过以下命令启动:

sudo bin/xs-ctl.sh start

以上命令默认在本地回环地址(127.0.0.1)8383/8384上监听服务,如果你有多台机器需要访问迅搜服务端,需要通过以下命令启动:

bin/xs-ctl.sh -b inet start

以上过程没有报错,就意味着迅搜已经正常启动了。

如果通过 Docker 启动迅搜服务的话,对应 Dockerfile 如下:

# xunsearch-dev docker

# created by hightman.20150826

#

# START COMMAND:

# docker run -d --name xunsearch -p 8383:8383 -p 8384:8384 \

# -v /var/xunsearch/data:/usr/local/xunsearch/data hightman/xunsearch:latest

#

FROM ubuntu:14.04

MAINTAINER hightman, [email protected]

# Install required packages

RUN apt-get update -qq

RUN apt-get install -qy --no-install-recommends \

wget make gcc g++ bzip2 zlib1g-dev

# Download & Install xunsearch-latest

RUN cd /root && wget -qO - http://www.xunsearch.com/download/xunsearch-full-latest.tar.bz2 | tar xj

RUN cd /root/xunsearch-full-* && sh setup.sh --prefix=/usr/local/xunsearch

RUN echo '' >> /usr/local/xunsearch/bin/xs-ctl.sh \

&& echo 'tail -f /dev/null' >> /usr/local/xunsearch/bin/xs-ctl.sh

# Configure it

VOLUME /usr/local/xunsearch/data

EXPOSE 8383

EXPOSE 8384

WORKDIR /usr/local/xunsearch

RUN echo "#!/bin/sh" > bin/xs-docker.sh \

&& echo "rm -f tmp/pid.*" >> bin/xs-docker.sh \

&& echo "echo -n > tmp/docker.log" >> bin/xs-docker.sh \

&& echo "bin/xs-indexd -l tmp/docker.log -k start" >> bin/xs-docker.sh \

&& echo "sleep 1" >> bin/xs-docker.sh \

&& echo "bin/xs-searchd -l tmp/docker.log -k start" >> bin/xs-docker.sh \

&& echo "sleep 1" >> bin/xs-docker.sh \

&& echo "tail -f tmp/docker.log" >> bin/xs-docker.sh

ENTRYPOINT ["sh"]

CMD ["bin/xs-docker.sh"]

安装相关 PHP 扩展包

首先通过 Composer 安装 xunsearch 扩展包:

composer require hightman/xunsearch

安装完迅搜扩展包后,在 Laravel 中使用 Scout 也需要安装对应扩展包:

composer require laravel/scout

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

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

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

'xunsearch' => [

'host' => env('XUNSEARCH_HOST', '127.0.0.1'),

]

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

SCOUT_DRIVER=xunsearch

XUNSEARCH_HOST=迅搜服务端IP地址

SCOUT_PREFIX=academy_

SCOUT_QUEUE=true

注意到我们将 SCOUT_DRIVER 改成了 xunsearch,XUNSEARCH_HOST 必须与你安装迅搜所在的服务器IP一致,最后我们将索引构建设置为通过队列异步执行,学院君通过 Laravel Horizon 实现队列系统,关于这方面的内容请移步对应文档查看,这里不再单独介绍。

学院暂时只支持文章搜索,所以需要为对应模型中添加如下代码以支持自动更新索引和搜索:

use Searchable;

索引配置文件

由于我们只是对学院文章进行搜索,所以只要为其定义相应的索引配置文件即可,在 config 目录下创建xs_article.ini:

project.name = academy_article

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

[pid]

type = id

[title]

type = title

[summary]

[content]

type = body

[tag_text]

type = both

[category_id]

type = numeric

index = self

[author]

index = both

[author_id]

type = numeric

[view_count]

type = numeric

[vote_count]

type = numeric

[comment_count]

type = numeric

[publish_time]

索引哪些字段由你自己决定,这里只是个参考,关于字段明细介绍,请参考迅搜官方文档,这里不在赘述,要想了解迅搜搜索引擎工作流程和原理,请务必先仔细阅读一遍迅搜官方文档。

编写迅搜 Scout 扩展类

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

namespace App\Services\SearchEngine;

use Illuminate\Database\Eloquent\Collection;

use Illuminate\Database\Eloquent\SoftDeletes;

use Laravel\Scout\Builder;

use Laravel\Scout\Engines\Engine;

class XunSearchEngine extends Engine

{

/**

* @var \XS

*/

protected $xs;

public function __construct(\XS $xs)

{

$this->xs = $xs;

}

/**

* 更新给定模型索引

*

* @param \Illuminate\Database\Eloquent\Collection $models

* @return void

*/

public function update($models)

{

if ($models->isEmpty()) {

return;

}

if ($this->usesSoftDelete($models->first()) && config('scout.soft_delete', false)) {

$models->each->pushSoftDeleteMetadata();

}

$index = $this->xs->index;

$models->map(function ($model) use ($index) {

$array = $model->toSearchableArray();

if (empty($array)) {

return;

}

$doc = new \XSDocument;

$data = [

'pid' => $model->id,

'title' => $model->title,

'summary' => $model->summary,

'content' => $model->content,

'tag_text' => $model->tag_text,

'category_id' => $model->category_id,

'author' => $model->author->name,

'author_id' => $model->user_id,

'view_count' => $model->view_count,

'vote_count' => $model->vote_count,

'comment_count' => $model->comment_count,

'publish_time' => $model->posted_at

];

$doc->setFields($data);

$index->update($doc);

});

$index->flushIndex();

}

/**

* 从索引中移除给定模型

*

* @param \Illuminate\Database\Eloquent\Collection $models

* @return void

*/

public function delete($models)

{

$index = $this->xs->index;

$models->map(function ($model) use ($index) {

$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('pid')->values();

}

/**

* 将搜索结果和模型实例映射起来

*

* @param mixed $results

* @param \Illuminate\Database\Eloquent\Model $model

* @return \Illuminate\Database\Eloquent\Collection

*/

public function map($results, $model)

{

if (count($results) === 0) {

return Collection::make();

}

$keys = collect($results)

->pluck('pid')->values()->all();

$models = $model->getScoutModelsByIds($keys)->keyBy($model->getKeyName());

return Collection::make($results)->map(function ($hit) use ($model, $models) {

$key = $hit['pid'];

if (isset($models[$key])) {

return $models[$key];

}

})->filter();

}

/**

* 返回搜索结果总数

*

* @param mixed $results

* @return int

*/

public function getTotalCount($results)

{

return $this->xs->search->getLastCount();

}

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

}

}

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

// 注册新的搜索引擎

resolve(EngineManager::class)->extend('xunsearch', function ($app) {

$xs = new \XS(config_path('xs_article.ini'));

return new XunSearchEngine($xs);

});

演示搜索功能

完成以上所有工作后,就可以在更新/新增文章模型后对其进行搜索了,更新/新增模型后可以在 Horizon 后台看到队列中的索引更新/新增记录:

php 搜索多模型,基于迅搜(xunsearch) + Laravel Scout 实现 Laravel 学院全文搜索功能(支持多模型搜索)..._第1张图片

队列任务执行完成后,就可以通过搜索框进行搜索了,执行搜索的代码实现也很简单:

$keyword = $request->get('keyword');

$page = $request->get('page') ? : 1;

$pageSize = $request->get('page_size') ? : 10;

$articles = Article::search($keyword)->paginate($pageSize, 'page', $page);

以上是一个分页搜索,比如我们搜索「Laravel学院」,显示结果如下:

php 搜索多模型,基于迅搜(xunsearch) + Laravel Scout 实现 Laravel 学院全文搜索功能(支持多模型搜索)..._第2张图片

多模型搜索支持

=============== 2018.11.04 更新 ===============

上述实现只能对文章进行搜索,并且将索引字段硬编码到引擎类 XunSearchEngine 中,如果后续需要对更多模型进行搜索,比如问答模块要支持搜索功能,现在的实现就不能满足了,比较偷懒的实现是为每个索引生成不同的引擎实例,然后将 XunSearchEngine 类的 update 方法中索引字段同步部分迁移出去。下面给出一个简单的实现示例:

因为学院主要还是文章搜索,所以保留文章搜索引擎 xunsearch 作为主搜索引擎,即默认引擎,所以注册该引擎的地方保持不变,我们在 Article 模型类中定义一个新方法用于将模型字段数据同步到搜索索引字段:

public function searchableIndexData()

{

$indexData = [

'pid' => $this->id,

'title' => $this->title,

'summary' => $this->summary,

'content' => $this->content,

'tag_text' => $this->tag_text,

'category_id' => $this->category_id,

'author' => $this->author->name,

'author_id' => $this->user_id,

'view_count' => $this->view_count,

'vote_count' => $this->vote_count,

'comment_count' => $this->comment_count,

'publish_time' => $this->posted_at

];

return $indexData;

}

然后将 XunSearchEngine 的 update 方法中同步模型数据到索引部分代码修改如下:

$doc = new \XSDocument;

$doc->setFields($model->searchableIndexData());

$index->update($doc);

这样就将这部门硬编码重构出去了,接下来,要实现问答模块搜索,需要在其模型类 Discussion 中使用 Searchable Trait:

use SoftDeletes, Searchable;

然后在这个模型类中通过重写 Searchable 中的 searchableUsing 方法来定义该模型搜索使用的搜索引擎实例:

public function searchableUsing()

{

$xs = new \XS(config_path('xs_discussion.ini'));

return new XunSearchEngine($xs);

}

我们为 Discussion 模型创建了新的索引配置文件 xs_discussion.ini,具体配置参考 xs_article.ini 定义即可,这里不再赘述,然后返回新的引擎实例用于问答模型搜索,当然还要在模型类中定义模型字段同步方法:

public function searchableIndexData()

{

$indexData = [

'pid' => $this->id,

'title' => $this->title,

'description' => $this->desscription,

'tag_text' => $this->tag_text,

'category_id' => $this->category_id,

'author' => $this->author->name,

'author_id' => $this->user_id,

'view_count' => $this->view_count,

'comment_count' => $this->comment_count,

'publish_time' => $this->posted_at

];

return $indexData;

}

这样我们就完成了基于迅搜 + Laravel Scout 的多模型搜索功能的实现,在终端运行如下命令初始化问答模型索引数据:

php artisan scout:import "App\Models\Discussion"

这样就可以在应用中通过 Discussion::search('问题描述')->paginate($pageSize, 'page', $page) 进行问答模型的搜索了。

你可能感兴趣的:(php,搜索多模型)