当前位置:   article > 正文

ElasticSearch内容分享(四):ES搜索引擎

es搜索引擎

目录

ES搜索引擎

1. DSL设置查询条件

1.1 DSL查询分类

1.2 全文检索查询

1.2.1 使用场景

1.2.2 match查询

1.2.3 mulit_match查询

1.3 精准查询

1.3.1 term查询

1.3.2 range查询

1.4 地理坐标查询

1.4.1 矩形范围查询

1.4.2 附近(圆形)查询

1.5 复合查询

1.5.0 复合查询归纳

1.5.1 相关性算分

1.5.2 算分函数查询

function score 查询

1.5.3 布尔查询

bool查询

2. 设置搜索结果

2.0 搜索结果种类

2.1 排序

2.1.1普通字段排序

2.1.2 地理坐标排序

2.2 分页

2.2.1 基本分页

2.2.2 深度分页

2.3 高亮

高亮原理

实现高亮

2.4 数据聚合

2.4.1 聚合种类

2.4.2 桶(Bucket)聚合

2.4.3 度量(Metric) and 管道(pipeline)聚合

3. RestClient查询文档

3.1 快速入门

3.1.1 发送查询请求

3.1.2 解析响应结果

3.1.3 完整代码

3.2 设置查询条件

3.2.1 全文检索查询

3.2.2 精准查询

3.2.3 地理查询

3.2.4 布尔查询

3.2.5 算分函数查询

3.3 设置搜索结果

3.3.1 排序和分页

3.3.2 高亮

3.3.3 聚合


ES搜索引擎

elasticsearch的查询依然是基于JSON风格的DSL来实现的。

1. DSL设置查询条件

1.1 DSL查询分类

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all

  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:

    • match_query
    • multi_match_query
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:

    • ids
    • range
    • term
  • 地理(geo)查询:根据经纬度查询。例如:

    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:

    • bool
    • function_score

查询的语法基本一致:

  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "查询类型": {
  5.       "查询条件""条件值"
  6.     }
  7.   }
  8. }

我们以查询所有为例,其中:

  • 查询类型为match_all
  • 没有查询条件
  1. // 查询所有
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "match_all": {
  6. }
  7.   }
  8. }

其它查询无非就是查询类型查询条件的变化。

1.2 全文检索查询

match和multi_match的区别是什么?

  • match:根据一个字段查询【推荐:使用copy_to构造all字段】
  • multi_match:根据多个字段查询,参与查询字段越多,查询性能越差

注:搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式。

1.2.1 使用场景

全文检索查询的基本流程如下:

  • 对用户搜索的内容做分词,得到词条
  • 根据词条去倒排索引库中匹配,得到文档id
  • 根据文档id找到文档,返回给用户

比较常用的场景包括:

  • 商城的输入框搜索
  • 百度输入框搜索

例如京东:

image

因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段。

常见的全文检索查询包括:

  • match查询:单字段查询
  • multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
1.2.2 match查询

match查询语法如下:

  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "match": {
  5.       "FIELD""TEXT"
  6.     }
  7.   }
  8. }

match查询示例:

image

1.2.3 mulit_match查询

mulit_match语法如下:

  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "multi_match": {
  5.       "query""TEXT",
  6.       "fields": ["FIELD1"" FIELD12"]
  7.     }
  8.   }
  9. }

multi_match查询示例:

image

1.3 精准查询

精准查询类型:

  • term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
  • range查询:根据数值范围查询,可以是数值、日期的范围

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:

  • term:根据词条精确值查询
  • range:根据值的范围查询
1.3.1 term查询

因为精确查询的字段搜时不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。

语法说明:

  1. // term查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "term": {
  6.       "FIELD": {
  7.         "value""VALUE"
  8.       }
  9.     }
  10.   }
  11. }

示例:

当我搜索的是精确词条时,能正确查询出结果:

image

但是,当我搜索的内容不是词条,而是多个词语形成的短语时,反而搜索不到:

image

1.3.2 range查询

范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。

基本语法:

  1. // range查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "range": {
  6.       "FIELD": {
  7.         "gte": 10, // 这里的gte代表大于等于,gt则代表大于
  8.         "lte": 20 // lte代表小于等于,lt则代表小于
  9.       }
  10.     }
  11.   }
  12. }

示例:

image

1.4 地理坐标查询

所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html

常见的使用场景包括:

  • 携程:搜索我附近的酒店
  • 滴滴:搜索我附近的出租车
  • 微信:搜索我附近的人

附近的酒店:

image

附近的车:

image

1.4.1 矩形范围查询

很少有业务有这种需求

矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:

image

查询时,需要指定矩形的左上右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。

语法如下:

  1. // geo_bounding_box查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "geo_bounding_box": {
  6.       "FIELD": {
  7.         "top_left": { // 左上点
  8.           "lat": 31.1,
  9.           "lon": 121.5
  10.         },
  11.         "bottom_right": { // 右下点
  12.           "lat": 30.9,
  13.           "lon": 121.7
  14.         }
  15.       }
  16.     }
  17.   }
  18. }
1.4.2 附近(圆形)查询

附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。

换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:

image

语法说明:

  1. // geo_distance 查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "geo_distance": {
  6.       "distance""15km", // 半径
  7.       "FIELD""31.21,121.5" // 圆心
  8.     }
  9.   }
  10. }

示例:

我们先搜索陆家嘴附近15km的酒店:

image

发现共有47家酒店。

1.5 复合查询

复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:

  • fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
  • bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
1.5.0 复合查询归纳
  1. GET /hotel/_search
  2. {
  3. "query": {
  4.     "function_score": {
  5. "query": { // 原始查询,可以是任意条件
  6. "bool": {
  7. "must": [
  8. {"term": {"city""上海" }}
  9. ],
  10. "should": [
  11. {"term": {"brand""皇冠假日" }},
  12. {"term": {"brand""华美达" }}
  13. ],
  14. "must_not": [
  15. "range": { "price": { "lte": 500 } }}
  16. ],
  17. "filter": [
  18. "range": {"score": { "gte": 45 } }}
  19. ]
  20. }
  21. },
  22.       "functions": [ // 算分函数
  23.         {
  24.           "filter": { // 满足的条件,品牌必须是如家【品牌是如家的才加分,这里是加分条件】
  25.             "term": {
  26.               "brand""如家"
  27.             }
  28.           },
  29.           "weight": 2 // 算分权重为2
  30.         }
  31.       ],
  32. "boost_mode": "sum" // 加权模式,求和
  33.     }
  34.   }
  35. }
1.5.1 相关性算分

elasticsearch会根据词条和文档的相关度做打分,算法由两种:

  • TF-IDF算法
  • BM25算法,elasticsearch5.1版本后采用的算法

当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。

例如,我们搜索 "虹桥如家",结果如下:

  1. [
  2.   {
  3.     "_score" : 17.850193,
  4.     "_source" : {
  5.       "name" : "虹桥如家酒店真不错",
  6.     }
  7.   },
  8.   {
  9.     "_score" : 12.259849,
  10.     "_source" : {
  11.       "name" : "外滩如家酒店真不错",
  12.     }
  13.   },
  14.   {
  15.     "_score" : 11.91091,
  16.     "_source" : {
  17.       "name" : "迪士尼如家酒店真不错",
  18.     }
  19.   }
  20. ]

在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:

image

在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:

image

TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:

image

1.5.2 算分函数查询

在搜索出来的结果的分数基础上,再手动与指定的数字进行一定运算来改变算分,从而改变结果的排序。

function score query定义的三要素是什么?

  • 过滤条件:哪些文档要加分
  • 算分函数:如何计算function score
  • 加权方式:function score 与 query score如何运算

根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。

以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图:

image

要想认为控制相关性算分,就需要利用elasticsearch中的function score 查询了。

function score 查询

1)语法说明

image

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)根据过滤条件,过滤文档
  • 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
  • 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。

2)举例

需求:给“如家”这个品牌的酒店排名靠前一些

翻译一下这个需求,转换为之前说的四个要点:

  • 原始条件:不确定,可以任意变化
  • 过滤条件:brand = "如家"
  • 算分函数:可以简单粗暴,直接给固定的算分结果,weight
  • 运算模式:比如求和

因此最终的DSL语句如下:

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "function_score": {
  5.       "query": { .... }, // 原始查询,可以是任意条件
  6.       "functions": [ // 算分函数
  7.         {
  8.           "filter": { // 满足的条件,品牌必须是如家【品牌是如家的才加分,这里是加分条件】
  9.             "term": {
  10.               "brand""如家"
  11.             }
  12.           },
  13.           "weight": 2 // 算分权重为2
  14.         }
  15.       ],
  16. "boost_mode": "sum" // 加权模式,求和
  17.     }
  18.   }
  19. }

测试,在未添加算分函数时,如家得分如下:

image

添加了算分函数后,如家得分就提升了:

image

1.5.3 布尔查询

布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分

注意:尽量在筛选的时候多使用不参与算分的must_not和filter,以保证性能良好

比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤:

image

每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用bool查询了。

需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:

  • 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
  • 其它过滤条件,采用filter查询。不参与算分
bool查询

1)语法

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "bool": {
  5.       "must": [
  6.         {"term": {"city""上海" }}
  7.       ],
  8.       "should": [
  9.         {"term": {"brand""皇冠假日" }},
  10. {"term": {"brand""华美达" }}
  11.       ],
  12.       "must_not": [
  13.         { "range": { "price": { "lte": 500 } }}
  14.       ],
  15.       "filter": [
  16.         { "range": {"score": { "gte": 45 } }}
  17.       ]
  18.     }
  19.   }
  20. }

2)示例

需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

分析:

  • 名称搜索,属于全文检索查询,应该参与算分。放到must中
  • 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
  • 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中

image

2. 设置搜索结果

搜索的结果可以按照用户指定的方式去处理或展示。

2.0 搜索结果种类

查询的DSL是一个大的JSON对象,包含下列属性:

  • query:查询条件
  • from和size:分页条件
  • sort:排序条件
  • highlight:高亮条件
  • aggs:定义聚合

示例:

image

image

2.1 排序

在使用排序后就不会进行算分了,根据排序设置的规则排列

普通字段是根据字典序排序

地理坐标是根据举例远近排序

2.1.1普通字段排序

keyword、数值、日期类型排序的排序语法基本一致。

语法

排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
(可以参考下面的图片案例)

  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "match_all": {}
  5.   },
  6.   "sort": [
  7.     {
  8.       "FIELD""desc"  // 排序字段、排序方式ASC、DESC
  9.     }
  10.   ]
  11. }

示例

需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序

image

2.1.2 地理坐标排序

地理坐标排序略有不同。

语法说明

  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "match_all": {}
  5.   },
  6.   "sort": [
  7.     {
  8.       "_geo_distance" : {
  9.           "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
  10.           "order" : "asc", // 排序方式
  11.           "unit" : "km" // 排序的距离单位
  12.       }
  13.     }
  14.   ]
  15. }

这个查询的含义是:

  • 指定一个坐标,作为目标点
  • 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
  • 根据距离排序

示例:

需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序

提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/

假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。

image

2.2 分页

elasticsearch会禁止from+ size 超过10000的请求

elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

类似于mysql中的limit ?, ?

2.2.1 基本分页

分页的基本语法如下:

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "match_all": {}
  5.   },
  6.   "from": 0, // 分页开始的位置,默认为0
  7.   "size": 10, // 期望获取的文档总数
  8.   "sort": [
  9.     {"price""asc"}
  10.   ]
  11. }
2.2.2 深度分页

原理:elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条

现在,我要查询990~1000的数据,查询逻辑要这么写:

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "match_all": {}
  5.   },
  6.   "from": 990, // 分页开始的位置,默认为0
  7.   "size": 10, // 期望获取的文档总数
  8.   "sort": [
  9.     {"price""asc"}
  10.   ]
  11. }

这里是查询990开始的数据,也就是 第990~第1000条 数据。

集群情况的深度分页

针对深度分页,ES提供了两种解决方案,官方文档

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。【官方推荐】
  • scroll:原理将排序后的文档id形成快照,保存在内存。

不过,elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:

image

查询TOP1000,如果es是单点模式,这并无太大影响。

但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。

因为节点A的TOP200,在另一个节点可能排到10000名以外了。

因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。

image

那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求

2.3 高亮

注意:

  • 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
  • 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
  • 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false

使用场景:在百度等搜索后,会对结果中出现搜索字段的部分进行高亮处理。

高亮原理

高亮显示的实现分为两步:

  • 1)给文档中的所有关键字都添加一个标签,例如<em>标签
  • 2)页面给<em>标签编写CSS样式
实现高亮

1)语法

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "match": {
  5.       "FIELD""TEXT" // 查询条件,高亮一定要使用全文检索查询
  6.     }
  7.   },
  8.   "highlight": {
  9.     "fields": { // 指定要高亮的字段
  10.       "FIELD": { //【要和上面的查询字段FIELD一致】
  11.         "pre_tags""<em>",  // 用来标记高亮字段的前置标签
  12.         "post_tags""</em>" // 用来标记高亮字段的后置标签
  13.       }
  14.     }
  15.   }
  16. }

2)示例:组合字段all的案例

image

2.4 数据聚合

类似于mysql中的【度量(Metric)聚合】聚合语句实现AVG,MAX,MIN;以及【桶(Bucket)聚合】GroupBy实现分组

聚合(aggregations可以让我们极其方便的实现对数据的统计、分析、运算。例如:

  • 什么品牌的手机最受欢迎?
  • 这些手机的平均价格、最高价格、最低价格?
  • 这些手机每月的销售情况如何?

实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。

aggs代表聚合,与query同级,此时query的作用是?

  • 限定聚合的的文档范围

聚合必须的三要素:

  • 聚合名称
  • 聚合类型
  • 聚合字段

聚合可配置属性有:

  • size:指定聚合结果数量
  • order:指定聚合结果排序方式
  • field:指定聚合字段
2.4.1 聚合种类

注意:参加聚合的字段必须是keyword、日期、数值、布尔类型

聚合常见的有三类:

  • 桶(Bucket)聚合:用来对文档做分组

    • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等

    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求max、min、avg、sum等
  • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

    如:用桶聚合实现种类排序,然后使用度量聚合实现各个桶的最大值、最小值、平均值等

2.4.2 桶(Bucket)聚合

以统计酒店品牌种类,并对其进行数据分组

  1. GET /hotel/_search
  2. {
  3. "query": { //限定要聚合的文档范围,只要添加query条件【一般在没搜索关键字时不写query】
  4.     "range": {
  5.       "price": {
  6.         "lte": 200 // 只对200元以下的文档聚合
  7.       }
  8.     }
  9.   }, 
  10.   "size": 0,  // 设置size为0,结果中不包含查询结果文档,只包含聚合结果
  11.   "aggs": { // 定义聚合
  12.     "brandAgg": { //给聚合起个名字
  13.       "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
  14.         "field""brand", // 参与聚合的字段
  15. "order": {
  16.           "doc_count""asc" // 对聚合结果按照doc_count升序排列
  17.         },
  18.         "size": 20 // 希望获取的聚合结果数量【设置多少就最多只显示多少】
  19.       }
  20.     }
  21.   }
  22. }

image

2.4.3 度量(Metric) and 管道(pipeline)聚合

度量聚合很少单独使用,一般是和桶聚合一并结合使用

我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。

这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。

语法如下:

这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。

  1. GET /hotel/_search
  2. {
  3.   "size": 0, 
  4.   "aggs": {
  5.     "brandAgg": { 
  6.       "terms": { 
  7.         "field""brand"
  8. "order": {
  9.           "scoreAgg.avg""desc" // 对聚合结果按照指定字段降序排列
  10.         },
  11.         "size": 20
  12.       },
  13.       "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
  14.         "score_stats": { // 聚合名称
  15.           "stats": { // 聚合类型,这里stats可以计算min、max、avg等
  16.             "field""score" // 聚合字段,这里是score
  17.           }
  18.         }
  19.       }
  20.     }
  21.   }
  22. }

另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序:

image

3. RestClient查询文档

文档的查询同样适用昨天学习的 RestHighLevelClient对象,基本步骤包括:

  • 1)准备Request对象
  • 2)准备请求参数
  • 3)发起请求
  • 4)解析响应
3.1 快速入门

查询的基本步骤是:

  1. 创建SearchRequest对象

  2. 准备Request.source(),也就是DSL。

    ① QueryBuilders来构建查询条件

    ② 传入Request.source() 的 query() 方法

  3. 发送请求,得到结果

  4. 解析结果(参考JSON结果,从外到内,逐层解析)

3.1.1 发送查询请求

image

代码解读:

  • 第一步,创建SearchRequest对象,指定索引库名

  • 第二步,利用request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等

    • query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
  • 第三步,利用client.search()发送请求,得到响应

这里关键的API有两个,一个是request.source(),其中包含了查询、排序、分页、高亮等所有功能:

image

另一个是QueryBuilders,其中包含match、term、function_score、bool等各种查询:

image

3.1.2 解析响应结果

响应结果的解析:

image

elasticsearch返回的结果是一个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文档数据
3.1.3 完整代码

完整代码如下:

  1. @Test
  2. void testMatchAll() throws IOException {
  3. // 1.准备Request
  4. SearchRequest request = new SearchRequest("hotel");
  5. // 2.准备DSL
  6. request.source()
  7. .query(QueryBuilders.matchAllQuery());
  8. // 3.发送请求
  9. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  10. // 4.解析响应
  11. handleResponse(response);
  12. }
  13. private void handleResponse(SearchResponse response) {
  14. // 4.解析响应
  15. SearchHits searchHits = response.getHits();
  16. // 4.1.获取总条数
  17. long total = searchHits.getTotalHits().value;
  18. System.out.println("共搜索到" + total + "条数据");
  19. // 4.2.文档数组
  20. SearchHit[] hits = searchHits.getHits();
  21. // 4.3.遍历
  22. for (SearchHit hit : hits) {
  23. // 获取文档source
  24. String json = hit.getSourceAsString();
  25. // 反序列化
  26. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  27. System.out.println("hotelDoc = " + hotelDoc);
  28. }
  29. }
3.2 设置查询条件
3.2.1 全文检索查询

全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。

image

因此,Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法:

image

而结果解析代码则完全一致,可以抽取并共享。

完整代码如下:

  1. @Test
  2. void testMatch() throws IOException {
  3. // 1.准备Request
  4. SearchRequest request = new SearchRequest("hotel");
  5. // 2.准备DSL
  6. request.source()
  7. .query(QueryBuilders.matchQuery("all", "如家"));
  8. // 3.发送请求
  9. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  10. // 4.解析响应
  11. handleResponse(response);
  12. }
3.2.2 精准查询

精确查询主要是两者:

  • term:词条精确匹配
  • range:范围查询

与之前的查询相比,差异同样在查询条件,其它都一样。

查询条件构造的API如下:

image

3.2.3 地理查询

DSL格式

image

cn.itcast.hotel.service.implHotelServicesearch方法中,添加一个排序功能:

image

完整代码:

  1. @Override
  2. public PageResult search(RequestParams params) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. // 2.1.query
  8. buildBasicQuery(params, request);
  9. // 2.2.分页
  10. int page = params.getPage();
  11. int size = params.getSize();
  12. request.source().from((page - 1) * size).size(size);
  13. // 2.3.排序
  14. String location = params.getLocation();
  15. if (location != null && !location.equals("")) {
  16. request.source().sort(SortBuilders
  17. .geoDistanceSort("location", new GeoPoint(location))
  18. .order(SortOrder.ASC)
  19. .unit(DistanceUnit.KILOMETERS)
  20. );
  21. }
  22. // 3.发送请求
  23. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  24. // 4.解析响应
  25. return handleResponse(response);
  26. } catch (IOException e) {
  27. throw new RuntimeException(e);
  28. }
  29. }
3.2.4 布尔查询

布尔查询是用must、must_not、filter等方式组合其它查询,代码示例如下:

image

可以看到,API与其它查询的差别同样是在查询条件的构建,QueryBuilders,结果解析等其他代码完全不变。

完整代码如下:

  1. @Test
  2. void testBool() throws IOException {
  3. // 1.准备Request
  4. SearchRequest request = new SearchRequest("hotel");
  5. // 2.准备DSL
  6. // 2.1.准备BooleanQuery
  7. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  8. // 2.2.添加term
  9. boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
  10. // 2.3.添加range
  11. boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
  12. request.source().query(boolQuery);
  13. // 3.发送请求
  14. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  15. // 4.解析响应
  16. handleResponse(response);
  17. }
3.2.5 算分函数查询

java代码逻辑:添加一个isAD字段,在算分函数的filter中判断isAD=ture就进行重新算分

function_score查询结构如下:

image

对应的JavaAPI如下:

image

我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件算分函数加权模式了。

  1. // 算分控制
  2. FunctionScoreQueryBuilder functionScoreQuery =
  3. QueryBuilders.functionScoreQuery(
  4. // 原始查询,相关性算分的查询
  5. boolQuery,
  6. // function score的数组
  7. new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
  8. // 其中的一个function score 元素
  9. new FunctionScoreQueryBuilder.FilterFunctionBuilder(
  10. // 过滤条件
  11. QueryBuilders.termQuery("isAD", true),
  12. // 算分函数
  13. ScoreFunctionBuilders.weightFactorFunction(10)
  14. )
  15. });
  16. //将查询请求放入查询
  17. request.source().query(functionScoreQuery);
3.3 设置搜索结果
3.3.1 排序和分页

由于这两个比较简单,所以一起写了

搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。

对应的API如下:

image

完整代码示例:

  1. @Test
  2. void testPageAndSort() throws IOException {
  3. // 页码,每页大小
  4. int page = 1, size = 5;
  5. // 1.准备Request
  6. SearchRequest request = new SearchRequest("hotel");
  7. // 2.准备DSL
  8. // 2.1.query
  9. request.source().query(QueryBuilders.matchAllQuery());
  10. // 2.2.排序 sort
  11. request.source().sort("price", SortOrder.ASC);
  12. // 2.3.分页 from、size
  13. request.source().from((page - 1) * size).size(5);
  14. // 3.发送请求
  15. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  16. // 4.解析响应
  17. handleResponse(response);
  18. }
3.3.2 高亮

高亮的代码与之前代码差异较大,有两点:

  • 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
  • 结果解析:结果除了要解析_source文档数据,还要解析高亮结果

1)高亮请求构建

高亮请求的构建API如下:

image

上述代码省略了查询条件部分,但是大家不要忘了:高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮。

完整代码如下:

  1. @Test
  2. void testHighlight() throws IOException {
  3. // 1.准备Request
  4. SearchRequest request = new SearchRequest("hotel");
  5. // 2.准备DSL
  6. // 2.1.query
  7. request.source().query(QueryBuilders.matchQuery("all", "如家"));
  8. // 2.2.高亮
  9. request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
  10. // 3.发送请求
  11. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  12. // 4.解析响应
  13. handleResponse(response);
  14. }

2)高亮结果解析

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

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

image

代码解读:

  • 第一步:从结果中获取source。hit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为HotelDoc对象
  • 第二步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值
  • 第三步:从map中根据高亮字段名称,获取高亮字段值对象HighlightField
  • 第四步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
  • 第五步:用高亮的结果替换HotelDoc中的非高亮结果

完整代码如下:

  1. private void handleResponse(SearchResponse response) {
  2. // 4.解析响应
  3. SearchHits searchHits = response.getHits();
  4. // 4.1.获取总条数
  5. long total = searchHits.getTotalHits().value;
  6. System.out.println("共搜索到" + total + "条数据");
  7. // 4.2.文档数组
  8. SearchHit[] hits = searchHits.getHits();
  9. // 4.3.遍历
  10. for (SearchHit hit : hits) {
  11. // 获取文档source
  12. String json = hit.getSourceAsString();
  13. // 反序列化
  14. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  15. // 获取高亮结果
  16. Map<String, HighlightField> highlightFields = hit.getHighlightFields();
  17. if (!CollectionUtils.isEmpty(highlightFields)) {
  18. // 根据字段名获取高亮结果
  19. HighlightField highlightField = highlightFields.get("name");
  20. if (highlightField != null) {
  21. // 获取高亮值
  22. String name = highlightField.getFragments()[0].string();
  23. // 覆盖非高亮结果
  24. hotelDoc.setName(name);
  25. }
  26. }
  27. System.out.println("hotelDoc = " + hotelDoc);
  28. }
  29. }
3.3.3 聚合

聚合条件与query条件同级别,因此需要使用request.source()来指定聚合条件。

聚合条件的语法:

image

聚合的结果也与查询结果不同,API也比较特殊。不过同样是JSON逐层解析:

image

举例:业务代码

  1. @Override
  2. public Map<String, List<String>> filters(RequestParams params) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. // 2.1.query查询语句
  8. buildBasicQuery(params, request);
  9. // 2.2.设置size
  10. request.source().size(0);
  11. // 2.3.聚合
  12. buildAggregation(request);
  13. // 3.发出请求
  14. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  15. // 4.解析结果
  16. Map<String, List<String>> result = new HashMap<>();
  17. Aggregations aggregations = response.getAggregations();
  18. // 4.1.根据品牌名称,获取品牌结果
  19. List<String> brandList = getAggByName(aggregations, "brandAgg");
  20. result.put("品牌", brandList);
  21. // 4.2.根据品牌名称,获取品牌结果
  22. List<String> cityList = getAggByName(aggregations, "cityAgg");
  23. result.put("城市", cityList);
  24. // 4.3.根据品牌名称,获取品牌结果
  25. List<String> starList = getAggByName(aggregations, "starAgg");
  26. result.put("星级", starList);
  27. return result;
  28. } catch (IOException e) {
  29. throw new RuntimeException(e);
  30. }
  31. }
  32. private void buildAggregation(SearchRequest request) {
  33. request.source().aggregation(AggregationBuilders
  34. .terms("brandAgg")
  35. .field("brand")
  36. .size(100)
  37. );
  38. request.source().aggregation(AggregationBuilders
  39. .terms("cityAgg")
  40. .field("city")
  41. .size(100)
  42. );
  43. request.source().aggregation(AggregationBuilders
  44. .terms("starAgg")
  45. .field("starName")
  46. .size(100)
  47. );
  48. }
  49. private List<String> getAggByName(Aggregations aggregations, String aggName) {
  50. // 4.1.根据聚合名称获取聚合结果
  51. Terms brandTerms = aggregations.get(aggName);
  52. // 4.2.获取buckets
  53. List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
  54. // 4.3.遍历
  55. List<String> brandList = new ArrayList<>();
  56. for (Terms.Bucket bucket : buckets) {
  57. // 4.4.获取key
  58. String key = bucket.getKeyAsString();
  59. brandList.add(key);
  60. }
  61. return brandList;
  62. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/780127
推荐阅读
相关标签
  

闽ICP备14008679号