ShiningDan的博客

博客优化之本地搜索

在本博客中,本来想使用 bing 搜索或者 Google 搜索来实现站内搜索的功能,尽管我已经设置了在发布和更新文章的时候通知不同的搜索引擎,但是它们什么时候更新索引却不能受到我的控制,所以,我最后选择使用 Elasticsearch 来实现站内搜索的功能。

这篇文章很多地方的实现参考了 使用 Elasticsearch 实现博客站内搜索 | QuQu QuQu 老师的这篇文章,在此表示感谢。

安装 Elasticsearch

下载和安装 Elasticsearch

下载和安装 Elasticsearch 的步骤很简单,就是安装包下载下来解压就行,具体的步骤可以查看官方文档 Installation Elasticsearch

然后运行 <es>/bin/elasticsearch 脚本,就可以启动 Elasticsearch 了。

使用 curl 来验证 Elasticsearch 是否运行成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -XGET http://127.0.0.1:9200/\?pretty

{
"name" : "Yy0Pe-N",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "VnbxeqF_QpG9Xv62p1nePA",
"version" : {
"number" : "5.4.0",
"build_hash" : "780f8c4",
"build_date" : "2017-04-28T17:43:27.229Z",
"build_snapshot" : false,
"lucene_version" : "6.5.0"
},
"tagline" : "You Know, for Search"
}

如果返回值和上面类似,就是运行成功了。

在配置的时候遇到了以下几个错误:

JVM 内存不足

Elasticsearch 默认情况下最小需要 2G 内存才能运行,但是我的虚拟机最多只有 2G 内存,所以,需要设置以下 Elasticsearch 启动的内存大小:

1
2
3
4
<es-root>/config/jvm.options

-Xms1g
-Xmx1g

将对应最小启动内存设置成 1G,就可以启动了

Elasticsearch 不能 root 运行

Elasticsearch 由于安全的原因,在 >= 5.0 版本之后,不允许使用 root 用户来运行,现有的解决方法有:

修改源代码并且编译来支持使用 root 用户:How to run Elasticsearch 5.2.1 as root user in linux machine

或者创建一个新的用户来执行 Elasticsearch,我这里选择的是第二种方法。CENTOS安装ElasticSearch

配置启动和关闭脚本

后面为了使用 root 用户来运行 Elasticsearch,我最后使用了 docker 来进行 Elasticsearch。

具体安装 docker Elasticsearch 的步骤可以参考 Install Elasticsearch

在官网中下载的 Elasticsearch 默认开启了 X-Pack 权限验证,我在运行 docker 的时候取消了该权限验证,具体可以参考 Security Settings

1
docker run -p 9200:9200 -e "http.host=0.0.0.0" -e "transport.host=127.0.0.1" -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" -e "xpack.security.enabled=false" -e "plugins=/usr/share/elasticsearch/plugin" docker.elastic.co/elasticsearch/elasticsearch:5.4.0

安装 IK Analysis

Elasticsearch 自带的分词器会粗暴地把每个汉字直接分开,没有根据词库来分词。为了处理中文搜索,还需要安装中文分词插件。所以需要使用 IK Analysis for Elasticsearch 来支持 Elasticsearch 的中文分词能力。

具体的下载以及安装过程可以参考 IK Analysis for Elasticsearch 文档中的内容。

首先查看 Elasticsearch 的版本号,然后在 elasticsearch-analysis-ik | release 中找到对应的版本下载。

查看版本号的方法,就和上面 使用 curl 来验证 Elasticsearch 是否运行成功 的方法相同,其中,返回值里面有一个

1
"number" : "5.4.0"

表示当前 Elasticsearch 的版本,就可以下载对应的 elasticsearch-analysis-ik 版本。

根据要求,解压 target/releases/elasticsearch-analysis-ik-{version}.zipyour-es-root/plugins/ik

然后重启 Elasticsearch。

在启动的时候,看到下面的信息,就说明 analysis-ik 安装成功

1
[o.e.p.PluginsService     ] [Yy0Pe-N] loaded plugin [analysis-ik]

在 elasticsearch-analysis-ik 中有两种不同的分词方法,根据官方分档的解释,分为:

  • ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合;
  • ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。

我在使用的时候使用的是最细粒度的拆分

创建 Mapping 和 Query DSL

在使用 Elastic 存储需要被检索的数据之前,我们要先为其创建一个 Index,然后在该 Index 下定义 Document 中数据的类型,所使用的 analyzer、是否索引等属性。创建 Mapping 的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
curl -XDELETE 'http://localhost:9200/articles'

curl -XPUT 'http://localhost:9200/articles' -d '
{
"settings": { "number_of_shards": 1 },
"mappings" : {
"article": {
"properties": {
"title": {
"type": "text",
"term_vector": "with_positions_offsets",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"content": {
"type": "text",
"term_vector": "with_positions_offsets",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"link": {
"type": "text"
},
"categories": {
"type": "keyword",
"index" : "true"
},
"create_date": {
"type" : "date",
"index" : "not_analyzed"
}
}
}
}
}'

其中,相关配置的含义为:

1
"term_vector": "with_positions_offsets" // 保存值和token位置信息和Token的offset

设置 titlecontent,使用 ik_max_word 作为分词器。然后将 create_datacategories 设置不分词。

然后在保存新的文章和更新文章的时候,可以将数据写入到 Elasticsearch 以供搜索:

1
2
3
4
5
6
7
8
9
10
11
12
res.es.index({
index: 'articles',
type: 'article',
id: _article.link.slice(6),
body: {
title: _article.title,
content: _article.md,
link: _article.link.slice(6),
categories: _article.categories,
create_data: _article.meta.createAt
}
});

在我们的博客中,需要实现对于 contenttitlelinkcategories 的复杂搜索,并且为不同检索区域的内容涉及不同的权重,具体的权重以及复杂检索如下,参考的是 QuQu 老师的检索策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
index : 'articles',
type : 'article',
from : start,
body : {
query : {
dis_max : {
queries : [
{
match : {
title : {
query : s,
minimum_should_match : '50%',
boost : 4,
}
}
}, {
match : {
content : {
query : s,
minimum_should_match : '75%',
boost : 4,
}
}
}, {
match : {
categories : {
query : s,
minimum_should_match : '100%',
boost : 2,
}
}
}, {
match : {
link : {
query : s,
minimum_should_match : '100%',
boost : 1,
}
}
}
],
tie_breaker : 0.3
}
},
highlight : {
pre_tags : ['<b>'],
post_tags : ['</b>'],
fields : {
title : {},
content : {},
}
}
}
}

因为我的原文没有保存只有文字格式的,而是直接编辑 Markdown 来呈现文章的,所以,我在检索的时候,直接检索的是原文的 Markdown 文件。

当然,检索得到的 Markdown 格式不适合直接在浏览器上进行显示,所以需要将其中的 Markdown 语法相关的标记都删除,这里我使用的是 remove-markdown 这个库来实现将标记溢出的功能。当然,这个库中有一些处理的地方不合我的要求,比如:

  1. Markdown 原文中,如果搜索出来的结果包含 <!--more--> 标记,则通过该库的编辑后,会成为 !--more-- 格式
  2. 通过 Elasticsearch 中, higtlight 返回的字段有 <b></b> 标签的包裹,但是通过该库的编辑后,会变成 b/b

所以针对以上两点,我也使用了正则表达式进行了修正:

1
2
3
4
5
6
7
8
9
10
11
12
13
(value) => {
let reg = /b([\S]{1,20}?)\/b/g;
let moreReg = /!--more--/g;
value.hits.hits.map((a) => {
a.highlight.content = a.highlight.content.map((c) => {
c = removeMd(c);
c = c.replace(moreReg, () => "");
return c.replace(reg, (v, p1) => {
return '<b>'+p1+'</b>';
})
});
return a;
}

参考