[教程一] 写一个搜索:使用 Laravel Scout,Elasticsearch,ik 分词
文字太长,你可以直接看代码:
https://github.com/lijinma/laravel-scout-e...
过年的时候,我在家写了一个小网站,名字叫“笑来搜”,整个过程是这样的:
- 开始使用
tntsearch
,非常小巧,依赖也少,很喜欢。 - 不过用了一下发现
tntsearch
没有配套的中文分词,有一个小伙子写了一个,但是很不完善。 - 最终还是选择了
ElasticSearch
,虽然相对tntsearch
更重一点。 ElasticSearch
中的ik
分词插件简单好用,而且非常容易扩展词库。
笑来搜 上线后,好几个朋友询问如何可以简单的实现一个类似的搜索网站,所以我就抽时间做了一个类似的 Demo,代码在 https://github.com/lijinma/laravel-scout-e... ,对你有帮助的请 Star,这个 Demo 至少有这两个优点:
- 尽可能写清楚安装中的每一个步骤,我假设你是一名新手。
- 这个 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 代码如下,主要做了两件事情:
- 创建对应的 index
- 创建一个 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 协议》,转载必须注明作者和本文链接
这样复制代码,好像有点略长。。
好文章 :+1:
文章里面不要使用 h1 标签,就是这种:
SEO 对层级关系会敏感,最顶层也就是 H1 是被页面的标题占用了。所以文章里的层级关系只能是 h2 ~ h6 ,他们都隶属于此篇文章 :smile_cat:
@Summer 明白了,Summer,马上改好
真的不错,好评!
马上就想按着你的教程折腾一下。:kissing_heart:
@dodo 谢谢。
@JokerLinly 快折腾下,正好我看看我教程写的有没有问题。。
任何问题叫我,秒回。
最近在怼 opensearch,已经出了一个内部版本的 SDK,等成熟了可以交流下
@RryLee 恩恩,期待哈
我前端时间刚使用es做实时日志的收集和查询,赞一个!
之前没有听说过ElasticSearch,看过这三篇文章后去查阅了相关资料,感觉很厉害.这个软件如果用在普通的企业站做站内搜索会不会有些大材小用呢?似乎很吃内存,要2G以上是吗?
@Chrisdowson :)
@Artisan 还好,我现在内存紧张,es 使用 512M,其实多个企业站用一个搜索服务就行。搜索不那么频繁的话。
@lijinma 您所谓的搜索服务是 Algolia 这种服务吗?还是别的什么,搜索确实不多.
好厉害的东西,金马哥。
@Artisan 恩,比如 Algolia,比如阿里云,腾讯云都有提供,如果你搜索不多,数据不多,价格不贵。
如果想自己折腾,就自己搭建 ElasticSearch ,其实也快。
@张铁林 哈哈,谢谢铁林。。
:+1:
很详细, 准备走一遍!!
报错 Type error: Too few arguments to function Illuminate\Support\Manager::createDriver()
@ohr445321
不够详细,无法判断是哪的问题
@ohr445321 你config里面没有scout.php! php artisan es:init && php artisan post:import 应该没执行! 我开始也跟你一样的错误
@lijinma 谢谢提供这么好的技术文章! 我在爬完公众号数据后 数据库也有了数据!! 不知道为什么 一条数据都搜索不到
关于 “北京” 的搜索结果, 共 0 条
关于 “十分钟读完《刻意练习》” 的搜索结果, 共 0 条
@lijinma 是不是没有把posts表里面的title content放到索引里去?
ElasticSearch 安装3%就动不了。搞了一下午还是不行
@JokerLinly 金马的搜索 折腾的怎样? ElasticSearch安装上了吗
@Artisan 金马的搜索 折腾的怎样? ElasticSearch安装上了吗
@Silence9312 不带你这样批量发问的。。可以同时 @ 两个人嘛~
参考下 5 分钟配置并使用 Elasticsearch 和 Elasticsearch,为了搜索
@JokerLinly 多谢Joker 哈哈 现在就去试试 不过下载的速度老慢了
不得不赞。完美。
@ahkxhyl 初始化 ES init 的配置没运行吧。
按照步骤走了一遍, 但是搜索数字和英文有结果, 搜索汉字没有结果, 不支持中文?? 但是单独执行elasticsearch 搜索接口是能够获取到数据的,请问十什么原因呢??
@lijinma 按照步骤走了一遍, 但是搜索数字和英文有结果, 搜索汉字没有结果, 不支持中文?? 但是单独执行elasticsearch 搜索接口是能够获取到数据的,请问十什么原因呢??
@珠珠 - 。- debug 下。。。
@Destiny 都是按教程一步一步来的 代码都没测试就放上来 反馈也不回信息~
提示这个报错:
[GuzzleHttp\Exception\ConnectException]
cURL error 6: Couldn't resolve host 'localhost:9200' (see http://curl.haxx.se/libcurl/c/libcurl-erro...)
谷歌了一遍还是没找到解决方案
mac下一切正常 部署到服务器之后英文数字有结果...中文没结果啊 是编码问题?
@珠珠 同样问题啊 你解决了吗
Call to undefined method GuzzleHttp\Psr7\Response::filter()
, 执行抓取数据的命令的时候报这个错。@珠珠 我也有碰到这个问题,这个需要先执行es:init 配置,然后再使用scout:import 就可以进行中文的搜索了
@珠珠 我也是中文搜不到和你一样 你的问题解决了吗
ES和Mysql之间的数据同步你是怎么解决的;做到增量同步好像有点困难
你好,根据你的github源码 运行 php artisan es:init 报错如下
添加一个配置::文中漏了的坑scout.php中
金老师。求助问题,刚刚跑通你的demo,

但是发现问题,
我把posts表的数据全部删掉后去搜索,发现搜不到,而且报错,但是报错信息里存在ES的数据,
怎么把报错的信息变为正常信息返回
当我把ES里的索引全删后,搜索不会报错。
发现搜索的驱动还是MYSQL,跟ES没什么关系
疑惑中,求解
{"_index":"localhost","_type":"course","_id":"2","found":false}
导入模型数据成功了 而且显示出来表的ID数 但是 浏览器输入http://127.0.0.1:9200/localhost/course/2 查询时是为空
已解决
@zbc123 请问你是怎么解决的 ?我也刚遇到这个问题
前辈,您好,看了您的文章,已经基本入门elasticsearch,但是现在遇到一个问题,烦请指点一下。我有两个表,分别为:posts [文章表] comments[评论表 ] ,一篇文章有多条评论。当我在输入框输入搜索关键词时,我希望,也能够将comments 表中的评论内容也加入搜索的范围,我在 App\Models\Post 中的 toSearchableArray() 方法如何设置呢?烦请指点一下。
写的太好了
@ittiro 我的好像是在模型里指定对应的表就好了 。
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
@zbc123 你的es是哪个版本?是对单个模型进行了导入吧,多个模型搜索如何解决的?
@lijinma 问一下楼主我这为啥中英文啥也搜不出来呢?

请问使用paginate时报错:说不支持types ,如何解决啊 是因为我es版本为7.2,版本高的原因吗?
@Summer 细啊
我想问一下,这个搜索是不是只支持搜索词全文模糊匹配,我的搜索词不会自动分词,比如说,搜“中华人民共和国”,他就搜不出只带“中华”或者“共和国”的内容。
厉害