ElasticSearch02(DSL查询文档,DSL处理结果,RestClient查询,旅游案例,数据聚合)【全详解】

目录

一、DSL查询文档

1. 说明

2. 文本检索

3. 精确查询

4. 地理坐标查询

5. 复合查询

6. 课堂演示

7. 小结

二、DSL处理结果

1. 排序

2. 分页

3. 高亮

4. 课堂演示

5. 小结

三、RestClient查询

1.快速入门

2.match查询

3.精确查询

4.布尔查询

5. 算分函数

6.排序、分页

7.高亮

8.小结

四、黑马旅游案例

需求

分析

实现

五、数据聚合

1. 聚合的种类

2. DSL实现聚合

3. RestAPI实现聚合

4. 练习-动态搜索条件

需求

分析

实现

六、大总结


一、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的运行流程如下:

  1. 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)

  2. 根据过滤条件,过滤出符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)

  3. 原始算分(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"
      }
    }
  ]
}

练习

  1. 查询北京酒店,按照评分降序排列,如果评分相同,则按照价格升序排列

  2. 查询所有酒店,按照与你的距离升序排列

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对象,基本步骤包括:

  1. 发起请求,得到响应

    需要先准备SearchRequest对象,构建请求参数

  2. 处理响应,得到结果

    可在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设置高亮信息

构造高亮请求如下:

注意:高亮查询必须使用文本检索查询,并且要有搜索关键字,将来才可以对关键字高亮。

处理高亮结果

高亮的结果与查询的文档结果默认是分离的,并不在一起。

因此解析高亮的代码需要额外处理:

代码说明:

  1. 从结果中获取原始文档json:

    hit.getSourceAsString(),这部分是非高亮的结果,json格式的字符串。还需要反序列为HotelDoc对象

  2. 获取高亮结果,替换掉原本的非高亮值:

    1. 获取高亮Map:hit.getHighlightFields()

      返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值

    2. 从高亮Map中,根据高亮字段名称,获取高亮字段值对象HighlightField

    3. HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了

    4. 用高亮的结果替换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();
}

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值