01、课程目标
- 掌握Kibana的基本操作
- 掌握SpringDataElasticsearch常用操作
02、Elasticsearch准备:ElasticSearch工具安装
1)安装ElasticSearch服务器
把 elasticsearch-6.6.2.zip 解压到没有中文的路径下,然后再把ik插件解压缩,拷贝到es的plugins目录下,启动前,可以修改es的config目录下的jvm.options文件,修改内存大小:默认值是1g,我们改小点,128m或者256m都可以,如果你的电脑内存够,不改也行。
链接:https://pan.baidu.com/s/1TUuc-mfnXS-QjA1QiPUmYQ
提取码:dqol
链接:https://pan.baidu.com/s/1weqAj2UzqjdCjlJgOGbCXw
提取码:agao
2)安装ik分词器
把以上目录解压到ES的plugins目录下即可。
3)安装head插件
链接:https://pan.baidu.com/s/1V6jx_Z6efV9IC1Ps6SYy2Q
提取码:1pki
head插件有安装版、tomcat版和chrome的插件版,我们采用插件版即可。
在谷歌中按照head插件的流程如下图所示:
4)安装kibana
什么是Kibana? logstash + Elasticsearch + Kibana ELK 日志监听系统
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。
而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。
Kibana的安装
启动kibana的前提是,需要安装好nodejs的环境。
解压缩就可以使用了,双击解压后的bin目录的kibana.bat文件即可。
链接:https://pan.baidu.com/s/1qi4i0oAwtdrLIkM6NIbmhw
提取码:eu8j
发现kibana的监听端口是5601
我们访问:http://127.0.0.1:5601
5)Elasticsearch API
Elasticsearch提供了Rest风格的API,即http请求接口,也提供了各种语言的客户端API
Rest风格API
文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
各种语言客户端API
Elasticsearch支持的客户端非常多:https://www.elastic.co/guide/en/elasticsearch/client/index.html
点击Java Rest Client后,你会发现又有两个:
Low Level Rest Client是低级别封装,提供一些基础功能,但更灵活
High Level Rest Client,是在Low Level Rest Client基础上进行的高级别封装,功能更丰富和完善,而且API会变的简单
今天的学习思路,我们先补充ES的REST风格API,然后再学习SpringDataElasticsearch(对Java语言客户端API的封装)
03、Elasticsearch准备:搜索相关概念回顾
1)结构回顾
索引、类型、文档、域
2)文档对象的域字段需要设置的三个属性
是否索引
索引的字段才能被搜索到,不索引,就意味着不被搜索匹配。
日期,数量,点击数不索引。
是否分词
分词就意味着要先分词,再拿分词后的词条匹配搜索结果。
如果不分词,就直接把当前搜索的内容当作一个词条。
人名,地名都不分词。
是否存储
存储就意味着要显示,不存储不显示。
但是如果使用了elasticSearch,默认是所有字段全部存储。
被标记为store=true的是存储在索引库【lucene定义的】中。
如果没有store=true标记,存储在elasticSearch自己的库中。
这些三部分内容我们都可以通过mapping来定义:
Mapping是用来定义Document中每个Field的特征的(数据类型,是否存储,是否索引,是否分词等)
类型名称:就是前面将的type的概念,类似于数据库中的表
字段名:任意填写,下面指定许多属性,例如:
- type:类型,可以是text、long、short、date、integer、object等
- index:是否索引,默认为true
- store:是否存储,默认为false
- analyzer:分词器,这里的ik_max_word即使用ik分词器
04、ES的REST风格API:基本CRUD(了解)
接下来我们先学习ES的REST风格API如何操作ES。
1)创建索引
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求
创建索引的请求格式:
-
请求方式:PUT
-
请求路径:/索引库名
-
请求参数:json格式:
{ "settings": { "number_of_shards": 3, "number_of_replicas": 2 } }
- settings:索引库的设置
- number_of_shards:分片数量
- number_of_replicas:副本数量
- settings:索引库的设置
kibana的控制台,可以对http请求进行简化,示例:
创建goods索引:
相当于是省去了elasticsearch的服务器地址
而且还有语法提示,非常舒服。
2)删除索引
删除索引使用DELETE请求
语法
DELETE /索引库名
示例
查看goods索引库是否存在:
当然,我们也可以用HEAD请求,查看索引是否存在:
3)新增数据
随机生成id
通过POST请求,可以向一个已经存在的索引库中添加数据。
语法:
POST /索引库名/类型名
{
"key":"value"
}
示例:
POST /goods/docs
{
"title":"小米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":2699.00
}
响应:
{
"_index" : "goods",
"_type" : "docs",
"_id" : "QGGcR3QBMpeF1wVyBCzk",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
通过kibana查看数据:
GET /goods/_search
{
"query": {
"match_all": {
}
}
}
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "QGGcR3QBMpeF1wVyBCzk",
"_score" : 1.0,
"_source" : {
"title" : "小米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
}
]
}
}
_source
:源文档信息,所有的数据都在里面。_id
:这条文档的唯一标示,与文档自己的id字段没有关联
自定义id
如果我们想要自己新增的时候指定id,可以这么做:
POST /索引库名/类型/id值
{
...
}
示例:
POST /goods/docs/2
{
"title":"大米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":2899.00
}
得到的数据:
{
"_index": "goods",
"_type": "docs",
"_id": "2",
"_score": 1,
"_source": {
"title": "大米手机",
"images": "http://image.leyou.com/12479122.jpg",
"price": 2899
}
}
4)修改数据
把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,
- id对应文档存在,则修改
- id对应文档不存在,则新增
比如,我们把id为3的数据进行修改:
PUT /goods/docs/2
{
"title":"超大米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":3899.00,
"stock": 100,
"saleable":true
}
结果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"title" : "超大米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 3899.0,
"stock" : 100,
"saleable" : true
}
}
]
}
}
5)删除数据
删除使用DELETE请求,同样,需要根据id进行删除:
语法
DELETE /索引库名/类型名/id值
示例:
# 创建索引库
PUT /goods
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
# 删除索引库
DELETE /goods
#插入文档
# 随机生成文档ID
POST /goods/docs
{
"title":"小米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":2699.00
}
# 自行赋值文档ID
POST /goods/docs/2
{
"title":"华为手机",
"images":"http://image.leyou.com/12479123.jpg",
"price":3699.00
}
# 修改文档(修改时必须有文档ID)
PUT /goods/docs/2
{
"title":"华为手机",
"images":"http://image.leyou.com/12479123.jpg",
"price":4699.00
}
# 删除文档
DELETE /goods/docs/2
05、ES的REST风格API:基本查询(了解)
1)查询基本语法
基本语法
GET /索引库名/_search
{
"query":{
"查询类型":{
"查询条件":"查询条件值"
}
}
}
这里的query代表一个查询对象,里面可以有不同的查询属性
- 查询类型:
- 例如:
match_all
,match
,term
,range
等等
- 例如:
- 查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解
导入测试数据:
PUT /goods
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
POST /goods/docs/1
{
"title":"小米Redmi K30",
"subTitle":"小米手机 120Hz流速屏,王一博同款火爆开售",
"category":"手机",
"price": 1500
}
POST /goods/docs/2
{
"title":"小米Redmi 8A",
"subTitle":"小米手机 5000mAh大电量,支持18W快充,Type-C充电接口,6.22英寸水滴屏",
"category":"手机",
"price": 700
}
POST /goods/docs/3
{
"title":"小米电视",
"subTitle":"高清教育互联",
"category":"电视",
"price": 3500
}
POST /goods/docs/4
{
"title":"华为P90",
"subTitle":"华为P90 高端大气上档次",
"category":"手机",
"price":2500
}
POST /goods/docs/5
{
"title":"小米电视P10",
"subTitle":"小米电视P10 高端大气上档次",
"category":"电视",
"price":2900
}
2)查询所有(match_all)
示例:
GET /goods/_search
{
"query":{
"match_all": {}
}
}
query
:代表查询对象match_all
:代表查询所有
结果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "QGGcR3QBMpeF1wVyBCzk",
"_score" : 1.0,
"_source" : {
"title" : "小米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
}
]
}
}
- took:查询花费时间,单位是毫秒
- time_out:是否超时
- _shards:分片信息
- hits:搜索结果总览对象
- total:搜索到的总条数
- max_score:所有结果中文档得分的最高分
- hits:搜索结果的文档对象数组,每个元素是一条搜索到的文档信息
- _index:索引库
- _type:文档类型
- _id:文档id
- _score:文档得分
- _source:文档的源数据
3)匹配查询(match)
匹配查询(会先分词再匹配):
GET /goods/_search
{
"query": {
"match": {
"title": "小米手机"
}
}
}
结果:
{
"took" : 15,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.1507283,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "QGGcR3QBMpeF1wVyBCzk",
"_score" : 1.1507283,
"_source" : {
"title" : "小米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
},
{
"_index" : "goods",
"_type" : "docs",
"_id" : "3",
"_score" : 0.5753642,
"_source" : {
"title" : "小米电视4A",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 3899.0
}
}
]
}
}
在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是or
的关系。
- and关系
某些情况下,我们需要更精确查找,我们希望这个关系变成and
,可以这样做:
GET /heima/_search
{
"query":{
"match": {
"title": {
"query": "小米电视",
"operator": "and"
}
}
}
}
结果:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 3,
"successful": 3,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.5753642,
"hits": [
{
"_index": "heima",
"_type": "goods",
"_id": "3",
"_score": 0.5753642,
"_source": {
"title": "小米电视4A",
"images": "http://image.leyou.com/12479122.jpg",
"price": 3899
}
}
]
}
}
本例中,只有同时包含小米
和电视
的词条才会被搜索到。
4)多字段查询(multi_match)
multi_match
与match
类似,不同的是它可以在多个字段中查询
GET /goods/_search
{
"query": {
"multi_match": {
"query": "小米高端大气",
"fields": ["title","subTitle"]
}
}
}
# 查询全部
GET /goods/_search
{
"query": {
"match_all": {}
}
}
# 匹配查询(match ,先分词再查询)
GET /goods/_search
{
"query": {
"match": {
"title": "小米电视"
}
}
}
# 匹配查询(match + or/and )
GET /goods/_search
{
"query": {
"match": {
"title": {
"query": "小米电视",
"operator": "and"
}
}
}
}
# 多字段匹配(multi_match)
GET /goods/_search
{
"query": {
"multi_match": {
"query": "小米",
"fields": ["title","subTitle"]
}
}
}
5)词条匹配(term)
term
查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串
GET /goods/_search
{
"query": {
"term": {
"price": {
"value": 2500
}
}
}
}
结果:
{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"title" : "华为P90",
"subTitle" : "华为P90 高端大气上档次",
"category" : "手机",
"price" : 2500
}
}
]
}
}
6)多词条精确匹配(terms)
terms
查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:
GET /goods/_search
{
"query": {
"terms": {
"price": [
4899.0,
2699.0
]
}
}
}
结果:
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"title" : "华为手机",
"subTitle" : "小米",
"images" : "http://image.leyou.com/12479124.jpg",
"price" : 4899.0
}
},
{
"_index" : "goods",
"_type" : "docs",
"_id" : "QGGcR3QBMpeF1wVyBCzk",
"_score" : 1.0,
"_source" : {
"title" : "小米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
}
]
}
}
7)结果过滤(_source)
默认情况下,elasticsearch在搜索的结果中,会把文档中保存在_source
的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加_source
的过滤
直接指定字段
示例:
GET /goods/_search
{
"query": {
"term": {
"price": 4899.0
}
},
"_source": ["title","price"]
}
返回的结果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"price" : 4899.0,
"title" : "华为手机"
}
}
]
}
}
指定includes和excludes
我们也可以通过:
- includes:来指定想要显示的字段
- excludes:来指定不想要显示的字段
二者都是可选的。
includes示例:
GET /goods/_search
{
"query": {
"term": {
"price": 4899.0
}
},
"_source":{
"includes": ["title","subTitle"]
}
}
excludes示例:
GET /goods/_search
{
"query": {
"term": {
"price": 4899.0
}
},
"_source":{
"excludes": ["images"]
}
}
8)布尔组合(bool)
bool
把各种其它查询通过must
(与)、must_not
(非)、should
(或)的方式进行组合
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米"
}
},
{
"match": {
"subTitle": "小米"
}
}
]
}
}
}
结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.18232156,
"hits" : [
{
"_index" : "item",
"_type" : "docs",
"_id" : "2",
"_score" : 0.18232156,
"_source" : {
"id" : 2,
"title" : "坚果手机R1",
"category" : "手机",
"brand" : "锤子",
"price" : 3699.0,
"images" : "http://image.leyou.com/123.jpg"
}
},
{
"_index" : "item",
"_type" : "docs",
"_id" : "3",
"_score" : 0.18232156,
"_source" : {
"id" : 3,
"title" : "华为META10",
"category" : "手机",
"brand" : "华为",
"price" : 4499.0,
"images" : "http://image.leyou.com/3.jpg"
}
}
]
}
}
9)过滤条件(filter)
所有的查询都会影响到文档的评分及排名。如果我们需要在查询结果中进行过滤,并且不希望过滤条件影响评分,那么就不要把过滤条件作为查询条件来用。而是使用filter
方式:
注意:filter
通常和bool一起使用(在bool基础上进行过滤)。
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米"
}
},
{
"match": {
"subTitle": "小米"
}
}
],
"filter": {
"term": {
"category.keyword": "手机"
}
}
}
}
}
# 查询所有
GET /goods/_search
{
"query": {
"match_all": {}
}
}
# 匹配查询(模糊查询)(先分词再查询)
GET /goods/_search
{
"query": {
"match": {
"title": "小米"
}
}
}
# 多字段匹配查询(模糊查询)
GET /goods/_search
{
"query": {
"multi_match": {
"query": "小米",
"fields": ["title","subTitle"]
}
}
}
# 词条搜素(精确搜素)(不分词搜素)
GET /goods/_search
{
"query": {
"term": {
"price": {
"value": 700
}
}
}
}
# 多词条搜索
GET /goods/_search
{
"query": {
"terms": {
"price": [
700,
2500
]
}
}
}
# 结果过滤
# 语法1
GET /goods/_search
{
"query": {
"match_all": {}
},
"_source": ["title","price"]
}
# 语法2
GET /goods/_search
{
"query": {
"match_all": {}
},
"_source":{
"excludes": ["subTitle"]
}
}
# 布尔组合
# 把分词的Field转换为不分词的Field: filed.keyword
# 使用布尔的条件
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米"
}
},
{
"term": {
"category.keyword": {
"value": "手机"
}
}
}
]
}
}
}
# 使用布尔的过滤
# 注意:如果我们使用过滤条件,那么过滤后的结果不会影响原来结果的排序。反之,如果不用过滤,则会影响原来结果排序
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米"
}
}
],
"filter":{
"term": {
"category.keyword": "手机"
}
}
}
}
}
06、ES的REST风格API:聚合查询(aggregations)(了解)
聚合查询就是分组统计
聚合可以让我们极其方便的实现对数据的统计、分析。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现实时搜索效果。
1)聚合基本概念
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫桶
,一个叫度量
:
桶(bucket)
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个桶
,例如我们根据国籍对人划分,可以得到中国桶
、英国桶
,日本桶
……或者我们按照年龄段对人进行划分:010,1020,2030,3040等。
Elasticsearch中提供的划分桶的方式有很多:
- Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
- Histogram Aggregation:根据数值阶梯分组,与日期类似
- Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组
- Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
- ……
bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
度量(metrics)
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量
比较常用的一些度量聚合方式:
- Avg Aggregation:求平均值
- Max Aggregation:求最大值
- Min Aggregation:求最小值
- Percentiles Aggregation:求百分比
- Stats Aggregation:同时返回avg、max、min、sum、count等
- Sum Aggregation:求和
- Top hits Aggregation:求前几
- Value Count Aggregation:求总数
- ……
为了测试聚合,我们先批量导入一些数据
创建索引:
PUT /cars
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"transactions": {
"properties": {
"color": {
"type": "keyword"
},
"make": {
"type": "keyword"
}
}
}
}
}
注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词。这里我们将color和make这两个文字类型的字段设置为keyword类型,这个类型不会被分词,将来就可以参与聚合
导入数据
POST /cars/transactions/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }
2)聚合为桶 (Aggragation for Buket)
聚合为桶其实就是分组统计!
首先,我们按照 汽车的颜色color
来划分桶
GET /cars/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
- size: 查询条数,这里设置为0,因为我们不关心搜索到的数据,只关心聚合结果,提高效率
- aggs:声明这是一个聚合查询,是aggregations的缩写
- popular_colors:给这次聚合起一个名字,任意。
- terms:划分桶的方式,这里是根据词条划分
- field:划分桶的字段
- terms:划分桶的方式,这里是根据词条划分
- popular_colors:给这次聚合起一个名字,任意。
结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 8,
"max_score": 0,
"hits": []
},
"aggregations": {
"popular_colors": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "red",
"doc_count": 4
},
{
"key": "blue",
"doc_count": 2
},
{
"key": "green",
"doc_count": 2
}
]
}
}
}
- hits:查询结果为空,因为我们设置了size为0
- aggregations:聚合的结果
- popular_colors:我们定义的聚合名称
- buckets:查找到的桶,每个不同的color字段值都会形成一个桶
- key:这个桶对应的color字段的值
- doc_count:这个桶中的文档数量
通过聚合的结果我们发现,目前红色的小车比较畅销!
# 聚合查询(分组统计)
# AGG_TYPE: 聚合类型
# 常见的聚合类型
# terms: 按数量聚合(统计),类似SQL的count(*)
# avg: 按均值聚合(统计),类似SQL的avg(age)
# max: 按最大值聚合(统计),类似SQL的max(age)
# min: 按最小聚合(统计),类似SQL的min(age)
# sum: 按求和聚合(统计),类似SQL的sum(score)
#统计不同颜色的汽车分别有多少部?
GET /cars/_search
{
"aggs": {
"colorAgg": {
"terms": {
"field": "color"
}
}
}
}
# 统计所有汽车的均价
GET /cars/_search
{
"aggs": {
"priceAgg": {
"avg": {
"field": "price"
}
}
}
}
#查询honda的汽车的均价?
GET /cars/_search
{
"query": {
"term": {
"make": {
"value": "honda"
}
}
},
"aggs": {
"priceAgg": {
"avg": {
"field": "price"
}
}
}
}
# 统计不同颜色的汽车的均价分别是多少?
GET /cars/_search
{
"aggs": {
"colorAgg": {
"terms": {
"field": "color"
},
"aggs": {
"priceAgg": {
"avg": {
"field": "price"
}
}
}
}
}
}
07、SpringDataElasticsearch:搭建项目(重点)
1)SpringDataElasticsearch简介
Spring Data Elasticsearch是Spring Data项目下的一个子模块。
查看 Spring Data的官网:http://projects.spring.io/spring-data/
Spring Data的使命是为数据访问提供熟悉且一致的基于Spring的编程模型,同时仍保留底层数据存储的特殊特性。
它使得使用数据访问技术,关系数据库和非关系数据库,map-reduce框架和基于云的数据服务变得容易。这是一个总括项目,其中包含许多特定于给定数据库的子项目。这些令人兴奋的技术项目背后,是由许多公司和开发人员合作开发的。
Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提高开发效率。
包含很多不同数据操作的模块:
Spring Data Elasticsearch的页面:https://projects.spring.io/spring-data-elasticsearch/
特征:
- 支持Spring的基于
@Configuration
的java配置方式,或者XML配置方式 - 提供了用于操作ES的便捷工具类**
ElasticsearchTemplate
**。包括实现文档到POJO之间的自动智能映射。 - 利用Spring的数据转换服务实现的功能丰富的对象映射
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
2)搭建SpringDataElasticsearch环境
创建项目及导入依赖
我们使用spring脚手架新建一个demo,学习Elasticsearch
pom依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>spring-data-es</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!--spring data es-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
application.yml配置
spring:
data:
elasticsearch:
cluster-name: elasticsearch # 集群名称
cluster-nodes: 127.0.0.1:9300 # 节点地址
编写启动类
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
*/
@SpringBootApplication
public class ESApplication {
public static void main(String[] args) {
SpringApplication.run(ESApplication.class,args);
}
}
实体类及注解
首先我们准备好实体类:
public class Item {
Long id;
String title; //标题
String category;// 分类
String brand; // 品牌
Double price; // 价格
String images; // 图片地址
}
映射
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document
作用在类,标记实体类为文档对象,一般有四个属性- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认5
- replicas:副本数量,默认1
@Id
作用在成员变量,标记一个字段作为id主键@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:- type:字段类型,取值是枚举:FieldType
- index:是否索引,布尔类型,默认是true
- store:是否存储,布尔类型,默认是false
- analyzer:分词器名称:ik_max_word
示例:
package com.itheima.pojo;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
/**
* @Document: 映射索引库中的文档
* indexName: 索引库名称
* type: 类型
* shards:分片
* replicas:副本
* @Id: 映射文档ID
* @Field
* type: 字段的类型
* Text: 文本类型,分词的
* Keyword: 文本类型,不分词的
* Integer、Long、Float、Double: 数值类型,必须不分词的
* Boolean:布尔类型,必须不分词的
* Date: 日期类型,必须不分词的
* Object: 自定义对象或集合(List,Set,Map等),所有对象里面的属性都是索引和分词的
* index:
* 该字段是否索引 默认为true
* analyzer
* 指定分词器的算法
* 如:ik分词器的算法
* ik_smart: 最小分词 我是程序员 -> 我 是 程序员
* ik_max_word: 最细分词 我是程序员 -> 我 是 程序员 程序 员
*/
@Data
@Document(indexName = "goods",type = "docs" ,shards = 1,replicas = 1)
public class Item {
@Id
Long id;
@Field(type = FieldType.Text,analyzer ="ik_max_word" )
String title; //标题
@Field(type = FieldType.Keyword)
String category;// 分类
@Field(type = FieldType.Keyword)
String brand; // 品牌
@Field(type = FieldType.Double)
Double price; // 价格
@Field(type = FieldType.Keyword,index = false)
String images; // 图片地址
}
08、SpringDataElasticsearch:基本CRUD(重点)
1)编写Repository接口
Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
我们只需要定义接口,然后继承它就OK了。
package com.ithiema.repository;
import com.ithiema.pojo.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* Dao接口
* 泛型一:操作的实体类类型
* 泛型二:实体类的ID类型
*/
public interface ItemRepository extends ElasticsearchRepository<Item,Long>{
}
来看下Repository的继承关系:
我们看到有一个ElasticsearchRepository接口:
2)新增文档
package com.itcast;
import com.itcast.pojo.Item;
import com.itcast.repository.ItemRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class EsDemo1 {
@Autowired
private ItemRepository itemRepository;
@Test
public void save(){
Item item = new Item(1L, "小米手机7", "手机",
"小米", 3499.00, "http://image.leyou.com/13123.jpg");
itemRepository.save(item);
}
}
去页面查询看看:
GET /goods/_search
结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "docs",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"id" : 1,
"title" : "小米手机7",
"category" : "手机",
"brand" : "小米",
"price" : 3499.0,
"images" : "http://image.leyou.com/13123.jpg"
}
}
]
}
}
3)批量新增
代码:
/**
* 批量新增索引
*/
@Test
public void testBatchSave(){
List<Item> list = new ArrayList<>();
list.add(new Item(2L, "坚果手机R1", "手机","锤子", 3699.00, "http://image.leyou.com/13124.jpg"));
list.add(new Item(3L, "华为META10", "手机","华为", 4499.00, "http://image.leyou.com/13125.jpg"));
list.add(new Item(4L, "小米电视1", "电视","小米", 5499.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(5L, "小米手机8", "手机","小米", 2199.00, "http://image.leyou.com/13124.jpg"));
itemRepository.saveAll(list);
}
4)修改文档
修改和新增是同一个接口,区分的依据就是id,这一点跟我们在页面发起PUT请求是类似的。
/**
* 修改索引
*/
@Test
public void testUpdate(){
Item item = new Item(1L, "小米手机8", "手机",
"小米", 4499.00, "http://image.leyou.com/13124.jpg");
itemRepository.save(item);
}
5)简单查询
ElasticsearchRepository提供了一些基本的查询方法:
我们来试试查询所有:
/**
* 查询所有
*/
@Test
public void testFindAll(){
Iterable<Item> list = itemRepository.findAll();
list.forEach(System.out::println);
}
/**
* 根据id查询
*/
@Test
public void testFindById(){
Item item = itemRepository.findById(1L).get();
System.out.println(item);
}
结果:
6)删除文档
/**
* 删除索引
*/
@Test
public void testDelete(){
Item item = new Item();
item.setId(1L);
itemRepository.delete(item);
}
package com.itheima;
import com.itheima.pojo.Item;
import com.itheima.repository.ItemRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
/***
* 基本CRUD
*/
//@RunWith(SpringJUnit4ClassRunner.class)
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class) //加载启动类
public class ESDemo1 {
@Autowired
private ItemRepository itemRepository;
/**
* 增加、修改
*/
@Test
public void testSave(){
Item item = new Item(1L, "小米手机7", "手机",
"小米", 3499.00, "http://image.leyou.com/13123.jpg");
itemRepository.save(item);
}
/**
* 批量增加
*/
@Test
public void testSaveAll(){
List<Item> list = new ArrayList<>();
list.add(new Item(2L, "坚果手机R1", "手机","锤子", 3699.00, "http://image.leyou.com/13124.jpg"));
list.add(new Item(3L, "华为META10", "手机","华为", 4499.00, "http://image.leyou.com/13125.jpg"));
list.add(new Item(4L, "小米电视1", "电视","小米", 5499.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(5L, "小米手机8", "手机","小米", 2199.00, "http://image.leyou.com/13124.jpg"));
itemRepository.saveAll(list);
}
/**
* 查询所有
*/
@Test
public void testFindAll(){
Iterable<Item> it = itemRepository.findAll();
for(Item item:it){
System.out.println(item);
}
}
@Test
public void testFindById(){
Item item = itemRepository.findById(3L).get();
System.out.println(item);
}
@Test
public void testDeleteById(){
itemRepository.deleteById(2L);
}
}
09、SpringDataElasticsearch:高级查询(重点)
如果要完成更加复杂的查询,同时包含条件,分页,高亮,聚合等操作,需要用到ElasticsearchTemplate对象
1)基本条件查询
# 基本查询
GET /goods/_search
{
"query": {
"match": {
"title": "小米"
}
}
}
@Test
public void test1(){
//1.创建本地查询构造器对象: 用于封装所有查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//添加query条件
//注意:所有的Query条件都是使用QueryBuilders类构造的
queryBuilder.withQuery(QueryBuilders.matchQuery("title","小米"));
//2.执行查询(执行本地查询),获取结果
/**
* 参数一:本地查询对象
* 参数二:需要封装数据的实体类
*/
List<Item> items = esTemplate.queryForList(queryBuilder.build(), Item.class);
//3.处理结果
items.forEach(System.out::println);
}
2)分页查询
# 分页
# from: 起始索引行号,从0开始
# size: 页面大小
GET /goods/_search
{
"query": {
"match": {
"title": "小米"
}
},
"from": 0 ,
"size": 2
}
/**
* 2)分页条件查询
*/
@Test
public void testPageQuery(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//1. 添加Query条件
queryBuilder.withQuery( QueryBuilders.matchQuery("title","小米") );
//实际页面传递的参数
int page = 1;//页面传递的当前页码
int size = 2;//页面大小
//2.添加分页参数
/**
* page: ES的当前页码(page从0开始计算)
* size: 页面大小
*/
queryBuilder.withPageable(PageRequest.of(page-1,size));
//Page: 用于封装分页查询结果
Page<Item> pageBean = esTemplate.queryForPage(queryBuilder.build(),Item.class);
System.out.println("总记录数:"+pageBean.getTotalElements());
System.out.println("总页数:"+pageBean.getTotalPages());
pageBean.getContent().forEach(System.out::println);
}
3)布尔组合+过滤查询(*)
# 布尔+过滤
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米"
}
}
],
"filter": {
"term": {
"category": "手机"
}
}
}
}
}
/**
* 3)布尔+过滤
*/
@Test
public void testBoolQueryAndFilter(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//1.1 添加Query条件
//1)创建布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//2)往布尔查询中添加must条件
boolQueryBuilder.must( QueryBuilders.matchQuery("title","小米") );
//3)往布尔查询中添加filter过滤
boolQueryBuilder.filter( QueryBuilders.termQuery("category","手机") );
queryBuilder.withQuery( boolQueryBuilder );
List<Item> items = esTemplate.queryForList(queryBuilder.build(), Item.class);
items.forEach(System.out::println);
}
4)聚合查询(*)
# 统计不同品牌的手机分别有多少台?
# 聚合查询
GET /goods/_search
{
"aggs": {
"brandAgg": {
"terms": {
"field": "brand"
}
}
}
}
/**
* 4)聚合查询
*/
@Test
public void testAggQuery(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//1.添加聚合条件
//注意:聚合的条件都是由AggrationBuilders构建而来的
queryBuilder.addAggregation( AggregationBuilders.terms("brandAgg").field("brand") );
//注意:执行聚合查询必须使用queryForPage方法,否则无法获取聚合结果
//AggregatedPage是Page的子类,Page只是分页查询结果,AggregatedPage既封装了分页结果,也封装了聚合查询
AggregatedPage<Item> aggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Item.class);
//只取出聚合结果
Aggregations aggregations = aggregatedPage.getAggregations();
//根据聚合别名取出聚合结果
Terms terms = aggregations.get("brandAgg");
//取出所有Bucket
List<? extends Terms.Bucket> buckets = terms.getBuckets();
//遍历Bucket
for(Terms.Bucket bucket:buckets){
String brandName = bucket.getKeyAsString();
long count = bucket.getDocCount();
System.out.println(brandName+"\t"+count);
}
}
/**
* 4)聚合查询取出聚合查询里边的的数据 进行2次分组
*/
@Test
public void testAggQuery2(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//1.添加聚合条件
//注意:聚合的条件都是由AggregationBuilders构建而来的
queryBuilder.addAggregation(AggregationBuilders.terms("brandAggs").field("brand")
.subAggregation(AggregationBuilders.terms("priceAggs").field("price")));
//注意:执行聚合查询必须使用queryForPage方法,否则无法获取聚合结果
AggregatedPage<Item> aggregatedPage = esTemplate.queryForPage(queryBuilder.build(), Item.class);
//AggregatedPage是Page的子类,Page只是分页查询结果,AggregatedPage既封装了分页结果,也封装了聚合查询
//只取出聚合结果
Aggregations aggregations = aggregatedPage.getAggregations();
//根据聚合别名取出聚合结果
Terms terms = aggregations.get("brandAggs");
//取出所有Bucket
List<? extends Terms.Bucket> buckets = terms.getBuckets();
//遍历Bucket
for (Terms.Bucket bucket : buckets) {
String brandName = bucket.getKeyAsString();
long count = bucket.getDocCount();
Aggregations aggregations1 = bucket.getAggregations();
Terms priceAggs = aggregations1.get("priceAggs");
List<? extends Terms.Bucket> buckets1 = priceAggs.getBuckets();
Terms.Bucket bucket1 = buckets1.get(0);
System.out.println(brandName+"\t"+count+"\t"+bucket1.getKey());
}
}
5)高亮查询(*)
DSL语句:
# 高亮显示
# 注意:如果要某字段高亮显示,该字段必须是参与搜索,且可以分词
GET /goods/_search
{
"query": {
"match": {
"title": "小米"
}
},
"highlight": {
"pre_tags": "<font color='red'>",
"post_tags": "</font>",
"fields": {
"title": {}
}
}
}
/**
* 5)高亮查询
*/
@Test
public void testHighlightQuery(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//1.添加Query条件(title要高亮,所以title参与搜索)
queryBuilder.withQuery( QueryBuilders.matchQuery("title","小米") );
//2.添加highlight高亮字段
HighlightBuilder.Field field = new HighlightBuilder.Field("title");
//设置前缀和后缀
field.preTags("<font color='red'>");
field.postTags("</font>");
queryBuilder.withHighlightFields(field);
//3.执行查询
//SearchResultMapper: 用于自行封装搜索结果
AggregatedPage<Item> pageBean = esTemplate.queryForPage(queryBuilder.build(), Item.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<T> list = new ArrayList<>();
SearchHits hits = response.getHits();
for(SearchHit hit:hits){
//取出原来的文档内容
String json = hit.getSourceAsString();
//把json字符串转换对象
Item item = JsonUtils.toBean(json, Item.class);
//自行取出高亮title的内容
HighlightField titleField = hit.getHighlightFields().get("title");
if(titleField!=null){
//赋值给Item对象的title
item.setTitle(titleField.getFragments()[0].toString());
}
list.add((T)item);
}
AggregatedPage<T> aggregatedPage = new AggregatedPageImpl<>(list);
return aggregatedPage;
}
});
pageBean.getContent().forEach(System.out::println);
}
结果为:
Item(id=3, title=<font color='red'>华为</font>META10, category= 手机, brand=华为, price=4499.0, images=http://image.leyou.com/3.jpg)
加入HighlightUtils工具类,简化后:
/**
* 5)高亮查询 - 使用工具类
*/
@Test
public void testHighlightQuery2(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//1.添加Query条件(title要高亮,所以title参与搜索)
queryBuilder.withQuery( QueryBuilders.matchQuery("title","小米") );
//2.添加highlight高亮字段
HighlightUtils.highlightField(queryBuilder,"title");
//3.执行查询
//SearchResultMapper: 用于自行封装搜索结果
AggregatedPage<Item> pageBean = esTemplate.queryForPage(
queryBuilder.build(),
Item.class,
HighlightUtils.highlightBody(Item.class,"title"));
pageBean.getContent().forEach(System.out::println);
}
…