[教程一] 写一个搜索:使用 Laravel Scout,Elasticsearch,ik 分词

文字太长,你可以直接看代码:

https://github.com/lijinma/laravel-scout-e...

过年的时候,我在家写了一个小网站,名字叫“笑来搜”,整个过程是这样的:

  1. 开始使用 tntsearch ,非常小巧,依赖也少,很喜欢。
  2. 不过用了一下发现 tntsearch 没有配套的中文分词,有一个小伙子写了一个,但是很不完善。
  3. 最终还是选择了 ElasticSearch,虽然相对 tntsearch 更重一点。
  4. ElasticSearch 中的 ik 分词插件简单好用,而且非常容易扩展词库。

笑来搜 上线后,好几个朋友询问如何可以简单的实现一个类似的搜索网站,所以我就抽时间做了一个类似的 Demo,代码在 https://github.com/lijinma/laravel-scout-e... ,对你有帮助的请 Star,这个 Demo 至少有这两个优点:

  1. 尽可能写清楚安装中的每一个步骤,我假设你是一名新手。
  2. 这个 Demo 直接跑在了我的服务器上,你可以直观的玩起来。http://scout.lijinma.com/search

下面是整个教程:

首先:我们要做一个什么?

我们要做的东西比较简单,就是把一个公众账号的文章拉下来,然后实现所有文章的“标题”和“内容”的搜索,在项目中我选择了李笑来老师的”学习学习再学习“中的50篇文章。

先看看要做的东西的样子: http://scout.lijinma.com/search

第一步:安装好 Laravel 5.4

不管你是使用 homestead,还是 valet,还是 docker ,还是直接自己本地环境搭建,反正第一步你要把 Laravel 5.4 项目跑起来,可以看到 welcome 的页面。

这里分享一下我是如何开发的,一般来说,只有我一个人开发的简单的 Laravel 项目,我都不使用 homestead 或者 valet 或者 docker 跑的,我直接在 Mac 本地跑,Mac 上只需要装一个 mysql,然后开发调试的时候直接使用 php artisan serve,总体来说效率比较高,配置快。

第二步:配置

配置数据库

create database laravel_scout_elastic_demo;

安装 ElasticSearch Scout Engine 包

$ composer require tamayo/laravel-scout-elastic

安装这个包的时候,顺便就会装好 Laravel Scout,我们 publish 一下 config

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

添加对应的 ServiceProvider:

//app.php ... Laravel\Scout\ScoutServiceProvider::class, ScoutEngines\Elasticsearch\ElasticsearchProvider::class, ...

安装 Goutte Client

我们需要通过公众号文章的 url 爬到文章的标题和内容,所以需要安装这个 库:

composer require fabpot/goutte

第三步:安装 ElasticSearch

因为我们要使用 ik 插件,在安装这个插件的时候,如果自己想办法安装这个插件会浪费你很多精力。

所以我们直接使用项目: https://github.com/medcl/elasticsearch-rtf

当前的版本是 Elasticsearch 5.1.1,ik 插件也是直接自带了。

安装好 ElasticSearch,跑起来服务,测试服务安装是否正确:

$ curl http://localhost:9200 { "name" : "Rkx3vzo", "cluster_name" : "elasticsearch", "cluster_uuid" : "Ww9KIfqSRA-9qnmj1TcnHQ", "version" : { "number" : "5.1.1", "build_hash" : "5395e21", "build_date" : "2016-12-06T12:36:15.409Z", "build_snapshot" : false, "lucene_version" : "6.3.0" }, "tagline" : "You Know, for Search" }

如果正确的打印以上信息,证明 ElasticSearch 已经安装好了。

接着你需要查看一下 ik 插件是否安装(请在你的 ElasticSearch 文件夹中执行):

$ ./bin/elasticsearch-plugin list analysis-ik

如果出现 analysis-ik,证明 ik 已经安装。

第四步,开始写代码:

添加 InitEs 命令,初始化 ES 的一些数据

$ php artisan make:command InitEs

InitEs.php 代码如下,主要做了两件事情:

  1. 创建对应的 index
  2. 创建一个 template,你可以通过下面的链接了解一下什么是 Index template
    https://www.elastic.co/guide/en/elasticsea...
<?php namespace App\Console\Commands; use GuzzleHttp\Client; use Illuminate\Console\Command; class InitEs extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'es:init'; /** * The console command description. * * @var string */ protected $description = 'Init es to create index'; /** * Create a new command instance. * */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { $client = new Client(); $this->createTemplate($client); $this->createIndex($client); } protected function createIndex(Client $client) { $url = config('scout.elasticsearch.hosts')[0] . ':9200/' . config('scout.elasticsearch.index'); $client->put($url, [ 'json' => [ 'settings' => [ 'refresh_interval' => '5s', 'number_of_shards' => 1, 'number_of_replicas' => 0, ], 'mappings' => [ '_default_' => [ '_all' => [ 'enabled' => false ] ] ] ] ]); } protected function createTemplate(Client $client) { $url = config('scout.elasticsearch.hosts')[0] . ':9200/' . '_template/rtf'; $client->put($url, [ 'json' => [ 'template' => '*', 'settings' => [ 'number_of_shards' => 1 ], 'mappings' => [ '_default_' => [ '_all' => [ 'enabled' => true ], 'dynamic_templates' => [ [ 'strings' => [ 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'text', 'analyzer' => 'ik_smart', 'ignore_above' => 256, 'fields' => [ 'keyword' => [ 'type' => 'keyword' ] ] ] ] ] ] ] ] ] ]); } } 

创建 Post 表,存放公众号的文章

php artisan make:migration create_posts_table

代码:

<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->text('url'); $table->string('author', 64)->nullable()->default(null); $table->text('title'); $table->longText('content'); $table->dateTime('post_date')->nullable()->default(null); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } }

在数据库中创建表:

$ php artisan migrate

添加 Post Model:

$ php artisan make:model Post

代码:

<?php namespace App; use Illuminate\Database\Eloquent\Model; use Laravel\Scout\Searchable; /** * Class Post * @package App * @property string $url * @property string $author * @property string $content * @property string $title * @property string $post_date * @property string $created_at * @property string $updated_at */ class Post extends Model { use Searchable; protected $table = 'posts'; protected $fillable = [ 'url', 'author', 'title', 'content', 'post_date' ]; public function toSearchableArray() { return [ 'title' => $this->title, 'content' => $this->content ]; } }

添加一个命令 ImportPosts,通过此命令去爬去数据,并导入到 Post 表中。

$ php artisan make:command ImportPosts

代码:

<?php namespace App\Console\Commands; use App\Libraries\WechatPostSpider; use App\Post; use Goutte\Client; use Illuminate\Console\Command; class ImportPosts extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'posts:import'; /** * The console command description. * * @var string */ protected $description = 'Import posts!'; /** * Create a new command instance. * */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { $client = new Client(); foreach (config('post-urls') as $url) { /** * 这里 url 可能需要索引,但是用 url 做唯一标示不太好,索引太大 */ if (Post::where('url', $url)->exists()) { continue; } $wechatPostSpider = new WechatPostSpider($client, $url); $this->savePost($wechatPostSpider); $this->info('create one post!'); } } protected function savePost(WechatPostSpider $wechatPostSpider) { Post::create([ 'url' => $wechatPostSpider->getUrl(), 'author' => $wechatPostSpider->getAuthor(), 'title' => $wechatPostSpider->getTitle(), 'content' => $wechatPostSpider->getContent(), 'post_date' => $wechatPostSpider->getPostDate(), ]); } }

此时,需要依赖两个文件,一个是 app/Libraries/WechatPostSpider.php,一个是 config/post-urls.php 配置文件。

WechatPostSpider.php 负责爬去数据

<?php namespace App\Libraries; use Goutte\Client; use Symfony\Component\DomCrawler\Crawler; /** * Created by PhpStorm. * User: lijinma * Date: 04/03/2017 * Time: 9:05 PM */ class WechatPostSpider { /** * @var Crawler|null */ protected $crawler; /** * @var string */ protected $url; /** * WechatPostSpider constructor. * @param Client $client * @param $url */ public function __construct(Client $client, $url) { $this->url = $url; $this->crawler = $client->request('GET', $url); } /** * @return string */ public function getTitle() { return trim($this->crawler->filter('title')->text()); } /** * @return string */ public function getContent() { return trim($this->crawler->filter('.rich_media_content')->text()); } /** * @return string */ public function getAuthor() { return trim($this->crawler->filter('#post-date')->nextAll()->text()); } /** * @return string */ public function getPostDate() { return $this->crawler->filter('#post-date')->text(); } /** * @return string */ public function getUrl() { return $this->url; } }

post-urls.php 存储需要爬取的公众号文章 urls,这里只列了一条

<?php return [ "http://mp.weixin.qq.com/s?__biz=MzAxNzI4MTMwMw==&mid=2651630953&idx=1&sn=9c4d8f2b4df2605fdaa1338303acc908&chksm=801ff511b7687c07303220a0c105d979f1a4a5db45689c95111a6c6ec2f5a6c0c6cecea88ba0&scene=4#wechat_redirect", ];

添加 PostController

$ php artisan make:controller PostController

PostController.php 代码:

<?php namespace App\Http\Controllers; use App\Post; use Illuminate\Http\Request; class PostController extends Controller { public function search(Request $request) { $q = $request->get('q'); $paginator = []; if ($q) { $paginator = Post::search($q)->paginate(); } return view('search', compact('paginator', 'q')); } }

PostController.php 需要依赖 view 文件,我们创建一个 resources/views/layouts/main.blade.php,一个 resources/views/search.blade.php

resources/views/layouts/main.blade.php 代码:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" id="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Styles --> <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> <link href="/css/main.css" rel="stylesheet"> <!-- Scripts --> <script> window.Laravel = {!! json_encode([ 'csrfToken' => csrf_token(), ]) !!}; </script> </head> <body> <div id="app"> <div class="container"> <div class="row"> <div class="col-md-12"> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">Laravel Scout Elastic Demo</a> </div> </div><!-- /.container-fluid --> </nav> </div> </div> @yield('content') </div> </div> <!-- Scripts --> <script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> </body> </html>

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="q" 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($paginator as $post) <div class="result"> <h2 class="title"> <a href="{{ $post->url }}" target="_blank"> {{ $post->title }} </a> </h2> <div class="info"> </div> <div class="desc"> {{ mb_substr($post->content, 0, 150) }}...... </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

现在我们的代码已经写完了,但是缺少一个功能,搜索结果如何高亮(highlight) 呢?

本作品采用《CC 协议》,转载必须注明作者和本文链接
写文字大部分时候是因为我希望能帮助到你,小部分时候是想做总结或做记录。我的微信是 lijinma,希望和你交朋友。 以下是我的公众账号,会分享我的学习和成长。
本帖由 Summer 于 8年前 加精
lijinma
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 59
lijinma

这样复制代码,好像有点略长。。

8年前 评论
Summer

好文章 :+1:

文章里面不要使用 h1 标签,就是这种:

file

SEO 对层级关系会敏感,最顶层也就是 H1 是被页面的标题占用了。所以文章里的层级关系只能是 h2 ~ h6 ,他们都隶属于此篇文章 :smile_cat:

8年前 评论
lijinma

@Summer 明白了,Summer,马上改好

8年前 评论

真的不错,好评!

8年前 评论

马上就想按着你的教程折腾一下。:kissing_heart:

8年前 评论
lijinma

@dodo 谢谢。

8年前 评论
lijinma

@JokerLinly 快折腾下,正好我看看我教程写的有没有问题。。

任何问题叫我,秒回。

8年前 评论

最近在怼 opensearch,已经出了一个内部版本的 SDK,等成熟了可以交流下

8年前 评论
lijinma

@RryLee 恩恩,期待哈

8年前 评论

我前端时间刚使用es做实时日志的收集和查询,赞一个!

8年前 评论
Artisan

之前没有听说过ElasticSearch,看过这三篇文章后去查阅了相关资料,感觉很厉害.这个软件如果用在普通的企业站做站内搜索会不会有些大材小用呢?似乎很吃内存,要2G以上是吗?

8年前 评论
lijinma

@Artisan 还好,我现在内存紧张,es 使用 512M,其实多个企业站用一个搜索服务就行。搜索不那么频繁的话。

8年前 评论
Artisan

@lijinma 您所谓的搜索服务是 Algolia 这种服务吗?还是别的什么,搜索确实不多.

8年前 评论

好厉害的东西,金马哥。

8年前 评论
lijinma

@Artisan 恩,比如 Algolia,比如阿里云,腾讯云都有提供,如果你搜索不多,数据不多,价格不贵。

如果想自己折腾,就自己搭建 ElasticSearch ,其实也快。

8年前 评论
lijinma

@张铁林 哈哈,谢谢铁林。。

8年前 评论

报错 Type error: Too few arguments to function Illuminate\Support\Manager::createDriver()

8年前 评论
lijinma

@ohr445321
不够详细,无法判断是哪的问题

8年前 评论

@ohr445321 你config里面没有scout.php! php artisan es:init && php artisan post:import 应该没执行! 我开始也跟你一样的错误

8年前 评论

@lijinma 谢谢提供这么好的技术文章! 我在爬完公众号数据后 数据库也有了数据!! 不知道为什么 一条数据都搜索不到
关于 “北京” 的搜索结果, 共 0 条
关于 “十分钟读完《刻意练习》” 的搜索结果, 共 0 条

8年前 评论

@lijinma 是不是没有把posts表里面的title content放到索引里去?

8年前 评论
Silencewj

ElasticSearch 安装3%就动不了。搞了一下午还是不行

8年前 评论
Silencewj

@JokerLinly 金马的搜索 折腾的怎样? ElasticSearch安装上了吗

8年前 评论
Silencewj

@Artisan 金马的搜索 折腾的怎样? ElasticSearch安装上了吗

8年前 评论

@Silence9312 不带你这样批量发问的。。可以同时 @ 两个人嘛~
参考下 5 分钟配置并使用 ElasticsearchElasticsearch,为了搜索

8年前 评论
Silencewj

@JokerLinly 多谢Joker 哈哈 现在就去试试 不过下载的速度老慢了

8年前 评论
Destiny

不得不赞。完美。

8年前 评论
Destiny

@ahkxhyl 初始化 ES init 的配置没运行吧。

8年前 评论

按照步骤走了一遍, 但是搜索数字和英文有结果, 搜索汉字没有结果, 不支持中文?? 但是单独执行elasticsearch 搜索接口是能够获取到数据的,请问十什么原因呢??

8年前 评论

@lijinma 按照步骤走了一遍, 但是搜索数字和英文有结果, 搜索汉字没有结果, 不支持中文?? 但是单独执行elasticsearch 搜索接口是能够获取到数据的,请问十什么原因呢??

file

file

8年前 评论
lijinma

@珠珠 - 。- debug 下。。。

8年前 评论

@Destiny 都是按教程一步一步来的 代码都没测试就放上来 反馈也不回信息~

8年前 评论

提示这个报错:
[GuzzleHttp\Exception\ConnectException]
cURL error 6: Couldn't resolve host 'localhost:9200' (see http://curl.haxx.se/libcurl/c/libcurl-erro...)
谷歌了一遍还是没找到解决方案

8年前 评论

mac下一切正常 部署到服务器之后英文数字有结果...中文没结果啊 是编码问题?

8年前 评论

@珠珠 同样问题啊 你解决了吗

8年前 评论

Call to undefined method GuzzleHttp\Psr7\Response::filter(), 执行抓取数据的命令的时候报这个错。

8年前 评论

@珠珠 我也有碰到这个问题,这个需要先执行es:init 配置,然后再使用scout:import 就可以进行中文的搜索了

8年前 评论

@珠珠 我也是中文搜不到和你一样 你的问题解决了吗

7年前 评论

ES和Mysql之间的数据同步你是怎么解决的;做到增量同步好像有点困难

7年前 评论

你好,根据你的github源码 运行 php artisan es:init 报错如下

file

7年前 评论

添加一个配置::文中漏了的坑scout.php中

'driver' => env('SCOUT_DRIVER', 'elasticsearch'), agloin 改成elasticsearch 驱动 并且加入 'elasticsearch' => [ 'index' => env('ELASTICSEARCH_INDEX', 'laravel_search'), 'hosts' => [ env('ELASTICSEARCH_HOST', 'http://127.0.0.1:9200'), ], ],
7年前 评论

金老师。求助问题,刚刚跑通你的demo,
但是发现问题,
我把posts表的数据全部删掉后去搜索,发现搜不到,而且报错,但是报错信息里存在ES的数据,
怎么把报错的信息变为正常信息返回
当我把ES里的索引全删后,搜索不会报错。
发现搜索的驱动还是MYSQL,跟ES没什么关系
疑惑中,求解
file

file

7年前 评论
易水 4年前

{"_index":"localhost","_type":"course","_id":"2","found":false}
导入模型数据成功了 而且显示出来表的ID数 但是 浏览器输入http://127.0.0.1:9200/localhost/course/2 查询时是为空

已解决

7年前 评论

@zbc123 请问你是怎么解决的 ?我也刚遇到这个问题

7年前 评论

前辈,您好,看了您的文章,已经基本入门elasticsearch,但是现在遇到一个问题,烦请指点一下。我有两个表,分别为:posts [文章表] comments[评论表 ] ,一篇文章有多条评论。当我在输入框输入搜索关键词时,我希望,也能够将comments 表中的评论内容也加入搜索的范围,我在 App\Models\Post 中的 toSearchableArray() 方法如何设置呢?烦请指点一下。

7年前 评论

@ittiro 我的好像是在模型里指定对应的表就好了 。

7年前 评论

elasticsearch 6.x不再支持一个索引下多个type,(Multiple types in the same index really shouldn't be used all that often and one of the few use cases for types is parent child relationships.)那如何对多表进行搜索呢?@lijinma

7年前 评论

@zbc123 你的es是哪个版本?是对单个模型进行了导入吧,多个模型搜索如何解决的?

7年前 评论

@lijinma 问一下楼主我这为啥中英文啥也搜不出来呢?
file

file

6年前 评论

请问使用paginate时报错:说不支持types ,如何解决啊 是因为我es版本为7.2,版本高的原因吗?

6年前 评论
suisai337 5年前

我想问一下,这个搜索是不是只支持搜索词全文模糊匹配,我的搜索词不会自动分词,比如说,搜“中华人民共和国”,他就搜不出只带“中华”或者“共和国”的内容。

4年前 评论