目录
一、DSL查询文档
- 文本检索:match_all、match、multi_match
- 精确查询:term、range
- 地理坐标查询:geo_distance
- 复合查询:function_score、bool
1. 说明
查询语法:
GET /索引库名/_search
{
"query": {}, #放查询条件
"sort": [], #放排序条件
"from": 起始索引, #放分页的起始索引
"size": 查询数量, #放分页的查询几条
"highlight": {}, #放高亮字段
"aggs": {}, #放聚合分组条件
"suggest": {} #放搜索提示条件
}
1 查询功能分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
-
查询所有:查询出所有数据,一般测试用。例如:match_all
-
全文本检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
-
match
-
multi_match
-
-
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
-
range
-
term
-
-
地理(geo)查询:根据经纬度查询。例如:
-
geo_distance
-
geo_bounding_box
-
-
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
-
bool
-
function_score
-
2 查询语法说明
查询的语法基本一致:
GET /索引名称/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
我们以查询所有为例,其中:
-
查询类型为match_all
-
没有查询条件
# 查询所有
GET /索引名称/_search
{
"query": {
"match_all": {
}
}
}
其它查询无非就是查询类型、查询条件的变化。
2. 文本检索
全文检索查询的基本流程如下:
-
对用户搜索的内容做分词,得到词条
-
根据词条去倒排索引库中匹配,得到文档id
-
根据文档id找到文档,返回给用户
比较常用的场景包括:
-
商城的输入框搜索
-
百度输入框搜索
例如京东:
因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段。
语法
常见的全文检索查询包括:
-
match查询:单字段查询
-
multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件。注意:字段越多,性能越差
match
match查询语法如下:
GET /索引名称/_search
{
"query": {
"match": {
"字段": "搜索值"
}
}
}
mulit_match
mulit_match语法如下:
GET /索引名称/_search
{
"query": {
"multi_match": {
"query": "搜索值",
"fields": ["字段1", "字段2", ...]
}
}
}
示例
#match 单字段检索
GET /hotel/_search
{
"query": {
"match": {
"all": "北京如家"
}
}
}#multi_match多字段检查
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "北京如家",
"fields": ["name", "brand", "business"]
}
}
}
注意
多字段检索的性能问题:使用多字段检索时,字段越多,检索的性能越差
解决方案:使用copy_to
,把多个字段值拷贝到一个字段里,对这个字段用match
检索,可以达到同样效果,而且效率更高
3. 精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
-
term:根据词条精确值查询
因为精确查询是不分词的,所有查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。
-
range:根据值的范围查询
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
语法
term查询
# term查询
GET /indexName/_search
{
"query": {
"term": {
"字段": {
"value": "值"
}
}
}
}
range查询
# range查询
GET /indexName/_search
{
"query": {
"range": {
"字段": {
"gte": 10, # 这里的gte代表大于等于,gt则代表大于
"lte": 20 # lte代表小于等于,lt则代表小于
}
}
}
}
示例
# 1. term词条匹配。不分词,必须精确匹配。 查询“如家”品牌的酒店
GET /hotel/_search
{
"query": {
"term": {
"brand": {
"value": "如家"
}
}
}
}# 2. range范围匹配。查询price价格在300~500之间的酒店
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 300,
"lte": 500
}
}
}
}
4. 地理坐标查询
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:Geo queries | Elasticsearch Guide [8.13] | Elastic
常见的使用场景包括:
-
携程:搜索我附近的酒店
-
滴滴:搜索我附近的出租车
-
微信:搜索我附近的人
附近的酒店:
附近的车:
语法
矩形范围查询【了解】
矩形范围查询,也就是geo_bounding_box
查询,查询坐标落在某个矩形范围的所有文档:
查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
语法如下:
# geo_bounding_box查询
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"字段": {
"top_left": { # 左上点
"lat": 31.1, #纬度
"lon": 121.5 #经度
},
"bottom_right": { # 右下点
"lat": 30.9, #纬度
"lon": 121.7 #经度
}
}
}
}
}
附近查询
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:
语法说明:
# geo_distance 查询
GET /索引名称/_search
{
"query": {
"geo_distance": {
"distance": "15km", # 查询半径
"字段": "31.21,121.5" # 圆心点的经纬度坐标
}
}
}
示例
# 1. 矩形范围查询。 划一个矩形区域,搜索区域内的数据。 需要指定矩形的左上角坐标和右下角坐标
GET /hotel/_search
{
"query": {
"geo_bounding_box": {
"location":{
"top_left":{
"lat": 31.1,
"lon": 121.5
},
"bottom_right":{
"lat": 30.9,
"lon": 121.7
}
}
}
}
}# 2. 附近查询(圆形范围查询)。划一个圆形区域,搜索区域内的数据。需要指定圆心坐标和半径距离
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "15km", #半径。单位:km千米,m米
"location": "31.21, 121.5" #圆心坐标
}
}
}
5. 复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
-
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
-
bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
相关性计算
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 "虹桥如家",结果如下:
[
{
"_score" : 17.850193,
"_source" : {
"name" : "虹桥如家酒店真不错",
}
},
{
"_score" : 12.259849,
"_source" : {
"name" : "外滩如家酒店真不错",
}
},
{
"_score" : 11.91091,
"_source" : {
"name" : "迪士尼如家酒店真不错",
}
}
]
在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:
在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:
TF-IDF算法有一个缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:
算分函数查询
根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。
以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图:
要想人为控制相关性算分,就需要利用elasticsearch中的function score 查询了。
语法
function score 查询中包含四部分内容:
-
原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
-
过滤条件:filter部分,符合该条件的文档才会重新算分
-
算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
-
weight:函数结果是常量
-
field_value_factor:以文档中的某个字段值作为函数结果
-
random_score:以随机数作为函数结果
-
script_score:自定义算分函数算法
-
-
运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
-
multiply:相乘
-
replace:用function score替换query score
-
其它,例如:sum、avg、max、min
-
function score的运行流程如下:
-
根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
-
根据过滤条件,过滤出符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
-
将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
因此,其中的关键点是:
-
过滤条件:决定哪些文档的算分被修改
-
算分函数:决定函数算分的算法
-
运算模式:决定最终算分结果
示例
需求:给“如家”这个品牌的酒店排名靠前一些
翻译一下这个需求,转换为之前说的四个要点:
-
原始条件:不确定,可以任意变化
-
过滤条件:brand = "如家"
-
算分函数:可以简单粗暴,直接给固定的算分结果,weight
-
运算模式:比如求和
因此最终的DSL语句如下:
#不添加算法函数,原始检索。如家酒店的相关性得分并不高
GET /hotel/_search
{
"query": {
"match": {
"all": "北京酒店"
}
}
}#使用算法函数,所有品牌为“如家”的酒店,在原始相关性得分基础上+10,最终相关性得分高了很多
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "北京酒店"
}
},
"functions": [
{
"filter": {"term": {"brand": "如家"}},
"weight": 10
}
],
"boost_mode": "sum"
}
}
}
不添加算法函数,原始检索。如家酒店的相关性得分并不高
使用算法函数,所有品牌为“如家”的酒店,在原始相关性得分基础上+10,最终相关性得分高了很多
练习
查询北京酒店,提升 其中id为 395799 的酒店 排名
布尔查询【重点】
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
-
must:必须匹配每个子查询,类似“与”,会影响算分(常用于搜索框里的文本检索)
-
filter:必须匹配,不参与算分,只是对搜索结果做再过滤
-
should:选择性匹配子查询,类似“或”
-
must_not:必须不匹配,不参与算分,类似“非”
比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤:
每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用bool查询了。
需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:
-
搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
-
其它过滤条件,采用filter查询。不参与算分
语法
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "华美达" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }}
]
}
}
}
示例
需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
分析:
-
名称搜索,属于全文检索查询,应该参与算分。放到must中
-
价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
-
周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gte": 400
}
}
}
],
"filter": {
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
}
}
}
6. 课堂演示
#query查询条件:
# match模糊查询,即文本检索,会分词检索查询结果
# term等值查询,即相当于SQL里 where 字段=值
# range范围查询,即相当于SQL里 where 字段>值 and 字段<值
# geo_distance地理坐标查询,比如搜附近
# function_score算分函数,用于影响查询结果排名【了解】
# bool多条件组合查询
POST /索引名/_search
{
"query": {
"查询类型":{
}
}
}POST /hotel/_search
#1. 查询“北京酒店”
POST /hotel/_search
{
"query": {
"match": {
"all": "北京酒店"
}
}
}#2. 查询品牌为希尔顿的酒店
POST /hotel/_search
{
"query": {
"term": {
"brand": {
"value": "希尔顿"
}
}
}
}#3. 查询价格为100(包含)~300(不包含)之间的酒店
POST /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lt": 300
}
}
}
}#4. 搜索天安门广场附近的酒店
# 天安门广场的坐标:116.404177, 39.909652
POST /hotel/_search
{
"query": {
"geo_distance":{
"location": "39.909652, 116.404177",
"distance": "1km"
}
}
}#5. 了解:算分函数
# 原理:基于原始查询条件计算的关联度得分,进行再运算
POST /hotel/_search
{
"query": {
"match": {
"all": "北京酒店"
}
}
}
POST /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "北京酒店"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "豪生"
}
},
"weight": 100
}
],
"boost_mode": "multiply"
}
}
}#6. 多条件组合查询:bool
# must:主搜索条件放这里,直接影响关联度得分和结果数量
# 通常是搜索框的条件
# should:偏好条件。只影响得分,不影响结果的数量
# 符合条件的数据,得分更高,排名更靠前
# 不符合条件的数据,得分较低,排名靠后
# filter:过滤条件。只保留符合条件的数据,不符合条件的剔除掉
# 只影响结果的数量,不影响关联度得分
# must_not:剔除条件。直接剔除符合条件的数据
POST /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"all": "北京酒店"
}
}
],
"should": [
{
"term": {
"brand": {
"value": "速8"
}
}
}
],
"filter": [
{
"range":{
"price":{
"lt": 500
}
}
}
],
"must_not": [
{
"range": {
"score": {
"lte": 45
}
}
}
]
}
}
}
7. 小结
#综合查询的语法
POST /索引/_search
{
"query":{}, #所有查询条件写这里
"sort": [], #所有排序条件写这里
"from":0, #分页的起始索引
"size":10, #分页的查询数量
"highlight":{}, #高亮条件
"aggs":{}, #聚合条件,即分组统计
"suggest":{} #搜索提示
}#查询条件:match, term, range, geo_distance
POST /索引/_search
{
"query":{
"match":{
"字段名": "搜索关键词"
}
}
}
POST /索引/_search
{
"query":{
"term":{
"字段名": "值"
}
}
}
POST /索引/_search
{
"query":{
"range":{
"字段名": { #包含 gte, gt, lte, lt
"gte": 最小值,
"lt": 最大值
}
}
}
}
POST /索引/_search
{
"query":{
"geo_distance":{
"字段名": "圆心的坐标 纬度在前经度在后 英文逗号分隔",
"distance": "搜索距离半径 比如 1km"
}
}
}#多条件组合查询,用:bool
# must:主搜索条件,直接影响结果的数量,和关联度得分。通常是主搜索框的条件使用
# should:偏好条件,只影响关联度得分,不影响结果的数量。符合条件的得分更高,排名更靠前
# filter:过滤条件,只保留符合条件的数量。影响结果的数量,但是不影响得分
# must_not:剔除条件,剔除掉符合条件的数据。影响结果的数量,但是不影响得分
POST /索引/_search
{
"query":{
"bool":{
"must":[
{
"match":{ "字段名":值 }
}
],
"should":[],
"filter":[],
"must_not":[]
}
}
}
二、DSL处理结果
- 掌握es的排序和分页
- 掌握es的高亮
1. 排序
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
普通字段排序
keyword、数值、日期类型排序的语法基本一致。
语法
GET /索引库名/_search
{
"query":{
"match_all":{}
},
"sort":[
{"字段1":{"order": "排序规则"}}, #排序规则:DESC,ASC
...
{"字段2":{"order": "排序规则"}}
]
}
示例
需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序
GET /hotel/_search
{
"query": {
"match": {
"all": "北京如家"
}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
地理坐标排序
地理坐标排序略有不同。
语法
GET /索引名称/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance" : {
"字段" : "纬度, 经度", # 文档中geo_point类型的字段名、目标坐标点
"order" : "asc", # 排序方式
"unit" : "km" # 排序的距离单位
}
}
]
}
这个查询的含义是:
-
指定一个坐标,作为目标点
-
计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
-
根据距离排序
示例
需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序
提示:获取你的位置的经纬度的方式:获取鼠标点击经纬度-地图属性-示例中心-JS API 2.0 示例 | 高德地图API
假设我的位置是:31.034661, 121.612282,寻找我周围距离最近的酒店。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.034,
"lon": 121.612
},
"unit": "km",
"order": "asc"
}
}
]
}
练习
-
查询北京酒店,按照评分降序排列,如果评分相同,则按照价格升序排列
-
查询所有酒店,按照与你的距离升序排列
2. 分页
基本分页
elasticsearch的分页与mysql数据库非常相似,都是指定两个值
-
from:起始索引
-
size:查询几条
语法
GET /索引库名/_search
{
"query":{
"match_all":{}
},
"from": 起始索引,
"size": 查询几条
}
示例
#分页查询:每页2条,查询第1页(从索引0开始,查询2条)
GET /product/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 2
}
结果
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "product",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "小米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2999,
"brand" : "小米"
}
},
{
"_index" : "product",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"title" : "华为手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 3999,
"brand" : "华为"
}
}
]
}
}
深度分页
深度分页问题
假如现在要查询990~1000的数据,查询逻辑要这么写:
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990, # 分页开始的位置,默认为0
"size": 10, # 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
这里是查询990开始的数据,也就是 第990~第1000条 数据。
单节点es的分页查询逻辑
elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:
查询TOP1000,如果es是单点模式,这并无太大影响。
es集群的分页查询逻辑
但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了:因为节点A的TOP200,在另一个节点可能排到10000名以外了。
因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。
那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?
当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。
深度分页解决方案
针对深度分页,ES提供了两种解决方案,官方文档:
-
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
-
scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
第一次查询
下一次查询
注意
要保证排序值是唯一不重复的,否则分页时可能会漏掉数据。
期望结果:
-
第一次查询:最后一条数据的排序值是 score=47,price=245。 score=47,price=245的数据只有一条
-
下一次查询:查询 score=47,price=245之后的数据,没有任何问题
但是如果:
-
score=45,price=245的数据有多条,假定为doc1、doc2
-
第一次查询第一页时,顺序是doc1、doc2,这一页刚好查询到了doc1
-
查询下一页时,顺序是doc2、doc1,从第2条开始,查询到了doc1
-
最终就漏掉了doc2
解决方案:
-
建议保证排序条件值不重复,就不会出现上面的问题了
-
例如:以score降序、price升序、
_id
降序。_id
是文档的唯一标识,是不重复的
3. 高亮
高亮:就是在搜索结果中,把搜索的关键字突出显示。
例如:
原理:
-
在搜索结果中,把关键字使用特定的标签标记出来
-
在页面上,给这些标签添加CSS样式
高亮的最终效果实现,需要结合前端代码。而我们能做的,就是使用特定的标签把搜索结果中关键字标记出来
语法
-
高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
-
默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
-
如果要对非搜索字段高亮,则需要添加一个属性:
required_field_match=false
GET /索引库名/_search
{
"query":{
"match":{
"字段": "值"
}
},
"highlight":{
"fields":{
"高亮字段1":{
"pre_tags":"开始标签",
"post_tags":"结束标签"
},
"高亮字段2":{
"pre_tags":"开始标签",
"post_tags":"结束标签"
},
"required_field_match": "false"
}
}
}
示例
GET /hotel/_search
{
"query": {
"match": {
"all": "北京酒店"
}
},
"highlight": {
"fields": {
"name":{
"pre_tags": "<em>",
"post_tags": "</em>"
}
},
"require_field_match": "false"
}
}
注意:
-
默认情况下,要求 检索字段 和 高亮字段 必须是同一个
-
如果检索字段与高亮字段不是同一个:需要添加
require_field_match
,设置为false
4. 课堂演示
#1. 排序
# 查询时如果不指定排序条件,默认按关联度得分。得分越高排名越靠前
# 自定义排序条件:
# 简单排序:按某字段值升序或降序排列
POST /hotel/_search
{
"sort": [
{
"price": {
"order": "asc"
}
},
{
"score": {
"order": "asc"
}
}
]
}
# 距离排序:按geo计算距离进行排序。把所有酒店按照与天安门的距离升序
POST /hotel/_search
{
"sort": [
{
"_geo_distance": {
"location": "39.909652, 116.404177",
"order": "asc",
"unit": "km"
}
}
]
}#2. 分页。
# from:查询的起始索引,从哪个数据开始查
# from值 = (页码-1) * 每页几条
# size:要查询几条
POST /hotel/_search
{
"from": 5,
"size": 5
}#3. 高亮
# 默认情况下,要求检索字段与高亮字段相同
# 检索哪个字段,哪个字段才会有高亮效果
# 如果检索字段与高亮字段不同,需要额外设置
# require_field_match设置为false
POST /hotel/_search
{
"query": {
"match": {
"all": "北京酒店"
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
},
"require_field_match": "false"
}
}
5. 小结
#排序:普通排序
POST /hotel/_search
{
"query":{...}, #查询条件。有条件就写,没有查询条件就不写
"sort":[
{
"字段名":{
"order": "排序规则" #排序规则有 asc升序,desc降序
}
}
]
}
#排序:geo距离排序
POST /hotel/_search
{
"query":{...},
"sort":[
{
"_geo_distance":{
"字段名": "圆心点坐标 纬度在前经度在后 英文逗号分隔",
"order": "排序规则",
"unit": "距离单位 比如km"
}
}
]
}#分页:
POST /hotel/_search
{
"from": 起始索引, #起始索引值 = (页码-1)*每页几条
"size": 查询几条
}#高亮:需要前后端配合实现高亮效果。
# 我们把需要高亮的内容,使用html标签标起来
# 前端开发人员编写css样式代码,选中高亮的标签设置样式
# 注意:如果检索字段和高亮字段不同,就必须添加require_field_match设置为false
POST /hotel/_search
{
"query":{
"match":{
"检索字段名": "搜索条件"
}
},
"highlight":{
"fields":{
"高亮字段名":{
"pre_tags": "前置标签",
"post_tags": "后置标签"
}
},
"require_field_match": "false"
}
}
三、RestClient查询
文档的查询同样适用昨天学习的 RestHighLevelClient对象,基本步骤包括:
-
发起请求,得到响应
需要先准备SearchRequest对象,构建请求参数
-
处理响应,得到结果
可在Kibana里执行DSL命令,看着执行结果编写这部分代码
1.快速入门
我们以match_all查询为例,查询酒店并把结果输出到控制台上
API说明
发起请求得到响应
SearchRequest:用于封装检索参数
searchRequest对象.source()
提供了构造检索参数的一系列方法:
searchRequest对象.source()
.query(检索条件) //可以使用QueryBuilders构造各种不同类型的查询条件
.sort(排序)
.from(起始索引).size(查询数量)
.highlighter(高亮);
-
QueryBuilders
提供了一系列方法,用于构造不同类型的查询条件,常用的有:-
QueryBuilders.matchAllQuery()
:查询全部 -
QueryBuilders.matchQuery("字段", 值)
:match查询条件 -
QueryBuilders.multiMatchQuery(值, "字段1", "字段2", ...)
:multi_match查询条件 -
QueryBuilders.termQuery("字段", 值)
:term查询条件 -
QueryBuilders.rangeQuery("字段").gte(值1).lte(值2)
:range查询条件 -
QueryBuilders.geoDistanceQuery("字段").distance(距离,长度单位)
:geo_distance查询条件 -
QueryBuilders.functionScoreQuery()
:算分函数查询 -
QueryBuilders.boolQuery().must(..).should(..).mustNot(..).filter(..)
:bool查询条件
-
-
client.search(SearchRequest request, RequestOptions options)
:发起请求进行检索
处理响应得到结果
client.search()
方法返回的结果是一个JSON,结构包含:
-
hits
:命中的结果-
total
:总条数,其中的value是具体的总条数值 -
max_score
:所有结果中得分最高的文档的相关性算分 -
hits
:搜索结果的文档数组,其中的每个文档都是一个json对象-
_source
:文档中的原始数据,也是json对象
-
-
因此,我们解析响应结果,就是逐层解析JSON,流程如下:
-
SearchHits
:通过response.getHits()
获取,就是JSON中的最外层的hits,代表命中的结果-
searchHits对象.getTotalHits().value
:获取总条数信息 -
searchHits对象.getHits()
:获取SearchHit数组,也就是文档数组-
searchHit对象.getSourceAsString()
:获取文档结果中的_source,即原始的json文档数据
-
-
示例代码
完整代码示例:
public class Demo01 {
private RestHighLevelClient client;@Test
public void testMatchAll() throws IOException {
//1. 构造SearchRequest对象。构造参数:索引库名
SearchRequest request = new SearchRequest("hotel");//2. 设置查询信息
request.source().query(QueryBuilders.matchAllQuery());//3. 发起请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4. 处理响应,得到结果
SearchHits result = response.getHits();
// 总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}@Before
public void init(){
client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200)));
}
@After
public void destroy() throws IOException {
client.close();
}
}
2.match查询
API说明
构造查询条件时:
-
QueryBuilders.matchQuery("字段", 值)
:match查询条件 -
QueryBuilders.multiMatchQuery(值, "字段1", "字段2", ...)
:multi_match查询条件
示例代码
@Test
public void testMatch() throws IOException {
//1. 构造SearchRequest对象。构造参数:索引库名
SearchRequest request = new SearchRequest("hotel");//2. 设置查询信息
// match
request.source().query(QueryBuilders.matchQuery("all", "北京酒店"));
// multi_match。字段过多的话,会影响性能,不推荐使用
//request.source().query(QueryBuilders.multiMatchQuery("北京酒店", "name", "brand"));//3. 发起请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4. 处理响应,得到结果
SearchHits result = response.getHits();
// 总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
3.精确查询
API说明
构造查询条件时:
-
QueryBuilders.termQuery("字段", 值)
:term查询条件 -
QueryBuilders.rangeQuery("字段").gte(值1).lte(值2)
:range查询条件
示例代码
@Test
public void testTerm() throws IOException {
//1. 构造SearchRequest对象。构造参数:索引库名
SearchRequest request = new SearchRequest("hotel");//2. 设置查询信息
// term
// request.source().query(QueryBuilders.termQuery("brand", "如家"));
// range
request.source().query(QueryBuilders.rangeQuery("price").gte(300).lte(500));//3. 发起请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4. 处理响应,得到结果
SearchHits result = response.getHits();
// 总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
4.布尔查询
API说明
构造查询条件时:
-
QueryBuilders.boolQuery().must(..).should(..).mustNot(..).filter(..)
:bool查询条件
示例代码
@Test
public void testBoolQuery() throws IOException {
//1. 构造SearchRequest对象。构造参数:索引库名
SearchRequest request = new SearchRequest("hotel");//2. 设置查询信息
// bool查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
//must条件
.must(QueryBuilders.matchQuery("all", "北京酒店"))
//should条件
.should(QueryBuilders.termQuery("starName", "五星"))
//must_not条件
.mustNot(QueryBuilders.rangeQuery("score").lte(40))
//filter条件
.filter(QueryBuilders.termQuery("brand", "希尔顿"));
request.source().query(boolQuery);//3. 发起请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4. 处理响应,得到结果
SearchHits result = response.getHits();
// 总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
5. 算分函数
@Test
public void testFunctionScore() throws IOException {
SearchRequest request = new SearchRequest("hotel");//设置查询条件
FunctionScoreQueryBuilder scoreQueryBuilder = QueryBuilders.functionScoreQuery(
//基础查询
QueryBuilders.matchQuery("all", "北京酒店"),
//对应DSL里functions数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
//filter,过滤出来要重新算分的数据
QueryBuilders.termQuery("brand", "希尔顿"),
//设置算分函数,使用权重值
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
//处分函数的加权模式:Multiply,相乘。数据的原始得分 乘 权重值。如果不设置加权模式,默认就是相乘
scoreQueryBuilder.boostMode(CombineFunction.MULTIPLY);request.source().query(scoreQueryBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);SearchHits result = response.getHits();
// 获取总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 获取数据列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
//获取文档对象的原始数据
String docJson = hit.getSourceAsString();
HotelDoc doc = JSON.parseObject(docJson, HotelDoc.class);
System.out.println("查询得到的数据:" + doc);System.out.println("匹配度得分:" + hit.getScore());
}
}
6.排序、分页
API说明
搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。
searchRequest对象.source()
.query(检索条件)
.sort(排序)
.from(起始索引).size(查询数量)
.highlighter(高亮);
示例代码
@Test
public void testSortAndPage() throws IOException {
//1. 构造SearchRequest对象。构造参数:索引库名
SearchRequest request = new SearchRequest("hotel");//2. 设置查询信息
request.source()
.query(QueryBuilders.matchAllQuery())
//按照price升序排列
.sort(SortBuilders.fieldSort("price").order(SortOrder.ASC))
//从索引0开始,查询5条
.from(0).size(5);//3. 发起请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4. 处理响应,得到结果
SearchHits result = response.getHits();
// 总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
7.高亮
高亮的代码与之前代码差异较大,有两点:
-
查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
-
结果解析:结果除了要解析_source文档数据,还要解析高亮结果
API说明
构建高亮请求
searchRequest对象.source()
.query(检索条件)
.sort(排序)
.from(起始索引).size(查询数量)
.highlighter(高亮); // 使用HighLightBuilder设置高亮信息
构造高亮请求如下:
注意:高亮查询必须使用文本检索查询,并且要有搜索关键字,将来才可以对关键字高亮。
处理高亮结果
高亮的结果与查询的文档结果默认是分离的,并不在一起。
因此解析高亮的代码需要额外处理:
代码说明:
-
从结果中获取原始文档json:
hit.getSourceAsString()
,这部分是非高亮的结果,json格式的字符串。还需要反序列为HotelDoc对象 -
获取高亮结果,替换掉原本的非高亮值:
-
获取高亮Map:
hit.getHighlightFields()
返回值是一个Map,key是高亮字段名称,值是
HighlightField
对象,代表高亮值 -
从高亮Map中,根据高亮字段名称,获取高亮字段值对象
HighlightField
-
从
HighlightField
中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了 -
用高亮的结果替换HotelDoc中的非高亮结果
-
示例代码
@Test
public void testHighLight() throws IOException {
//1. 构造SearchRequest对象。构造参数:索引库名
SearchRequest request = new SearchRequest("hotel");//2. 设置查询信息
request.source()
.query(QueryBuilders.matchQuery("all", "如家"))
// name中关键字高亮显示
.highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));//3. 发起请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4. 处理响应,得到结果
SearchHits result = response.getHits();
// 总数量
long total = result.getTotalHits().value;
System.out.println("总数量:" + total);
// 列表
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
// 获取原始文档数据(没有高亮的)
HotelDoc doc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
// 获取高亮值,把doc里非高亮值替换掉
Map<String, HighlightField> highlightFieldMap = hit.getHighlightFields();
if (highlightFieldMap != null) {
HighlightField nameField = highlightFieldMap.get("name");
if (nameField != null) {
String nameHighLightValue = nameField.getFragments()[0].string();
doc.setName(nameHighLightValue);
}
}System.out.println(doc);
}
}
8.小结
//如果要使用Java操作ES,查询检索文档数据:整体步骤
//1. 先准备SearchRequest对象:构造所有的请求信息:建议 看着在Kibana里写好的DSL命令,写Java代码
SearchRequest request = new SearchRequest("索引名");
request.source()
.query(查询条件)
.sort(排序条件)
.from(分页的起始索引).size(分页的查询几条)
.highlighter(高亮条件)
.aggregation(聚合条件);
//2. 再通过客户端对象,执行search方法,得到查询结果
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
//3. 处理结果:建议 看着在Kibana里执行DSL的结果,写Java代码
// 获取总数量
long total = response.getHits().getTotalHit().value;
// 获取结果数据列表
SearchHit[] hits = response.getHits().getHits();
for(SearchHit hit: hits){
//获取原始文档数据,json
String docJson = hit.getSourceAsString();
//获取排序值
Object[] sortValues = hit.getSortValues();
//获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField nameField = highlightFields.get("高亮字段名");
String nameValue = nameField.getFragments()[0].string();
}
//==========构造查询条件==========
QueryBuilders.matchQuery("字段名", "值");
QueryBuilders.termQuery("字段名", "值");
QueryBuilders.rangeQuery("字段名").gte(最小值).lt(最大值);
QueryBuilders.geoDistanceQuery("存储坐标的字段名")
.point(圆心点的纬度,圆心点的经度)
.distance(搜索距离数字, DistanceUnit.距离单位);
QueryBuilders.boolQuery()
.must(主搜索条件)
.should(偏好条件)
.filter(过滤条件)
.mustNot(剔除条件);
//=========构造排序条件=========
// 简单排序
request.source()
.sort("排序字段", SortOrder.排序规则)
.sort("排序字段", SortOrder.排序规则);
// 距离排序
SortBuilder sortBuilder = SortBuilders
.geoDistanceSort("坐标字段名", 圆心点纬度, 圆心点经度)
.order(SortOrder.ASC)
.unit(DistanceUnit.距离单位);
request.source().sort(sortBuilder);
//=======构造分页条件===========
// 分页的from值 = (页码-1)*每页几条
request.source().from(分页的起始索引).size(分页的每页几条);
//======构造高亮条件============
HighlightBuilder hb = new HighlightBuilder()
.field("高亮字段名").preTags("前置标签").postTags("后置标签")
.requireFieldMatch(false);
request.source().highligter(hb);
四、黑马旅游案例
需求
实现黑马旅游的酒店搜索功能,包括:
-
搜索条件:
-
搜索框里的关键字搜索
-
根据“城市”、“星级”、“品牌”、“价格”进行过滤
-
-
排序条件:
-
根据指定条件进行排序
-
当点击地图的“定位”按钮时,要按照与定位点的距离进行排序
-
-
分页功能
-
显示结果
-
搜索结果中,关键字高亮显示
-
广告酒店提升排名【拓展】
-
分析
-
准备数据:在kibana中执行以下命令,把两条酒店数据标记为广告
POST /hotel/_update/2056126831
{
"doc": {
"ad": true
}
}
POST /hotel/_update/2062643512
{
"doc": {
"ad": true
}
}
-
搜索条件:
-
多条件搜索,使用
bool
实现搜索框里的关键字,从
all
字段里搜索,并计算关联度得分。其它过滤条件“城市”、“星级”、“品牌”、“价格”,仅仅是数据的再过滤,不参与关联度得分计算
-
如果需要广告酒店提升排名,则需要使用算法函数【拓展】
以上边的多条件搜索 作为基本查询条件
添加filter:
-
过滤出要提升排名的酒店数据(
"term":{"ad":true}
) -
设置一个固定权重值:10
设置加权模式为默认相乘(如果不设置,就是默认的相乘)
-
-
-
排序条件:
如果客户端提交了定位点,则根据酒店位置与定位点之间的距离,升序排列,距离单位
km
如果客户端没有提交定位点,则根据客户端提交的其它排序字段进行排序
-
分页功能:
from
+size
-
显示结果:
-
搜索结果添加高亮
-
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"all": "北京酒店"
}
}
],
"filter": [
{
"term": {
"brand": "希尔顿"
}
},
{
"term": {
"starName": "五星"
}
},
{
"range": {
"price": {
"gte": 500
}
}
}
]
}
},
"sort": [
{
"_geo_distance": {
"location": "36, 121",
"unit": "km",
"order": "asc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {
"name":{}
},
"require_field_match": "false"
}
}
实现
引导类
创建RestHighLevelClient对象,注册到容器里
@SpringBootApplication
@MapperScan("com.itheima.mapper")
public class HotelApplication {
public static void main(String[] args) {
SpringApplication.run(HotelApplication.class, args);
}
@Bean
public RestHighLevelClient esClient(){
return new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200)));
}
}
HotelService
未实现广告功能的版本
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> {
@Autowired
private RestHighLevelClient esClient;
public ResponseEntity search(QueryVO vo) throws IOException {
//1. 从es中搜索数据
SearchRequest request = new SearchRequest("hotel");
//1.1 设置搜索条件
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// key
if (StringUtils.hasText(vo.getKey())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", vo.getKey()));
}
// brand
if (StringUtils.hasText(vo.getBrand())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand", vo.getBrand()));
}
// city
if (StringUtils.hasText(vo.getCity())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("city", vo.getCity()));
}
// starName
if (StringUtils.hasText(vo.getStarName())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("starName", vo.getStarName()));
}
// minPrice
if (vo.getMinPrice() != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(vo.getMinPrice()));
}
// maxPrice
if (vo.getMaxPrice() != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(vo.getMaxPrice()));
}
request.source().query(boolQueryBuilder);
//1.2 设置排序条件
if (StringUtils.hasText(vo.getLocation())) {
//如果客户端提交了地理位置坐标,格式:“纬度,经度”,则按照距离进行排序。单位km,升序
GeoDistanceSortBuilder sortBuilder = SortBuilders
.geoDistanceSort("location", new GeoPoint(vo.getLocation()))
.unit(DistanceUnit.KILOMETERS)
.order(SortOrder.ASC);
request.source().sort(sortBuilder);
} else if (!"default".equals(vo.getSortBy())) {
//如果没有地理位置坐标,但是指定了排序字段,按指定字段排序(没有指定规则)
request.source().sort(vo.getSortBy());
}
//1.3 设置分页条件
int index = (vo.getPage() - 1) * vo.getSize();
request.source().from(index).size(vo.getSize());
//1.4 设置高亮:name字段高亮。因为不是从name里搜索的数据,所以要设置require_field_match为false
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("name").requireFieldMatch(false);
request.source().highlighter(highlightBuilder);
//执行查询,得到结果
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
//2. 封装结果并返回
SearchHits result = response.getHits();
//2.1 总数量
long total = result.getTotalHits().value;
//2.2 获取列表
List<HotelDoc> docList = new ArrayList<>();
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
//获取文档数据
String docJson = hit.getSourceAsString();
HotelDoc doc = JSON.parseObject(docJson, HotelDoc.class);
//获取高亮值
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null) {
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
String nameHighLight = highlightField.getFragments()[0].string();
doc.setName(nameHighLight);
}
}
//设置酒店与搜索点之间的距离
Object[] sortValues = hit.getSortValues();
if (sortValues != null && sortValues.length > 0) {
doc.setDistance(sortValues[0]);
}
docList.add(doc);
}
return ResponseEntity.ok(new PageResult(total, docList));
}
}
实现了广告功能的版本
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> {
@Autowired
private RestHighLevelClient esClient;
public ResponseEntity search(QueryVO vo) throws IOException {
//1. 从es中搜索数据
SearchRequest request = new SearchRequest("hotel");
//1.1 设置搜索条件
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// key
if (StringUtils.hasText(vo.getKey())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", vo.getKey()));
}
// brand
if (StringUtils.hasText(vo.getBrand())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand", vo.getBrand()));
}
// city
if (StringUtils.hasText(vo.getCity())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("city", vo.getCity()));
}
// starName
if (StringUtils.hasText(vo.getStarName())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("starName", vo.getStarName()));
}
// minPrice
if (vo.getMinPrice() != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(vo.getMinPrice()));
}
// maxPrice
if (vo.getMaxPrice() != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(vo.getMaxPrice()));
}
// 算分函数
FunctionScoreQueryBuilder scoreQueryBuilder = new FunctionScoreQueryBuilder(
//基本查询条件
boolQueryBuilder,
//过滤出要重新计算分数的数据,设置权重为10
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("ad", true),
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
request.source().query(scoreQueryBuilder);
//1.2 设置排序条件
if (StringUtils.hasText(vo.getLocation())) {
//如果客户端提交了地理位置坐标,格式:“纬度,经度”,则按照距离进行排序。单位km,升序
GeoDistanceSortBuilder sortBuilder = SortBuilders
.geoDistanceSort("location", new GeoPoint(vo.getLocation()))
.unit(DistanceUnit.KILOMETERS)
.order(SortOrder.ASC);
request.source().sort(sortBuilder);
} else if (!"default".equals(vo.getSortBy())) {
//如果没有地理位置坐标,但是指定了排序字段,按指定字段排序(没有指定规则)
request.source().sort(vo.getSortBy());
}
//1.3 设置分页条件
int index = (vo.getPage() - 1) * vo.getSize();
request.source().from(index).size(vo.getSize());
//1.4 设置高亮:name字段高亮。因为不是从name里搜索的数据,所以要设置require_field_match为false
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("name").requireFieldMatch(false);
request.source().highlighter(highlightBuilder);
//执行查询,得到结果
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
//2. 封装结果并返回
SearchHits result = response.getHits();
//2.1 总数量
long total = result.getTotalHits().value;
//2.2 获取列表
List<HotelDoc> docList = new ArrayList<>();
SearchHit[] hits = result.getHits();
for (SearchHit hit : hits) {
//获取文档数据
String docJson = hit.getSourceAsString();
HotelDoc doc = JSON.parseObject(docJson, HotelDoc.class);
//获取高亮值
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null) {
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
String nameHighLight = highlightField.getFragments()[0].string();
doc.setName(nameHighLight);
}
}
//设置酒店与搜索点之间的距离
Object[] sortValues = hit.getSortValues();
if (sortValues != null && sortValues.length > 0) {
doc.setDistance(sortValues[0]);
}
docList.add(doc);
}
return ResponseEntity.ok(new PageResult(total, docList));
}
}
五、数据聚合
聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:
-
什么品牌的手机最受欢迎?
-
这些手机的平均价格、最高价格、最低价格?
-
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
1. 聚合的种类
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型。 text类型不允许参与聚合
聚合常见的有三类:
-
桶(Bucket)聚合--聚合成桶,把数据进行归类分组:用来对文档做分组
-
TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照城市分组
-
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
-
Range Aggregation:按范围分组,指定开始和结束,然后按段分组
-
……
-
-
度量(Metric)聚合-桶内度量:用以计算一些值,比如:最大值、最小值、平均值等
-
Avg:求平均值
-
Max:求最大值
-
Min:求最小值
-
Sum:求和
-
Stats:同时求max、min、avg、sum、count等
-
Percentiles:求百分比
-
Top hits:求前几
-
……
-
-
管道(pipeline)聚合:其它聚合的结果为基础做聚合
聚合成桶:把数据按照指定条件进行分组归类的过程。比如:如家一组,速8一组,7天连锁一组,……
桶内度量:针对每一组的内容进行数据统计的过程
2. DSL实现聚合
现在,我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是Bucket聚合。
1 Bucket聚合为桶
聚合基本语法
语法如下:
GET /hotel/_search
{
"query":{ "match_all":{} }
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { //给聚合起个名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选择term
"field": "brand", // 参与聚合的字段
"size": 20 // 希望获取的聚合结果数量
}
}
}
}
结果如图:
聚合结果排序
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count
,并且按照_count
降序排序。
我们可以指定order属性,自定义聚合的排序方式:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 20
}
}
}
}
限定聚合范围
默认情况下,Bucket聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加query条件即可:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只对200元以下的文档聚合
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
这次,聚合得到的品牌明显变少了:
2 Metric桶内度量
刚刚我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。
这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。
语法如下:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
"score_stats": { // 聚合名称
"stats": { // 聚合类型,这里stats可以计算min、max、avg等
"field": "score" // 聚合字段,这里是score
}
}
}
}
}
}
这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。
另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序:
3. RestAPI实现聚合
1 API语法
聚合条件与query条件同级别,因此需要使用request.source()来指定聚合条件。
聚合条件的语法:
聚合的结果也与查询结果不同,API也比较特殊。不过同样是JSON逐层解析:
2 示例代码
package com.itheima.test;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Stats;
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.SpringRunner;
import java.io.IOException;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
public class Demo01AggTest {
@Autowired
private RestHighLevelClient esClient;
/**
* 聚合为桶
*/
@Test
public void test01() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//查询文档数量为0
TermsAggregationBuilder aggregationBuilder = AggregationBuilders
//聚合类型:terms聚合,聚合名称:brandAgg
.terms("brandAgg")
//聚合字段
.field("brand")
//桶的数量
.size(20)
//聚合排序
.order(BucketOrder.count(true));
request.source()
.size(0)
.aggregation(aggregationBuilder);
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
//获取所有聚合结果
Aggregations aggregations = response.getAggregations();
// 获取名称为brandAgg的聚合结果。是terms聚合,要把返回值转换成Terms
Terms brandAgg = aggregations.get("brandAgg");
// 获取所有桶
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
System.out.println("key = " + key + ", docCount = " + docCount);
}
}
/**
* 桶内度量
*/
@Test
public void test02() throws IOException {
SearchRequest request = new SearchRequest("hotel");
TermsAggregationBuilder aggregationBuilder = AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
.subAggregation(AggregationBuilders.stats("statAgg").field("price"))
.order(BucketOrder.aggregation("statAgg.avg", true));
request.source()
.size(0)
.aggregation(aggregationBuilder);
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
Aggregations aggregations = response.getAggregations();
Terms brandAgg = aggregations.get("brandAgg");
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
buckets.forEach(bucket->{
String key = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
Stats statAgg = bucket.getAggregations().get("statAgg");
long count = statAgg.getCount();
double sum = statAgg.getSum();
double max = statAgg.getMax();
double min = statAgg.getMin();
double avg = statAgg.getAvg();
System.out.println(String.format("key:%s, docCount:%s, sum:%s, max:%s, min:%s, avg:%s", key, docCount, sum, max, min, avg));
});
}
}
4. 练习-动态搜索条件
需求
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的:
分析
功能还存在的问题
目前,页面的城市列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变化而变化。但是用户搜索条件改变时,搜索结果会跟着变化。
例如:用户搜索“王府井”,那搜索的酒店肯定是在北京王府井附近。因此,城市只能是北京,此时城市列表中就不应该显示其它城市了。
也就是说,搜索结果中包含哪些城市,页面就应该列出哪些城市;搜索结果中包含哪些品牌,页面就应该列出哪些品牌。
问题的解决方案
如何得知搜索结果中包含哪些品牌?如何得知搜索结果中包含哪些城市?
使用聚合功能,利用Bucket聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
准备工作
把资料中的index.html
拷贝到项目里,覆盖掉原本的index.html
页面
API接口说明
打开浏览器抓包工具,刷新页面,发现客户端发出了一个请求,请求参数与搜索文档的参数完全一致。
返回值类型就是页面要展示的最终结果:
{
"city": [
"北京"
],
"starName": [
"五星",
"二钻",
"五钻"
],
"brand": [
"君悦",
"希尔顿",
"皇冠假日",
"速8"
]
}
实现
HotelController
@PostMapping("/filters")
public ResponseEntity filters(@RequestBody QueryVO vo) throws IOException {
return hotelService.filter(vo);
}
HotelService
public ResponseEntity filter(QueryVO vo) throws IOException {
SearchRequest request = new SearchRequest("hotel");
//1. 构造请求条件:和之前检索酒店数据的查询条件相同,所以抽取成了一个公用方法
buildQuery(vo, request);
//2. 构造聚合条件
// 2.1 构造城市聚合
buildTermsAggregation(request, "city", "cityAgg");
// 2.2 构造星级聚合
buildTermsAggregation(request, "starName", "starNameAgg");
// 2.3 构造品牌聚合
buildTermsAggregation(request, "brand", "brandAgg");
//3. 发起请求,得到响应
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
//4. 处理响应,得到结果
Aggregations aggregations = response.getAggregations();
Map<String, List<Object>> map = new HashMap<>();
// 获取city聚合结果
List<Object> cityList = getBucketKeyList(aggregations, "cityAgg");
map.put("city", cityList);
// 获取starName聚合结果
List<Object> starNameList = getBucketKeyList(aggregations, "starNameAgg");
map.put("starName", starNameList);
// 获取brand聚合结果
List<Object> brandList = getBucketKeyList(aggregations, "brandAgg");
map.put("brand", brandList);
return ResponseEntity.ok(map);
}
private void buildQuery(QueryVO vo, SearchRequest request) {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// key:搜索框里的内容。判断字符串是否为空,可以使用Spring提供的StringUtils工具
if (StringUtils.hasText(vo.getKey())) {
//搜索框里的内容,使用must,参与计分
boolQueryBuilder.must(QueryBuilders.matchQuery("all", vo.getKey()));
}
// brand:品牌
if (StringUtils.hasText(vo.getBrand())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand", vo.getBrand()));
}
// city:城市
if (StringUtils.hasText(vo.getCity())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("city", vo.getCity()));
}
// starName:星级
if (StringUtils.hasText(vo.getStarName())) {
boolQueryBuilder.filter(QueryBuilders.termQuery("starName", vo.getStarName()));
}
// minPrice
if (vo.getMinPrice() != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(vo.getMinPrice()));
}
// maxPrice
if (vo.getMaxPrice() != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(vo.getMaxPrice()));
}
// 算分函数
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(
//基础查询
boolQueryBuilder,
//过滤出来要重新算分的数据
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("ad", true),
ScoreFunctionBuilders.weightFactorFunction(100)
)
}
);
request.source().query(functionScoreQueryBuilder);
}
private List<Object> getBucketKeyList(Aggregations aggregations, String aggName) {
Terms cityAgg = aggregations.get(aggName);
return cityAgg.getBuckets().stream().map(o->((Terms.Bucket) o).getKey()).collect(Collectors.toList());
}
private void buildTermsAggregation(SearchRequest request, String field, String aggName) {
request.source().aggregation(
AggregationBuilders
.terms(aggName)
.field(field)
.order(BucketOrder.count(false))
.size(100)
);
}
六、大总结
DSL搜索
POST /索引/_search
{
"query":{}, #match,term,range,geo_distance,bool
"sort":[],
"from":0,
"size":10,
"highlight":{}
}POST /索引/_search
{
"query":{
"match":{
"字段名": "值"
}
}
}POST /索引/_search
{
"query":{
"term":{
"字段名": "值"
}
}
}POST /索引/_search
{
"query":{
"range":{
"字段名": {
"gt": 最小值,
"lt": 最大值
}
}
}
}POST /索引/_search
{
"query":{
"geo_distance":{
"字段名": "圆心点纬度,圆心点经度",
"distance": "距离"
}
}
}POST /索引/_search
{
"query":{
"bool":{
"must":[], #主搜索条件,直接影响结果的数量和关联度得分排名
"should":[], #偏好条件,只影响关联度得分,不影响结果的数量
"filter":[], #过滤条件,只保留符合条件的结果
"must_not":[] #剔除条件,要剔除掉符合条件的结果
}
}
}POST /索引/_search
{
"sort":[
{
"字段名":{
"order": "asc"
}
}
]
}POST /索引/_search
{
"sort":[
{
"_geo_distance":{
"字段名":"圆心点纬度,圆心点经度",
"order": "asc",
"unit": "km"
}
}
]
}POST /索引/_search
{
"query":{
"match":{
"检索字段":"查询条件"
}
},
"highlight":{
"fields":{
"高亮字段名":{
"pre_tags":"前置标签",
"post_tags":"后置标签"
}
},
"require_field_match":"false"
}
}
Java搜索
//1. 准备SearchRequest对象
SearchRequest request = new SearchRequest("索引名");
// 添加查询条件:使用QueryBuilders的静态方法,构造查询条件
// QueryBuilders.matchQuery("字段名", "值")
// QueryBuilders.termQuery("字段名", "值")
// QueryBuilders.rangeQuery("字段名").gte(最小值).lt(最大值);
// QueryBuilders.geoDistanceQuery("字段名").point(圆心点纬度,经度).distance(距离,距离单位)
// QueryBuilders.boolQuery()
// .must(主搜索条件)
// .should(偏好条件)
// .filter(过滤条件)
// .mustNot(剔除条件);
request.source().query(查询条件);
// 添加排序条件
// 简单排序:request.source().sort("排序字段", SortOrder.ASC)
// 距离排序:SortBuilders.geoDistanceSort("字段名", 圆心点纬度,圆心点经度).order(SortOrder.ASC).unit(DistanceUnit.距离单位)
request.source().sort(排序条件);
// 添加分页条件
// 起始索引from值 = (页码-1)*每页几条
request.source().from(起始索引).size(每页几条);
// 添加高亮条件
// new HigilightBuilder()
// .field("高亮字段").preTag("前置标签").postTags("后置标签")
// .requireFieldMatch(false)
request.source().highlighter(高亮条件);
//2. 使用客户端发送请求,获取查询的结果
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
//3. 处理结果
// 获取总数量
long total = response.getHits().getTotalHits().value;
// 获取数据列表
SearchHit[] hits = response.getHits().getHits();
for(SearchHit hit: hits){
//获取原始文档数据
String docJson = hit.getSourceAsString();
//如果添加了排序条件,可以获取排序值。如果没有排序条件,获取的是null
Object[] sortValues = hit.getSortValues();
//如果添加了高亮条件,可以获取高亮结果。如果没有高亮条件,获取的是null
Map<String, HighlightField> fields = hit.getHighlightFields();
HighlightField field = fields.get("高亮字段");
String value = field.getFragments()[0].string();
}