1)ElasticSearch基本介绍
ElasticSearch(以下简称ES)是Java编写的基于Lucene之上的全文检索服务器,它在Lucene的基础上做了简化,使用RESTful的形式提供分布式的全文检索功能。它在海量数据检索方面是Mysql远不能及的,对于一些需要从大量文本中按照特定条件检索数据的场景,比如博客的文章检索系统,用户输入关键字搜索时不只是匹配标题那么简单,还需要对文章正文中的数据匹配,使用ES显然比Mysql更为合适且高效。区别于Mysql,ES的结构名称不同,但都有对应关系:
!在ES6.x版本之后官方推荐在一个Index(索引)里尽量保证只有一个Type(类型)。
- 索引(Index):含有相同属性的文档的集合,索引名称必须是小写字母
- 类型(Type):索引可以定义一个或多个类型,文档必须属于一个类型
- 文档(Document):文档是数据存储的基本单位,对应Mysql的一行记录
- 字段(Field):ES中的字段与Mysql中的列概念是一样的
- 映射(Mapping):映射定义了ES的数据存储结构
2)RESTful API的格式
ES提供了RESTful形式的API可供使用,我们有必要了解URL中每段对应的是什么:http://:<端口>/<索引>/<类型>/<文档id>,辅以HTTP请求方法如GET、POST、DELETE、PUT等可以完成对ES的各种操作。
3)部署ElasticSearch
3.1)部署一个单节点的ElasticSearch
因为ES是使用Java语言编写的,运行需要Java环境支持,6.x版本注意对应Java 8,如果安装了高版本如JDK13可能导致版本不匹配而出现Caused by: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "accessClassInPackage.jdk.internal.vm.annotation")
等异常导致ES启动失败。由于我的电脑中只安装了JDK13,所以只能找7.x版本适配。这里使用的是Linux(Ubuntu18.04)环境,下载7.3.2版本的tar.gz文件(Windows环境下载对应文件并双击运行bin下的elasticsearch.bat
文件即可)并解压(sudo tar -zxvf elasticsearch-7.3.2-no-jdk-linux-x86_64.tar.gz
),ES解压后的目录如下:
- bin:ES的启动脚本程序(重要)
- config:ES配置文件(重要)
- lib:运行时依赖的第三方库
- logs:运行的日志信息
- modules:ES的模块程序目录
- plugins:放置第三方插件的目录(重要)
Linux下不能使用root用户(包括sudo
)运行脚本,需要一个普通用户。同样因为Linux中的权限问题,直接运行脚本会提示无法访问配置文件,还需要使用命令sudo chown -R tiny:tiny elasticsearch-7.3.2
对当前用户赋予对文件夹下内容的访问权限(tiny:tiny分别是用户组和组中用户)。最后进入到ES文件夹下执行bin/elasticsearch &
启动ES(加'&'可以后台形式启动ES,不会阻塞Bash进程);启动无报错信息后在浏览器输入localhost:9200
回车,出现类似如下响应表示一个单节点的ES启动成功:
!ElasticSearch在国内的下载速度缓慢,可下载需要的ES
3.2)安装Elasticsearch-head
可以看到ES返回的都是JSON形式的响应,对于用户来说并不友好,我们需要一个更为人性化的前端系统来展示和管理ES,这就是下面要用到的ealsticsearch-head。
安装elasticsearch-head前需要先安装node.js环境和npm,Ubuntu下使用命令安装:sudo apt install nodejs
、sudo apt install npm
;接下来拉取elasticsearch-head的代码:sudo git clone git://github.com/mobz/elasticsearch-head.git
,然后进入elasticsearch-head目录下,此时还需要安装phantomjs-prebuilt
,因为使用默认源安装会卡住,我们需要指定一下它的下载源:npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
,然后再执行命令sudo npm install phantomjs-prebuilt
,此时可能出现错误:
执行sudo npm install phantomjs-prebuilt@2.1.16 --ignore-scripts
,安装成功了!接下来开始正题,执行sudo npm install
安装elasticsearch-head,安装成功后使用命令npm run start
启动。
使用浏览器访问:localhost:9100,会看到如下界面:
可以看到elasticsearch-head并没有连接到ES,这是因为它们之间存在跨域问题导致elasticsearch-head无法正常访问到ES;接下来需要需改ES的配置文件,让它支持跨域访问:先停止ES,到它的根目录下,执行sudo vim config/elasticsearch.yml
,在其中加入如下配置:
# 设置支持跨域访问ES
http.cors.enabled: true
http.cors.allow-origin: "*"
重新启动ES,然后到到浏览器点击URL旁边的“连接”按钮就可以正常连接ES了:
3.3)部署多实例ElasticSearch
ES是一个分布式的全文检索引擎,在实际的生产中往往需要部署多个实例在多台机器上,以保证ES服务的高可用及负载均衡。这里作为演示只在一台机器上部署两个实例:一个主节点,一个随从节点(实际上在同一台机器上部署是没有意义的)!
我们先部署一个主节点实例,跟单实例的启动不同,多实例需要对elasticsearch.yml
文件做一些配置:
# 设置支持跨域访问ES
http.cors.enabled: true
http.cors.allow-origin: "*"
# 设置集群名称,同一个集群中的节点名称一个保持一致!
cluster.name: tiny
# 设置节点名称,并且是主节点
node.name: master
node.master: true
# 设置主机地址为本机
network.host: 127.0.0.1
使用命令./bin/elasticsearch &
后台启动一个ES实例,启动完成后回车,用elasticsearch-head查看该节点是否启动成功:
没有问题,接着部署一个随从节点。这里我们需要重新复制一份ES(直接修改同一份文件没法启动多个实例),这里我的目录名称仍然是elasticsearch-7.3.2,把它放到了其他路径下避免冲突,同样需要修改配置文件:
# 设置支持跨域访问ES
http.cors.enabled: true
http.cors.allow-origin: "*"
# 设置集群名称,同一个集群中的节点名称一个保持一致!
cluster.name: tiny
# 设置节点名称
node.name: slave
# node.master: true 这个属性不设置就是随从节点
# 设置主机地址为本机
network.host: 127.0.0.1
# 因为9200端口已经被主节点占用,这里要重新设置服务端口
http.port: 9400
# 主节点的主机地址在127.0.0.1,随从节点需要知道主节点的部署地址
discovery.zen.ping.unicast.hosts: ["127.0.0.1"]
在启动之前,Linux系统下仍然需要给当前用户权限:sudo chown -R tiny:tiny elasticsearch-7.3.2
;启动成功后到elasticsearch-head页面刷新,可以看到两个节点都部署成功了:
4)ES的简单使用
ES提供了HTTP形式的交互方式,对于所有的HTTP请求它都以JSON的形式响应。
4.1)创建索引
我们可以通过elasticsearch-head的“索引”菜单来快速的创建一个索引,创建之后我们回到概览:
可以看到节点都多了几个绿色的方框,这些方框被称作“分片”分片包括:主分片(粗边框)和备用分片(细边框),集群环境下每个主分片都有一个备用分片(纵向对应)。
我们上面只是创建了一个索引,就像创建了一个Mysql数据库,但是还没有定义它的数据存储结构映射mapping;通过“概览”可以看到article索引的mappings是空的:
要定义映射就要发送JSON格式的数据,我们知道elasticsearch-head也是支持对ES发送各种HTTP请求的,但是它对JSON的支持很不友好,所以这里使用Postman代替它。
Postman在Ubuntu等Linux系统下可能会因为缺少字体导致编辑页出现光标错位导致无法精准定位字符,下载安装后重启Postman即可。
我们发送POST请求,URL为:localhost:9200/article/_mappings
,在请求体中填入如下数据:
{
"properties": {
"title": {
"type": "text"
},
"content": {
"type": "text"
},
"word_count": {
"type": "integer"
},
"tag": {
"type": "keyword"
},
"create_date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
得到{ "acknowledged": true}
的响应,刷新elasticsearch-head页面,再次查看索引信息可以看到mappings中已经有对应结构了:
当然我们也完全可以使用Postman来创建索引;发送URL为localhost:9200/test
的PUT请求即可创建一个名为test的索引,如果需要定义mappings结构,可使用如下请求体:
{
"mappings": {
"properties": {
"title": {
"type": "text"
},
"content": {
"type": "text"
},
"create_time": {
"type": "date",
"format": "yyyy-MM-dd"
}
}
}
}
这里需要注意的是,如果JSON中写了mappings
,那么URL中就不需要写_mappings
,反之亦然。
4.2)插入数据
结构创建好了之后我们来插入数据,发送PUT请求,URL为localhost:9200/article/_doc/1
;对应之前创建的映射结构,在请求体中插入如下数据:
{
"title": "文章标题",
"content": "文章内容",
"word_count": 2000,
"create_time": "2020-01-02",
"tag": "elasticsearch"
}
响应如下内容表示插入成功:
{
"_index": "article",
"_type": "_doc",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed":
},
"_seq_no": ,
"_primary_term": 1
}
稍微修改一下请求体数据,并把URL末尾的文档id改为2,再次插入一条数据;我们可以刷新elasticsearch-head页面,点击“数据浏览”,可以看到数据确实存在:
上面我们是通过指定文档id插入数据的,这样比较繁琐,我们可以让ES自动生成id;只要把URL末尾的id去除,同时把HTTP请求方法改成POST即可。
4.3)更新数据
发送POST请求,URL为localhost:9200/article/_doc/1/_update
把要更新的字段写在doc
关键字里:
{
"doc": {
"title": "文章标题001"
}
}
返回如下数据,我们注意到_version
字段变成了2,在ES中对数据的修改操作都会促使版本的递增:
{
"_index": "article",
"_type": "_doc",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed":
},
"_seq_no": 2,
"_primary_term": 1
}
可以看到id为1的数据更新成功了:
这里再稍微提一下ES对脚本修改数据的支持,我们希望把id为1的文档数据的word_count
字段增加400(这是一个integer
类型的字段),请求体改为:
{
"script": {
"lang": "painless",
"inline": "ctx._source.word_count += 400"
}
}
其中script
表示要使用脚本的方式修改数据,lang
指定要使用的脚本语言,painless
是ES内置的脚本语言,inline
里就是要执行的脚本了;ctx
代表的是上下文,_source
代表当前要操作的文档对象。
它还支持定义参数变量,我们可以把需要的二手手游账号买卖参数都定义在param
关键字中,并在inline
中引用:
{
"script": {
"lang": "painless",
"inline": "ctx._source.word_count += params.num",
"params": {
"num": 400
}
}
}
4.4)删除数据
删除数据非常简单,因为我们不需要提供请求体,只要发送一个URL为localhost:9200/article/_doc/1
的DELETE请求即可,这里我删除了文档id为1的数据,响应结果如下:
{
"_index": "article",
"_type": "_doc",
"_id": "1",
"_version": 6,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 2,
"failed":
},
"_seq_no": 6,
"_primary_term": 1
}
4.5)查询数据
ES的查询可分为:简单查询、条件查询及聚合查询。
4.5.1) 简单查询
发送URL为localhost:9200/article/_doc/2
的GET请求,即可查询到id为2的数据:
{
"_index": "article",
"_type": "_doc",
"_id": "2",
"_version": 1,
"_seq_no": ,
"_primary_term": 1,
"found": true,
"_source": {
"title": "文章标题1",
"content": "文章内容1",
"word_count": 12000,
"create_time": "2020-01-03",
"tag": "java"
}
}
ES还支持在URL后跟查询条件,比如我们希望查询title
字段含有数字“1”的数据,则可以发送URL为localhost:9200/article/_search?q=title:1
的GET请求。
4.5.2)条件查询
条件查询的条件由请求提携带,接下来我们发送一个查询所有数据的请求;发送URL为localhost:9200/article/_search
的GET请求,在请求体中填充查询条件:
{
"query": {
"match_all": {}
}
}
类似Mysql中的分页查询,我们还可以指定从哪里开始查询以及要查询的条数:
{
"query": {
"match_all": {}
},
"from": ,
"size": 1
}
我们可以在match
下指定要匹配的字段:
{
"query": {
"match": {
"title": "1"
}
}
}
返回结果默认是以_score
这个字段排序的,可以在sort
下指定返回结果的排序规则:
{
"query": {
"match": {
"title": "1"
}
},
"sort": [
{
"word_count": {
"order": "desc"
}
}
]
}
4.5.3)聚合查询
接下来使用聚合查询来实现对word_count
字段的分组查询,同样是URL为localhost:9200/article/_search
的POST请求。
aggs
表示这是一个聚合查询;group_by_word_count
是一个自定义的名称,返回结果将被包含在这个字段中返回;terms
表示要聚合的字段条目;field
指定要聚合的字段;聚合的条件可以有多个:
{
"aggs": {
"group_by_word_count": {
"terms": {
"field": "word_count"
}
},
"group_by_create_time": {
"terms": {
"field": "create_time"
}
}
}
}
这里截取主要的返回信息,可以看到返回结果按照word_count
和create_time
字段分组了,字数12000的记录有两条,2000的有一条:
"aggregations": {
"group_by_word_count": {
"doc_count_error_upper_bound": ,
"sum_other_doc_count": ,
"buckets": [
{
"key": 12000,
"doc_count": 2
},
{
"key": 2000,
"doc_count": 1
}
]
},
"group_by_create_time": {
"doc_count_error_upper_bound": ,
"sum_other_doc_count": ,
"buckets": [
{
"key": 1578009600000,
"key_as_string": "2020-01-03T00:00:00.000Z",
"doc_count": 3
}
]
}
}
我们还可以对字段进行统计,这里对word_count
字段进行统计,请求体如下:
{
"aggs": {
"calc_word_count": {
"stats": {
"field": "word_count"
}
}
}
}
可以看到ES对word_count
字段进行了详细的统计计算,我们可以把stats
替换成min
、max
、avg
及sum
来满足我们的各种需求:
"aggregations": {
"calc_word_count": {
"count": 3,
"min": 2000.0,
"max": 12000.0,
"avg": 8666.666666666666,
"sum": 26000.0
}
}
5)高级查询
5.1)match_phrase查询
如果我们想查询title
为“文章标题1”的文章数据,可能会用如下查询条件:
{
"query": {
"match": {
"title": "文章标题1"
}
}
}
但是返回的结果不是我们想的那样:
"hits": [
{
"_index": "article",
"_type": "_doc",
"_id": "2",
"_score": 1.4384104,
"_source": {
"title": "文章标题1",
"content": "文章内容1",
"word_count": 12000,
"create_time": "2020-01-03",
"tag": "java"
}
},
{
"_index": "article",
"_type": "_doc",
"_id": "fnxVnHEBWezSmd7dfZlz",
"_score": 1.1507283,
"_source": {
"title": "文章标题3",
"content": "文章内容3",
"word_count": 2000,
"create_time": "2020-01-03",
"tag": "python"
}
},
{
"_index": "article",
"_type": "_doc",
"_id": "fXwdnHEBWezSmd7dqZnb",
"_score": 0.72928625,
"_source": {
"title": "文章标题2",
"content": "文章内容2",
"word_count": 12000,
"create_time": "2020-01-03",
"tag": "go"
}
}
]
因为上面的匹配条件被拆分了,并不是整句查询的;要想达到我们的预期结果,需要使用match_phrase
来代替match
,这样结果就是我们想要的了:
"hits": [
{
"_index": "article",
"_type": "_doc",
"_id": "2",
"_score": 1.4384103,
"_source": {
"title": "文章标题1",
"content": "文章内容1",
"word_count": 12000,
"create_time": "2020-01-03",
"tag": "java"
}
}
]
5.2)multi_match查询
有时我们需要看多个字段的内容匹配是否匹配给定的关键字,而关键字内容都是一致的时候,我们可以使用multi_match
查询:
{
"query": {
"multi_match": {
"query": "文章",
"fields": ["title", "content"]
}
}
}
上面的条件就是查询title
和content
字段内容包含“文章”的记录,只要title
或content
包含“文章”就可以返回对应数据。
5.3) query_string查询
我们使用query_string
查询包含“文章”和数字2的数据,它们是“且”的关系,在查询条件中使用“AND”连接(支持逻辑运算符和小括号),表示需要同时包含才算匹配:
{
"query": {
"query_string": {
"query": "文章 AND 2"
}
}
}
返回结果:
"hits": [
{
"_index": "article",
"_type": "_doc",
"_id": "fXwdnHEBWezSmd7dqZnb",
"_score": 1.0577903,
"_source": {
"title": "文章标题2",
"content": "文章内容2",
"word_count": 12000,
"create_time": "2020-01-03",
"tag": "go"
}
}
]
还可以指定要查询的字段:
{
"query": {
"query_string": {
"query": "(文章 AND 2) OR python",
"fields": ["title","content", "tag"]
}
}
}
5.4)term字段查询
我们可以使用term
指定字段查询,注意只能指定一个字段:
{
"query": {
"term": {
"word_count": 2000
}
}
}
5.5)range范围查询
使用range
对(数值、日期)字段指定范围查询:
{
"query": {
"range": {
"word_count": {
"gte": 1000,
"lte": 2000
}
}
}
}
这样就能查出word_count
字段值>=1000且<=2000的数据,可以使用gt
、lt
、gte
和lte
对范围进行筛选。
5.6)filter查询
使用filter查询可以对不满足指定条件的数据进行过滤,这里表示只有word_count
字段为2000的数据才进行返回:
{
"query": {
"bool": {
"filter": {
"term": {
"word_count": 2000
}
}
}
}
}
5.7)should查询
should查询表现出的是OR
的关系,它表示只要满足给定的任意一个条件即可返回数据:
{
"query": {
"bool": {
"should": [
{
"match": {
"title": "1"
}
},
{
"match": {
"title": "不存在"
}
}
]
}
}
}
5.8)must查询
与should对应,must是AND
的关系,表示给定的条件需要全部匹配才返回数据:
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "1"
}
},
{
"match": {
"title": "文章"
}
}
]
}
}
}
我们还可以结合filter查询来添加过滤条件,文章标题查询条件满足后,还要对返回结果过滤出word_count
等于12000的:
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "1"
}
},
{
"match": {
"title": "文章"
}
}
],
"filter": [
{
"term": {
"word_count": 12000
}
}]
}
}
}
5.9)must_not查询
如果我们希望找出给定条件之外的数据,可以使用must_not
查询,表示返回不匹配给定条件的数据;这里找到title
中不包含“2”的所有数据:
{
"query": {
"bool": {
"must_not": {
"term": {
"title": 2
}
}
}
}
}