ES

2/25/2022 es

# ElasticSearch

# 简介

Elasticsearch 是一个高度可伸缩的开源全文搜索引擎。Elasticsearch 让你可以快速、实时地存储、搜索和分析大量数据,它通常作为互联网应用的内部搜索引擎,为需要复杂搜索功能的应用提供支持。 ElasticSearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎

# 使用场景

  • 电商搜索引擎,使用 Elasticsearch 存储商品与品类信息,提供搜索和搜索建议功能(全文检索)
  • 日 志 系 统 , 收 集 、 分 析 日 志 数 据 , 可 以 使 用 Logstash(Elasticsearch/Logstash/Kibana 栈的一部分)来收集,然后将这些数据提供给Elasticsearch,通过搜索和聚合计算挖掘有价值的信息,最后通过 Kibana 进行可视化展示
  • 价格提醒平台,在价格变动时,让用户可以收到通知。抓取供应商的价格,推入Elasticsearch,并使用其反向搜索(Percolator)功能来匹配用户的价格通知设置,找到匹配后将提醒推送给用户
  • BI(商业智能),分析业务大数据,挖掘有价值的商务信息。可以使用 Elasticsearch来存储数据,然后使用 Kibana (Elasticsearch/Logstash/Kibana 堆栈的一部分)构建自定义仪表板,该仪表板可以可视化显示数据。此外,还可以使用Elasticsearch 聚合功能对数据执行复杂的业务智能分析。

# ElasticSearch 的特点

# 天然分片,天然集群

ES 把数据分成多个 shard,下图中的 P0-P2,多个 shard 可以组成一份完整的数据,这些 shard 可以分布在集群中的各个机器节点中。随着数据的不断增加,集群可以增加多个分片,把多个分片放到多个机子上,已达到负载均衡,横向扩展

在实际运算过程中,每个查询任务提交到某一个节点,该节点必须负责将数据进行整理汇聚,再返回给客户端,也就是一个简单的节点上进行 Map 计算,在一个固定的节点上进行 Reduces 得到最终结果向客户端返回。

这种集群分片的机制造就了 elasticsearch 强大的数据容量及运算扩展性

# 天然索引

ES 所有数据都是默认进行索引的,这点和 MySQL 正好相反,MySQL 是默认不加索引,要加索引必须特别说明,ES 只有不加索引才需要说明。

而 ES 使用的是倒排索引和 MySQL 的 B+Tree 索引不同

传统关系性数据库

弊端:

  • 对于传统的关系性数据库对于关键词的查询,只能逐字逐行的匹配,性能非常差
  • 匹配方式不合理,比如搜索“小密手机”,如果用 like 进行匹配, 根本匹配不到。但是考虑使用者的用户体验的话,除了完全匹配的记录,还应该显示一部分近似匹配的记录,至少应该匹配到“手机”。

倒排索引是怎么处理的

全文搜索引擎目前主流的索引技术就是倒排索引的方式

传统的保存数据的方式都是:记录→单词

而倒排索引的保存数据的方式是:单词→记录, 基于分词技术构建倒排索引,每个记录保存数据时,都不会直接存入数据库。系统先会对数据进行分词,然后以倒排索引结构保存。

等到用户搜索的时候,会把搜索的关键词也进行分词,会把“红海行动”分词分成:红海和行动两个词。

这样的话,先用红海进行匹配,得到 id=1 和 id=2 的记录编号,再用行动匹配可以迅速定位 id 为 1,3 的记录。

那么全文索引通常,还会根据匹配程度进行打分,显然 1 号记录能匹配的次数更多。所以显示的时候以评分进行排序的话,1 号记录会排到最前面。而 2、3 号记录也可以匹配到。

可以看到 Lucene 为倒排索引(Term Dictionary)部分又增加一层 Term Index 结构,用于快速定位,而这 Term Index 是缓存在内存中的,但 MySQL 的 B+tree 不在内存中,所以整体来看 ES 速度更快,但同时也更消耗资源(内存、磁盘)。

# Lucene、Nutch、ElasticSearch 关系

  • 1998 年 9 月 4 日,Google 公司在美国硅谷成立。正如大家所知,它是一家做搜索引擎起家的公司,同时期,一位名叫 Doug Cutting 的美国工程师,也迷上了搜索引擎,他基于 Java 语言开发了一个用于文本搜索的函数库(姑且理解为软件的功能组件),叫做 Lucene。Lucene是第一个提供全文文本搜索的函数库,提供了一个简单而强大的应用程序接口,是一个高性能、可伸缩的信息搜索库。作为一个成熟免费的开源项目,Lucene 在 Java信息检索程序库中得到了广泛的欢迎。开发者不仅能利用它构建 具体的全文检索应用,同时还能将其集成到各种系统软件中,它提供的很多 API 函数都能运用到各种实际应用程序中。
  • Nutch 则是 Doug 在 Lucene 基础上将开源思想继续深化的成果,是一个真正的应用程序,它是建立在 Lucene 核心之上的 Web 搜索的实现,其目的旨在减少人们使用过程中的复杂度,并在花费很少的情况下配置世界一流的 Web 搜索引擎,实现开箱即用的特性。 站内索引和搜索推广到全球网络的搜索上,就像 Google 和雅虎一样。
  • ElasticSearch ,简称为 ES , ES 是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。ES 也使用 Java开发并使用 Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

# ElasticSearch 的安装

Elasticsearch 官网:

https://www.elastic.co/products/elasticsearch

  1. 上传安装包并解压

  2. 在/opt/module 目录下对 ES 重命名

    mv elasticsearch-6.6.0/ elasticsearch
    
    1
  3. 修改 ES 配置文件

    1. 集群名称 ,同一集群名称必须相同

    2. 单个节点名称

    3. 修改网路主机

    4. 把 bootstrap 自检程序关掉

      bootstrap.system_call_filter: false

    5. 自发现配置:新节点向集群报到的主机名

# 环境启动优化

ES 是用在 Java 虚拟机中运行的,虚拟机默认启动占用 1G 内存。但是如果是装在 PC机学习用,实际用不了 1 个 G。所以可以改小一点内存;但生产环境一般 128G 内存是标配,这个时候需要将这个内存调大。

vim /opt/module/elasticsearch/config/jvm.options

# 分发 ES

xsync /opt/module/elasticsearch
1

# 修改ha02和ha03上的节点名以及网络地址

# 单台启动测试,以及 Linux 解决常见问题

./elasticsearch
1

因为默认 elasticsearch 是单机访问模式,就是只能自己访问自己。但是上面我们已经设置成允许应用服务器通过网络方式访问,而且生产环境也是这种方式。这时,Elasticsearch就会因为嫌弃单机版的低端默认配置而报错,甚至无法启动。所以我们在这里就要把服务器的一些限制打开,能支持更多并发。

  1. **问题 1:**max file descriptors [4096] for elasticsearch process likely too low,increase to at least [65536] elasticsearch

    原因
    系统允许 Elasticsearch 打开的最大文件数需要修改成 65536
    解决
    sudo vim /etc/security/limits.conf
    添加内容
    * soft nofile 65536
    * hard nofile 131072
    * soft nproc 2048
    * hard nproc 65536
    注意:“*” 不要省略掉
    分发文件
    sudo /home/damoncai/bin/xsync /etc/security/limits.conf
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  2. **问题2:**max virtual memory areas vm.max_map_count [65530] likely too low,increase to at least [262144]

    原因
    一个进程可以拥有的虚拟内存区域的数量。
    解决
    sudo vim /etc/sysctl.conf
    在文件最后添加一行
    vm.max_map_count=262144
    即可永久修改
    分发文件
    sudo /home/damoncai/bin/xsync /etc/sysctl.conf
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  3. **问题3:**max number of threads [1024] for user [judy2] likely too low,increase to at least [4096] (CentOS7.x 不用改)

    原因
    允许最大线程数修该成 4096
    解决
    sudo vim /etc/security/limits.d/20-nproc.conf
    修改如下内容
    * soft nproc 1024
    修改为
    * soft nproc 4096
    分发文件
    sudo /home/damoncai/bin/xsync /etc/security/limits.d/20-nproc.conf
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    重启 linux 使配置生效

测试方式 1:curl http://ha01:9200/_cat/nodes?v

测试方式 2:在浏览器中,输入 http://ha01:9200/查看效果

# 集群启动脚本

在/home/damoncai/bin 目录下创建 es.sh,并授予执行权限

#!/bin/bash
es_home=/opt/module/elasticsearch
case $1 in
	"start") {
	for i in ha01 ha02 ha03
do
    echo "==============$i 上 ES 启动=============="
    ssh $i "source /etc/profile;${es_home}/bin/elasticsearch >/dev/null 2>&1 &"
    done
};;
"stop") {
    for i in ha01 ha02 ha03
    do
		echo "==============$i 上 ES 停止=============="
		ssh $i "ps -ef|grep $es_home |grep -v grep|awk '{print \$2}'|xargs kill" >/dev/null 2>&1
done
};;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 测试脚本

如果启动未成功,请去查看相关日志

vim /opt/module/elasticsearch/logs/my-es.log

# Kibana 的安装

  1. 上传安装包并解压

  2. 重命名

    mv kibana-6.6.0-linux-x86_64/ kibana
    
    1
  3. 修改 Kibana 配置文件

    cd config/
    vim kibana.yml
    
    1
    2
    1. 授权远程访问

      server.host: "0.0.0.0"
      
      1
    2. 指定 ElasticSearch 地址(可以指定多个,多个地之间用逗号分隔)

      elasticsearch.hosts: ["http://ha01:9200","http://ha02:9200","http://ha03:9200"]
      
      1
    3. 启动、测试

      bin/kibana
      
      1
    4. 浏览器访问 http://ha01:5601/

# 最终集群脚本

在 es.sh 中,对 ES 和 Kibana 同时进行操作

#!/bin/bash
es_home=/opt/module/elasticsearch
kibana_home=/opt/module/kibana
case $1 in
	"start") {
	for i in ha01 ha02 ha03
	do
		echo "==============$i 上 ES 启动=============="
		ssh $i "source /etc/profile;${es_home}/bin/elasticsearch >/dev/null 2>&1 &"
		done
		nohup ${kibana_home}/bin/kibana >${kibana_home}/logs/kibana.log 2>&1 &
};;
"stop") {
	ps -ef|grep ${kibana_home} |grep -v grep|awk '{print $2}'|xargs kill
	for i in ha01 ha02 ha03
	do
		echo "==============$i 上 ES 停止=============="
		ssh $i "ps -ef|grep $es_home |grep -v grep|awk '{print \$2}'|xargs kill" >/dev/null 2>&1
	done
};;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 对索引的操作

# 查询各个索引状态

API:GET /_cat/indices?v
1

ES 中会默认存在一些索引

# 创建索引

API:PUT 索引名?pretty

PUT movie_index?pretty
1
2
3

使用 PUT 创建名为“movie_index”的索引。末尾追加 pretty,可以漂亮地打印 JSON响应(如果有的话)。红色警告说在 7.x 分片数会由默认的 5 改为 1,我们忽略即可索引名命名要求:

  • 仅可能为小写字母,不能下划线开头
  • 不能包括 , /, *, ?, ", <, >, |, 空格, 逗号, #
  • 7.0 版本之前可以使用冒号:,但不建议使用并在 7.0 版本之后不再支持
  • 不能以这些字符 -, _, + 开头
  • 不能包括 . 或 …
  • 长度不能超过 255 个字符

# 查询某个索引的分片情况

API:GET /_cat/shards/索引名

GET /_cat/shards/movie_index
1
2
3

默认 5 个分片,1 个副本。所以看到一共有 10 个分片,5 个主,每一个主分片对应一个副本,注意:同一个分片的主和副本肯定不在同一个节点上

# 删除索引

API:DELETE /索引名

DELETE /movie_index
1
2
3

# 对文档进行操作

# 创建文档

现在向索引 movie_index 中放入文档,文档 ID 分别为 1,2,3

API: PUT /索引名/类型名/文档 id
注意:文档 id 和文档中的属性”id”不是一回事
PUT /movie_index/movie/1
{ 
"id":100,
"name":"operation red sea",
"doubanScore":8.5,
"actorList":[
{"id":1,"name":"zhang yi"},
{"id":2,"name":"hai qing"},
{"id":3,"name":"zhang han yu"}
]
}

PUT /movie_index/movie/2
{
"id":200,
"name":"operation meigong river",
"doubanScore":8.0,
"actorList":[
{"id":3,"name":"zhang han yu"}
]
}

PUT /movie_index/movie/3
{
"id":300,
"name":"incident red sea",
"doubanScore":5.0,
"actorList":[
{"id":4,"name":"zhang san feng"}
]
}
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

注意,Elasticsearch 并不要求,先要有索引,才能将文档编入索引。创建文档时,如果指定索引不存在,将自动创建。默认创建的索引分片是 5,副本是 1,我们创建的文档会在其中的某一个分片上存一份,副本上存一份,所以看到的响应_shards-total:2

# 根据文档 id 查看文档

API:GET /索引名/类型名/文档 id
GET /movie_index/movie/1?pretty
1
2

这里有一个字段 found 为真,表示找到了一个 ID 为 3 的文档,另一个字段_source,该字段返回完整 JSON 文档

# 查询所有文档

API:GET /索引名/_search
Kinana 中默认显示 10 条,可以通过 size 控制
GET /movie_index/_search
{
"size":10
}
1
2
3
4
5
6

took:执行查询花费的时间毫秒数 _shards=>total:搜索了多少个分片(当前表示搜索了全部 5 个分片)

# 根据文档 id 删除文档

API: DELETE /索引名/类型名/文档 id
DELETE /movie_index/movie/3
1
2

注意:删除索引和删除文档的区别?

  • 删除索引是会立即释放空间的,不存在所谓的“标记”逻辑。
  • 删除文档的时候,是将新文档写入,同时将旧文档标记为已删除。 磁盘空间是否释放取决于新旧文档是否在同一个 segment file 里面,因此 ES 后台的 segment merge在合并 segment file 的过程中有可能触发旧文档的物理删除。
  • 也可以手动执行 POST /_forcemerge 进行合并触发

# 替换文档

  • PUT(幂等性操作)

    #当我们通过执行PUT /索引名/类型名/文档id命令的添加时候,如果文档id已经存在,那么再次执行上面的命令,ElasticSearch 将替换现有文档。

    PUT /movie_index/movie/3
    {
    "id":300,
    "name":"incident red sea",
    "doubanScore":5.0,
    "actorList":[
    {"id":4,"name":"zhang cuishan"}
    ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • POST(非幂等性操作)

    创建文档时,ID 部分是可选的。如果没有指定,Elasticsearch 将生成一个随机 ID,然后使用它来引用文档。

    POST /movie_index/movie/
    {
    "id":300,
    "name":"incident red sea",
    "doubanScore":5.0,
    "actorList":[
    {"id":4,"name":"zhang cuishan"}
    ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 根据文档 id 更新文档

除了创建和替换文档外,ES 还可以更新文档中的某一个字段内容。注意,Elasticsearch实际上并没有在底层执行就地更新,而是先删除旧文档,再添加新文档。

API:
POST /索引名/类型名/文档 id/_update?pretty
{
"doc": { "字段名": "新的字段值" } doc 固定写法
}
需求:把文档 ID 为 3 中的 name 字段更改为“wudang”:
POST /movie_index/movie/3/_update?pretty
{
"doc": {"name":"wudang"}
}
1
2
3
4
5
6
7
8
9
10

# 根据条件更新文档(了解)

POST /movie_index/_update_by_query
{
"query": {
"match":{
"actorList.id":1
}
},
"script": {
"lang": "painless",
"source":"for(int i=0;i<ctx._source.actorList.length;i++){if(ctx._source.actorList[i].id==3){
ctx._source.actorList[i].name='tttt'}}"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 删除文档属性(了解)

POST /movie_index/movie/1/_update
{
"script" : "ctx._source.remove('name')"
}
1
2
3
4

# 根据条件删除文档(了解)

POST /movie_index /_delete_by_query
{
"query": {
"match_all": {}
}
}
1
2
3
4
5
6

# 批处理

除了对单个文档执行创建、更新和删除之外,ElasticSearch 还提供了使用_bulk API批量执行上述操作的能力。

API: POST /索引名/类型名/_bulk?pretty _bulk 表示批量操作
注意:Kibana 要求批量操作的 json 内容写在同一行
1
2

需求 1:在索引中批量创建两个文档

需求 2:在一个批量操作中,先更新第一个文档(ID 为 66),再删除第二个文档(ID 为 88)

# 查询操作

# 搜索参数传递有 2 种方法

  1. URI 发送搜索参数查询所有数据

    GET /索引名/_search?q=* &pretty
    例如:GET /movie_index/_search?q=_id:66
    
    1
    2
  2. 请求体(request body)发送搜索参数查询所有数据

    GET /movie_index/_search
    {
    "query": {
    "match_all": {}
    }
    }
    
    1
    2
    3
    4
    5
    6

# 按条件查询(全部)

GET movie_index/movie/_search
{
"query":{
"match_all": {}
}
}
1
2
3
4
5
6

# 按分词查询(必须使用分词 text 类型)

测试前:将 movie_index 索引中的数据恢复到初始的 3
1
GET movie_index/movie/_search
{
"query":{
"match": {"name":"operation red sea"}
}
}
1
2
3
4
5
6

ES 中,name 属性会进行分词,底层以倒排索引的形式进行存储,对查询的内容也会进行分词,然后和文档的 name 属性内容进行匹配,所以命中 3 次,不过命中的分值不同。 注意:ES 底层在保存字符串数据的时候,会有两种类型 text 和 keyword

text:分词 keyword:不分词

# 按分词子属性查询

GET movie_index/movie/_search
{
"query":{
"match": {"actorList.name":"zhang han yu"}
}
}
1
2
3
4
5
6

# 按短语查询(相当于 like %短语%)

按短语查询,不再利用分词技术,直接用短语在原始数据中匹配

GET movie_index/movie/_search
{
"query":{
"match_phrase": {"actorList.name":"zhang han yu"}
}
}
1
2
3
4
5
6

# 通过 term 精准搜索匹配(必须使用 keyword 类型)

GET movie_index/movie/_search
{
"query":{
"term":{
"actorList.name.keyword":"zhang han yu"
}
}
}
1
2
3
4
5
6
7
8

# fuzzy 查询(容错匹配)

校正匹配分词,当一个单词都无法准确匹配,ES 通过一种算法对非常接近的单词也给与一定的评分,能够查询出来,但是消耗更多的性能,对中文来讲,实现不是特别好

GET movie_index/movie/_search
{
"query":{
"fuzzy": {"name":"rad"}
}
}
1
2
3
4
5
6

# 过滤—先匹配,再过滤

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red"}
},
"post_filter":{
"term": {
"actorList.id": 3
}
}
}
1
2
3
4
5
6
7
8
9
10
11

# 过滤—匹配和过滤同时(推荐使用)

GET movie_index/movie/_search
{
"query": {
"bool": {
"must": [
{"match": {
"name": "red"
}}
],
"filter": [
{"term": { "actorList.id": "1"}},
{"term": {"actorList.id": "3"}}
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 过滤--按范围过滤

GET movie_index/movie/_search
{
"query": {
"range": {
"doubanScore": {
"gte": 6,
"lte": 8.5
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11

关于范围操作符:

# 排序

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
},
"sort":
{
"doubanScore": {
"order": "desc"
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12

# 分页查询

from 参数(基于 0)指定从哪个文档序号开始,size 参数指定返回多少个文档,这两个参数对于搜索结果分页非常有用。注意,如果没有指定 from,则默认值为 0。

GET movie_index/movie/_search
{
"query": { "match_all": {} },
"from": 1,
"size": 1
}
1
2
3
4
5
6

# 指定查询的字段

GET movie_index/movie/_search
{
"query": { "match_all": {} },
"_source": ["name", "doubanScore"]
}
1
2
3
4
5

# 高亮

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
},
"highlight": {
"fields": {"name":{} }
}
}
1
2
3
4
5
6
7
8
9

# 聚合

聚合提供了对数据进行分组、统计的能力,类似于 SQL 中 Group By 和 SQL 聚合函数。在 ElasticSearch 中,可以同时返回搜索结果及其聚合计算结果,这是非常强大和高效的。

  1. 需求 1:取出每个演员共参演了多少部电影

    GET movie_index/movie/_search
    {
    "aggs": {
    "myAGG": {
    "terms": {
    "field": "actorList.name.keyword"
    }
    }
    }
    }
    
    aggs:表示聚合
    myAGG:给聚合取的名字,
    trems:表示分组,相当于 groupBy
    field:指定分组字段
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  2. 需求 2:每个演员参演电影的平均分是多少,并按评分排序

    GET movie_index/movie/_search
    {
    "aggs": {
    "groupby_actor_id": {
    "terms": {
    "field": "actorList.name.keyword" ,
    "order": {
    "avg_score": "desc"
    }
    },
    "aggs": {
    "avg_score":{
    "avg": {
    "field": "doubanScore"
    }
    }
    }
    }
    }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

聚合时为何要加 .keyword 后缀?

.keyword 是某个字符串字段,专门储存不分词格式的副本,在某些场景中只允许只用不分词的格式,比如过滤 filter 比如聚合 aggs, 所以字段要加上.keyword 的后缀。

# 分词

# 查看英文单词默认分词情况

GET _analyze
{
"text":"hello world"
}
按照空格对单词进行切分
1
2
3
4
5

# 中文分词器

ES本身自带的中文分词,就是单纯把中文一个字一个字的分开,根本没有词汇的概念。但是实际应用中,用户都是以词汇为条件,进行查询匹配的,如果能够把文章以词汇为单位切分开,那么与用户的查询条件能够更贴切的匹配上,查询速度也更加快速。

常见的一些开源分词器对比,我们使用IK分词器

# IK 分词器的安装及使用

  1. 上传文件https://github.com/medcl/elasticsearch-analysis-ik

  2. 解压 zip 文件

    unzip elasticsearch-analysis-ik-6.6.0.zip -d
    /opt/module/elasticsearch/plugins/ik
    注意
    使用 unzip 进行解压
    -d 指定解压后的目录
    必须放到 ES 的 plugins 目录下,并在 plugins 目录下创建单独的目录
    
    1
    2
    3
    4
    5
    6
  3. 查看/opt/module/elasticsearch/plugins/ik/conf 下的文件,分词就是将所有词汇分

  4. 分发

  5. 重启 ES

  6. 测试使用 - ik_smart 分词方式

    GET movie_index/_analyze
    {
    "analyzer": "ik_smart",
    "text": "我是中国人"
    }
    
    1
    2
    3
    4
    5
  7. 测试使用 - ik_max_word

    GET movie_index/_analyze
    {
    "analyzer": "ik_max_word",
    "text": "我是中国人"
    }
    
    1
    2
    3
    4
    5

# 自定义词库-本地指定

通过配置本地目录直接指定自定义词库

  1. 修改/opt/module/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">./myword.txt</entry>
    <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords"></entry>
    <!--用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry> -->
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
    </properties>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    名词概念:停止词,是由英文单词:stopword 翻译过来的,原来在英语里面会遇到很多 a,the,or 等使用频率很多的字或词,常为冠词、介词、副词或连词等。 如果搜索引擎要将这些词都索引的话,那么几乎每个网站都会被索引,也就是说工作量巨大。可以毫不夸张的说句,只要是个英文网站都会用到 a 或者是the。那么这些文的词跟我们中文有什么关系呢? 在中文网站里面其实也存在大量的 stopword,我们称它为停止词。比如,我们前面这句话,“在”、“里面”、“也”、“的”、“它”、“为”这些词都是停止词。这些词因为使用频率过高,几乎每个网页上都存在,所以搜索引擎开发人员都将这一类词语全部忽略掉。如果我们的网站上存在大量这样的词语,那么相当于浪费了很多资源。原本可以添加一个关键词,排名就可以上升一名的,为什么不留着添加为关键词呢?停止词对 SEO 的意义不是越多越好,而是尽量的减少为宜。

  2. 在/opt/module/elasticsearch/plugins/ik/config/当前目录下创建 myword.txt

    vim myword.txt
    
    蓝瘦
    蓝瘦香菇
    
    1
    2
    3
    4
  3. 分发配置文件以及 myword.txt

    xsync /opt/module/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
    xsync /opt/module/elasticsearch/plugins/ik/config/myword.txt
    
    1
    2
  4. 重启 ES 服务

  5. 测试

# 自定义词库-远程指定

远程配置一般是如下流程,我们这里简易通过 nginx 模拟

  1. 修改/opt/module/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <!--<entry key="ext_dict"> </entry>-->
    <!--用户可以在这里配置自己的扩展停止词字典-->
    <!--<entry key="ext_stopwords"></entry>-->
    <!--用户可以在这里配置远程扩展字典 -->
    <entry key="remote_ext_dict">http://ha01/fenci/myword.txt</entry>
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
    </properties>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    注意:将本地配置注释掉

  2. 分发配置文件

    xsync /opt/module/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
    
    1
  3. 在 nginx.conf 文件中配置静态资源路径

    vim nginx.conf
    
    location /fenci{
    root es;
    }
    
    1
    2
    3
    4
    5
  4. 在/opt/module/nginx/目录下创建 es/fenci 目录,并在 es/fenci 目录下创建myword.txt

    vim myword.txt
    蓝瘦
    蓝瘦香菇
    
    1
    2
    3
  5. 启动 nginx

  6. 重启es服务

  7. 更新完成后,ES 只会对新增的数据用新词分词。历史数据是不会重新分词的。如果想要历史数据重新分词。需要执行:

    POST movies_index_chn/_update_by_query?conflicts=proceed
    
    1

# 关于 mapping

实际上每个 Type 中的字段是什么数据类型,由 mapping 定义,如果我们在创建 Index的时候,没有设定 mapping,系统会自动根据一条数据的格式来推断出该数据对应的字段类型,具体推断类型如下:

  • true/false → boolean
  • 1020 → long
  • 20.1 → float
  • “2018-02-01” → date
  • “hello world” → text +keyword

默认只有 text 会进行分词,keyword 是不会分词的字符串。mapping 除了自动定义还可以手动定义,但是只能对新加的、没有数据的字段进行定义,一旦有了数据就无法再做修改了。

# 基于中文分词搭建索引-自动定义 mapping

  1. 直接创建 Document

    这个时候 index 不存在,建立文档的时候自动创建 index,同时 mapping 会自动定义

    PUT /movie_chn_1/movie/1
    { "id":1,
    "name":"红海行动",
    "doubanScore":8.5,
    "actorList":[
    {"id":1,"name":"张译"},
    {"id":2,"name":"海清"},
    {"id":3,"name":"张涵予"}
    ]
    }
    PUT /movie_chn_1/movie/2
    {
    "id":2,
    "name":"湄公河行动",
    "doubanScore":8.0,
    "actorList":[
    {"id":3,"name":"张涵予"}
    ]
    }
    PUT /movie_chn_1/movie/3
    {
    "id":3,
    "name":"红海事件",
    "doubanScore":5.0,
    "actorList":[
    {"id":4,"name":"张三丰"}
    ]
    }
    
    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
  2. 查看自动定义的 mapping

  3. 查询测试

    GET /movie_chn_1/movie/_search
    {
    "query": {
    "match": {
    "name": "海行"
    }
    }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

  4. 分析结论

    上面查询“海行”命中了三条记录,是因为我们在定义的 Index 的时候,没有指定分词器,使用的是默认的分词器,对中文是按照每个汉字进行分词的。

# 基于中文分词搭建索引-手动定义 mapping

  1. 定义 Index,指定 mapping

    PUT movie_chn_2
    {
    "mappings": {
    "movie":{
    "properties": {
    "id":{
    "type": "long"
    },
    "name":{
    "type": "text",
    "analyzer": "ik_smart"
    },
    "doubanScore":{
    "type": "double"
    },
    "actorList":{
    "properties": {
    "id":{
    "type":"long"
    },
    "name":{
    "type":"keyword"
    }
    }
    }
    }
    }
    }
    }
    
    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

  2. 向 Index 中放入 Document

    PUT /movie_chn_2/movie/1
    { "id":1,
    "name":"红海行动",
    "doubanScore":8.5,
    "actorList":[
    {"id":1,"name":"张译"},
    {"id":2,"name":"海清"},
    {"id":3,"name":"张涵予"}
    ]
    }
    PUT /movie_chn_2/movie/2
    {
    "id":2,
    "name":"湄公河行动",
    "doubanScore":8.0,
    "actorList":[
    {"id":3,"name":"张涵予"}
    ]
    }
    PUT /movie_chn_2/movie/3
    {
    "id":3,
    "name":"红海事件",
    "doubanScore":5.0,
    "actorList":[
    {"id":4,"name":"张三丰"}
    ]
    }
    
    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
  3. 查看手动定义的 mapping

  4. 查询测试

    GET /movie_chn_2/movie/_search
    {
    "query": {
    "match": {
    "name": "海行"
    }
    }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

  5. 分析结论

    上面查询没有命中任何记录,是因为我们在创建 Index 的时候,指定使用 ik 分词器进行分词

# 索引数据拷贝

ElasticSearch 虽然强大,但是却不能动态修改 mapping 到时候我们有时候需要修改结构的时候不得不重新创建索引;

ElasticSearch 为我们提供了一个 reindex 的命令,就是会将一个索引的快照数据 copy到另一个索引,默认情况下存在相同的_id 会进行覆盖(一般不会发生,除非是将两个索引的数据 copy 到一个索引中),可以使用 POST _reindex 命令将索引快照进行 copy

POST _reindex
{
"source": {
"index": "my_index_name"
},
"dest": {
"index": "my_index_name_new"
}
}
1
2
3
4
5
6
7
8
9

# 索引别名 _aliases

索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需要索引名的 API 来使用

# 创建索引别名

  • 创建 Index 的时候声明

    PUT 索引名
    {
    "aliases": {
    "索引别名": {}
    }
    }
    #创建索引的时候,手动 mapping,并指定别名
    PUT movie_chn_3
    {
    "aliases": {
    "movie_chn_3_aliase": {}
    },
    "mappings": {
    "movie":{
    "properties": {
    "id":{
    "type": "long"
    },
    "name":{
    "type": "text",
    "analyzer": "ik_smart"
    },
    "doubanScore":{
    "type": "double"
    },
    "actorList":{
    "properties": {
    "id":{
    "type":"long"
    },
    "name":{
    "type":"keyword"
    }
    }
    }
    }
    }
    }
    }
    
    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

  • 为已存在的索引增加别名

    POST _aliases
    {
    "actions": [
    { "add":{ "index": "索引名", "alias": "索引别名" }}
    ]
    }
    #给 movie_chn_3 添加别名
    POST _aliases
    {
    "actions": [
    { "add":{ "index": "movie_chn_3", "alias": "movie_chn_3_a2" }}
    ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

# 查询别名列表

GET _cat/aliases?v
1

# 使用索引别名查询

与使用普通索引没有区别

GET 索引别名/_search
1

# 删除某个索引的别名

POST _aliases
{
"actions": [
{ "remove": { "index": "索引名", "alias": "索引别名" }}
]
}
1
2
3
4
5
6

# 使用场景

给索引的一个子集创建视图

在运行的集群中可以无缝的从一个索引切换到另一个索引

# 索引模板

索引模板(Index Template),顾名思义就是创建索引的模具,其中可以定义一系列规则来帮助我们构建符合特定业务需求的索引的 mappings 和 settings,通过使用索引模板可以让我们的索引具备可预知的一致性。

# 创建索引模板、

PUT _template/template_movie2020
{
"index_patterns": ["movie_test*"],
"settings": {
"number_of_shards": 1
},
"aliases" : {
"{index}-query": {},
"movie_test-query":{}
},
"mappings": {
"_doc": {
"properties": {
"id": {
"type": "keyword"
},
"movie_name": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

其中 "index_patterns": ["movie_test*"]的含义就是凡是往 movie_test 开头的索引写入数据时,如果索引不存在,那么 ES 会根据此模板自动建立索引。

在 "aliases" 中用{index}表示,获得真正的创建的索引名。aliases 中会创建两个别名,一个是根据当前索引创建的,另一个是全局固定的别名。

# 测试

  • 向索引中添加数据

    POST movie_test_202011/_doc
    {
    "id":"333",
    "name":"zhang3"
    }
    
    1
    2
    3
    4
    5
  • 查询 Index 的 mapping,就是使用我们的索引模板创建的

    GET movie_test_202011-query/_mapping
    
    1

  • 根据模板中取的别名查询数据

    GET movie_test-query/_search
    
    1

# 查看系统中已有的模板清单

GET _cat/templates
1

# 查看某个模板详情

GET _template/template_movie2020
或者
GET _template/template_movie*
1
2
3

# 使用场景

  • 分割索引

    分割索引就是根据时间间隔把一个业务索引切分成多个索引。

    比如 把 order_info 变成 order_info_20200101,order_info_20200102 ….. 这样做的好处有两个:

    1. 结构变化的灵活性

      因为 ES 不允许对数据结构进行修改。但是实际使用中索引的结构和配置难免变化,那么只要对下一个间隔的索引进行修改,原来的索引维持原状。这样就有了一定的灵活性。要想实现这个效果,我们只需要在需要变化的索引那天将模板重新建立即可。

    2. 查询范围优化

      因为一般情况并不会查询全部时间周期的数据,那么通过切分索引,物理上减少了扫描数据的范围,也是对性能的优化。

# 注意

使用索引模板,一般在向索引中插入第一条数据创建索引,如果ES中的Shard特别多,有可能创建索引会变慢,如果延迟不能接受,可以不使用模板,使用定时脚本在头一天提前建立第二天的索引。

# Idea(Scala) 中操作 ElasticSearch

目前市面上有两类客户端

  1. 一类是 TransportClient 为代表的 ES 原生客户端,不能执行原生 DSL 语句必须使用它的 Java api 方法。
  2. 一类是以 Rest ApI 为主的 client,最典型的就是 jest。 这种客户端可以直接使用DSL 语句拼成的字符串,直接传给服务端,然后返回 json 字符串再解析。 两种方式各有优劣,但是最近 ElasticSearch 官网,宣布计划在 7.0 以后的版本中废除 TransportClient,以 RestClient 为主。

# 前置配置

  1. pom

    <dependencies>
    <!--Java 操作 ES 的客户端工具 Jest-->
    <dependency>
    <groupId>io.searchbox</groupId>
    <artifactId>jest</artifactId>
    <version>5.3.3</version>
    </dependency>
    <!--Jest 需要的依赖-->
    <dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>4.5.2</version>
    </dependency>
    <!--Jest 需要的依赖-->
    <dependency>
        <groupId>org.codehaus.janino</groupId>
    <artifactId>commons-compiler</artifactId>
    <version>3.0.16</version>
    </dependency>
    <!-- ElasticSearch 依赖 -->
    <dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.6.0</version>
    </dependency>
    </dependencies>
    
    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

# JestFactory对象创建

object ESUtil {

  private var factory:JestClientFactory = null;

  def getClient:JestClient = {
    if(factory == null) build();
    factory.getObject;
  }

  def build(): Unit = {
    factory = new JestClientFactory
    factory.setHttpClientConfig(
      new HttpClientConfig
      .Builder("http://ha01:9200")
        .multiThreaded(true)
        .maxTotalConnection(20)
        .connTimeout(10000)
        .readTimeout(1000)
        .build()
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 向 ES 中插入数据

/**
   * 插入数据
   */
  def put(): Unit = {
    val jestClient: JestClient = getClient

    val source =
      """
        |{
        |"id":400,
        |"name":"incident red sea",
        |"doubanScore":5.0,
        |"actorList":[
        |{"id":4,"name":"zhang cuishan"}
        |]
        |}
        |""".stripMargin

    val index:Index = new Index.Builder(source)
      .index("index_test")
      .`type`("movie")
      .id("1")
      .build()

    jestClient.execute(index)
    jestClient.close()
  }
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

# 查询单条数据

/**
   * 查询单条数据
   */
def selectOne(id: String): Unit = {
    val jestClient: JestClient = getClient

    val get: Get = new Get.Builder("index_test", id).build()

    val result: DocumentResult = jestClient.execute(get)
    println(result.getJsonString)
    jestClient.close()
}
1
2
3
4
5
6
7
8
9
10
11
12

# 多条件查询一

/**
   * 多条件查询一
   */
def mutiSearchOne: Unit = {
    val jestClient: JestClient = getClient

    val query:String =
    """
          |{
          |  "query": {
          |    "match_all": {}
          |  }
          |}
          |""".stripMargin

    val search: Search = new Search.Builder(query)
    .addIndex("index_test")
    .build()

    val result: SearchResult = jestClient.execute(search)
    import java.util
    val list: util.List[SearchResult#Hit[util.Map[String, Any], Void]] = result.getHits(classOf[util.Map[String, Any]])
    import scala.collection.JavaConverters._
    val list1: List[util.Map[String, Any]] = list.asScala.map(_.source).toList
    println(list1.mkString("\n"))
    jestClient.close()
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

# 多条件查询二(使用构造对象)

/**
   * 多条件查询二
   */
def mutiSearchTwo: Unit = {
    val jestClient: JestClient = getClient

    val searchSourceBuilder:SearchSourceBuilder  = new SearchSourceBuilder
    val boolQueryBuilder:BoolQueryBuilder = new BoolQueryBuilder()
    boolQueryBuilder.must(new MatchQueryBuilder("name","天龙"))
    boolQueryBuilder.filter(new TermsQueryBuilder("actorList.name.keyword","李若单"))
    searchSourceBuilder.query(boolQueryBuilder)
    searchSourceBuilder.from(0)
    searchSourceBuilder.size(10)
    searchSourceBuilder.sort("doubanScore",SortOrder.ASC)
    searchSourceBuilder.highlighter(new HighlightBuilder().field("name"))
    val query:String = searchSourceBuilder.toString()
    val search: Search = new Search.Builder(query)
    .addIndex("index_test")
    .build()

    val result: SearchResult = jestClient.execute(search)
    import java.util
    val list: util.List[SearchResult#Hit[util.Map[String, Any], Void]] = result.getHits(classOf[util.Map[String, Any]])
    import scala.collection.JavaConverters._
    val list1: List[util.Map[String, Any]] = list.asScala.map(_.source).toList
    println(list1.mkString("\n"))
    jestClient.close()
}
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
Last Updated: 3/14/2022, 1:43:10 PM