[教程二] 写一个搜索:解决搜索结果高亮问题,使用 Laravel Scout,Elasticsearch,ik 分词

代码:https://github.com/lijinma/laravel-scout-e... ,欢迎 Star

接着第一篇:博客:[教程一] 写一个搜索:使用 Laravel Scout,Elasticsearch,ik 分词 ,在这篇文章中,我要实现“搜索结果高亮“。

你先看看搜索结果高亮的效果:

http://scout.lijinma.com/search?query=%E8%...

我们知道 Laravel Scout 的 search 结果直接是一个一个对象,并没有提供搜索结果高亮功能,这个时候我们有两条路可以解决我们的问题:

  1. 搜索的时候不使用 Scout 提供的 search 方法,直接调用原生的 ElasticSearch 接口来做,搜索后自己组装需要的属性,这肯定是一条路,在我们没有 Laravel Scout 的时候确实也是这么做的,但这样写虽然带来了灵活性,但是我们要自己写的代码还是有点多。
  2. 第二条路,就是我们想办法修改 Scout ElasticSearch Engine,来满足我们的高亮需求。

我选择了第二条路:

1. 自定义 ElasticSearch Engine

首先,创建文件:app/Libraries/EsEngine.php
继承 ElasticsearchEngine,按照我们的需求添加或修改代码:

<?php namespace App\Libraries;

use Laravel\Scout\Builder;
use ScoutEngines\Elasticsearch\ElasticsearchEngine;

class EsEngine extends ElasticsearchEngine
{
    public function search(Builder $builder)
    {
        $result =  $this->performSearch($builder, array_filter([
            'numericFilters' => $this->filters($builder),
            'size' => $builder->limit,
        ]));
        return $result;
    }

    /**
     * Perform the given search on the engine.
     *
     * @param  Builder  $builder
     * @return mixed
     */
    protected function performSearch(Builder $builder, array $options = [])
    {
        $params = [
            'index' => $this->index,
            'type' => $builder->model->searchableAs(),
            'body' => [
                'query' => [
                    'bool' => [
                        'must' => [
                            [
                                'query_string' => [
                                    'query' => "{$builder->query}",
                                ]
                            ]
                        ]
                    ]
                ],
            ]
        ];
        /**
         * 这里使用了 highlight 的配置
         */
        if ($builder->model->searchSettings
            && isset($builder->model->searchSettings['attributesToHighlight'])
        ) {
            $attributes = $builder->model->searchSettings['attributesToHighlight'];
            foreach ($attributes as $attribute) {
                $params['body']['highlight']['fields'][$attribute] = new \stdClass();
            }
        }

        if (isset($options['from'])) {
            $params['body']['from'] = $options['from'];
        }

        if (isset($options['size'])) {
            $params['body']['size'] = $options['size'];
        }

        if (isset($options['numericFilters']) && count($options['numericFilters'])) {
            $params['body']['query']['bool']['must'] = array_merge($params['body']['query']['bool']['must'],
                $options['numericFilters']);
        }

        return $this->elastic->search($params);
    }

    /**
     * Map the given results to instances of the given model.
     *
     * @param  mixed  $results
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return Collection
     */
    public function map($results, $model)
    {
        if (count($results['hits']['total']) === 0) {
            return Collection::make();
        }

        $keys = collect($results['hits']['hits'])
            ->pluck('_id')->values()->all();

        $models = $model->whereIn(
            $model->getKeyName(), $keys
        )->get()->keyBy($model->getKeyName());

        return collect($results['hits']['hits'])->map(function ($hit) use ($model, $models) {
            $one = $models[$hit['_id']];
            /**
             * 这里返回的数据,如果有 highlight,就把对应的  highlight 设置到对象上面
             */
            if (isset($hit['highlight'])) {
                $one->highlight = $hit['highlight'];
            }
            return $one;
        });
    }

}

2. 替换掉 Scout 的 Engine 为我们创建的 EsEngine

修改 app/Providers/AppServiceProvider.php


use App\Libraries\EsEngine;
use Laravel\Scout\EngineManager;
use Elasticsearch\ClientBuilder as ElasticBuilder;

    public function boot()
    {
        resolve(EngineManager::class)->extend('es', function($app) {
            return new EsEngine(ElasticBuilder::create()
                ->setHosts(config('scout.elasticsearch.hosts'))
                ->build(),
                config('scout.elasticsearch.index')
            );
        });
    }

3. 添加 Model 需要的属性

为了方便一个 Model 是否在搜索的时候使用高亮,我们把这些代码抽出来写一个 Trait,使用的时候再 use
创建 Trait app/Libraries/EsSearchable.php

<?php namespace App\Libraries;

trait EsSearchable
{
    public $searchSettings = [
        'attributesToHighlight' => [
            '*'
        ]
    ];

    public $highlight = [];
}

Post.php Model 中使用 trait:

    use Searchable, EsSearchable;

4. 修改 view 满足我们的显示

修改 resources/views/search.blade.php ,查看高亮。

@extends('layouts.main')
@section('content')
    <div class="row">
        <div class="col-md-12">
            <form action="/search">
                <div class="input-group">
                    <input type="text" class="form-control h50" name="query" placeholder="关键字..." value="{{ $q }}">
                    <span class="input-group-btn"><button class="btn btn-default h50" type="submit" type="button"><span class="glyphicon glyphicon-search"></span></button></span>
                </div>
            </form>
        </div>
    </div>
    @if($q)
        <div class="row">
            <div class="col-md-12">
                <div class="panel panel-default list-panel search-results">
                    <div class="panel-heading">
                        <h3 class="panel-title ">
                            <i class="fa fa-search"></i> 关于 “<span class="highlight">{{ $q }}</span>” 的搜索结果, 共 {{ $paginator->total() }} 条
                        </h3>
                    </div>

                    <div class="panel-body ">
                        [@foreach](https://learnku.com/users/5651)($paginator as $post)
                            <div class="result">
                                <h2 class="title">
                                    <a href="{{ $post->url }}" target="_blank">
                                        @if (isset($post->highlight['title']))
                                            [@foreach](https://learnku.com/users/5651) ($post->highlight['title'] as $item)
                                                {!! $item !!}
                                            @endforeach
                                        @else
                                            {{ $post->title }}
                                        @endif
                                    </a>
                                </h2>
                                <div class="info">
                                </div>
                                <div class="desc">
                                    @if (isset($post->highlight['content']))
                                        [@foreach](https://learnku.com/users/5651) ($post->highlight['content'] as $item)
                                            ......{!! $item !!}......
                                        @endforeach
                                    @else
                                        {{ mb_substr($post->content, 0, 150) }}......
                                    @endif
                                </div>
                                <hr>
                            </div>
                        @endforeach
                    </div>
                    {{ $paginator->links() }}
                </div>
            </div>
        </div>
    @else
        <div class="row text-center">
            <div class="col-md-12">
                <br>
                <h2>你会搜索到什么?</h2>
                <br>
                <p>学习学习再学习公众号所有文章</p>
            </div>
        </div>
    @endif
@endsection

样式可以抄一抄 Laravel China 的搜索结果...
public/css/main.css

#app {
    margin-top: 20px;
}
.h50 {
    height: 50px;
}
.search-results {
    margin-top: 20px;
    padding: 20px;
    line-height: 25px;
}

.search-results .panel-heading h3 {
    color: #696969;
    font-size: 15px;
    margin-bottom: 12px;
}

.search-results a {
    color: #333;
}

.search-results .result {
    margin-bottom: 20px;
}

.search-results .user.result {
    margin-top: 8px;
    margin-bottom: 0px;
}

.search-results .result em {
    color: #EB5424;
    font-style: normal;
}

.search-results .result .title {
    font-size: 18px;
}

.search-results .result .title .badge {
    background: #EBEDEE;
    color: #9A9DA0;
    font-weight: normal;
    font-size: 12px;
    margin-left: 4px;
}

.search-results .result .info {
    margin-bottom: 6px;
    font-size: 14px;
}

.search-results .result .info .url a {
    color: #23863F;
}

.search-results .result .info .date {
    color: #999;
    margin-left: 8px;
}

.search-results .result .desc {
    color: #666;
    font-size: 14px;
    word-break: break-all;
}

.search-results .result .desc em {
    color: #F86334;
}

.search-results .user .info {
    margin-top: 4px;
    font-size: 14px;
}

.search-results .user .info.number {
    color: #666;
    font-size: 13px;
}

.search-results em {
    color: #e07b7a;
}

.search-results .role-label {
    display: inline-block;
    position: absolute;
}

.search-results .role-label a.label {
    font-size: 85%;
    font-weight: 100;
    padding: 0.2em 1em .2em;
    position: relative;
    margin: 8px;
    color: #fff;
}

.search-results .user-info {
    padding-top: 8px;
    padding-left: 8px;
}

.search-results hr {
    margin-top: 15px;
    margin-bottom: 15px;
}

.search-results .list-panel .panel-body {
    padding: 0px;
}

好了,这样就解决了高亮的问题了。

别忘记 Star 我的 demo 哦,我要尽可能写的清楚。https://github.com/lijinma/laravel-scout-e...

我为什么这么做?

因为我看别人也这么做: https://github.com/laravel/scout/pull/19

本作品采用《CC 协议》,转载必须注明作者和本文链接
写文字大部分时候是因为我希望能帮助到你,小部分时候是想做总结或做记录。我的微信是 lijinma,希望和你交朋友。 以下是我的公众账号,会分享我的学习和成长。
本帖由 Summer 于 7年前 加精
lijinma
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 30
Summer

高产,要把一个月文章都写了吗

7年前 评论
lijinma

@Summer 哈哈,过年的时候就想写,无奈一直拖延,这次写完,还有一篇,等下发。

7年前 评论

就是喜欢看着你折腾

7年前 评论
lijinma

@JokerLinly 因为这是我这样的小渣渣的进击之路。

7年前 评论

虽然我没有认真看,还是点个赞,写文章这件事情坚持下来挺不容易的。
大学的时候坚持写,毕业工作之后基本一个月一篇了。

7年前 评论

这个教程上没有说明 AppServiceProvider 里面的添加逻辑。

7年前 评论
lijinma

@RryLee 那也很厉害啊。

7年前 评论
lijinma

@jxlwqq 太感谢了。。谢谢你。。。已经改了。

7年前 评论

@lijinma 哈哈哈,过奖了,你这个是高产似母猪,:smile:

7年前 评论
lijinma

@RryLee 哈哈,谢谢夸奖。

7年前 评论

@lijinma 感觉还是漏了一点什么,我这边测试高亮无效果。

7年前 评论
lijinma

@jxlwqq 替换掉 Scout 的 Engine 为我们创建的 EsEngine了吗?

7年前 评论

@lijinma AppServiceProvider 里面的逻辑已经加了。对了,我的版本是 lavarel 5.3 的,是否是版本的原因。

7年前 评论

还有个建议。使用md5(url)来判断是否抓过比较好。长度能比url短,而且位数也统一

7年前 评论

ElasticSearch默认的用户名和密码
用户名:elastic
密码:changeme
如果要使用本demo需要跑通.需要去掉授权的(实际场景不建议去掉api的auth验证)。也就是去掉x-pack.
命令:./bin/elasticsearch-plugin remove x-pack
然后重启elasticsearch

7年前 评论

在提一个bug。如果在搜索中输入带有/,/hello就会报错

7年前 评论
lijinma

@jxlwqq 5.3应该没问题,你重新 index 下数据,flush 一下,再 index 一下?

7年前 评论
lijinma

@Will 非常感谢你。我修复一下。

7年前 评论

@lijinma 不客气。也感谢你提供了这个demo。可以作为Laravel Scout学习的案例。

7年前 评论

@lijinma 好的,谢谢,我再试试。

7年前 评论

现在搜索可以了,按着这个教程一步步写完还是没有高亮显示,索引也flush了也重新import了,是不是漏掉了什么?

6年前 评论

可以正常搜索,但是没有高亮显示,发现EsEngin中重写的方法都没有执行,是不是哪里少配置了?

6年前 评论

@lijinma 我感觉替换你写的那个搜索引擎有问题啊,没有成功,高亮没效果

6年前 评论

@JiberBoom 没有高亮效果是因为博主在App\Providers\AppServiceProvider里重新定义了搜索引擎为es,那么在scout.php里面的配置就要改成'driver' => env('SCOUT_DRIVER', 'es')

6年前 评论

@JohnsonChung 你这样做成功了没

6年前 评论

@JohnsonChung 对滴 完美运行

6年前 评论
he21cn 4年前

file

这个 total 本来是一个整型,应该去掉 count() 方法。

5年前 评论

老铁,如何实现多表链接查询?

5年前 评论

太感谢了,照着你的思路写,可以了 :smile:

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!