1.什么是Query DSL:
- 1.Elasticsearch提供了一个可以执行查询的Json风格的DSL(domain-specific language领域特定语言)。这个被称为Query DSL,该查询语言非常全面
2.为DSL测试创造数据环境:
- 1.下面是存储一些基础数据:
# POST /student/_doc/1001
{
"name":"zhangsan",
"nickname":"zhangsan",
"sex":"男",
"age":30
}
# POST /student/_doc/1002
{
"name":"lisi",
"nickname":"lisi",
"sex":"男",
"age":20
}
# POST /student/_doc/1003
{
"name":"wangwu",
"nickname":"wangwu",
"sex":"女",
"age":40
}
# POST /student/_doc/1004
{
"name":"zhangsan1",
"nickname":"zhangsan1",
"sex":"女",
"age":50
}
# POST /student/_doc/1005
{
"name":"zhangsan2",
"nickname":"zhangsan2",
"sex":"女",
"age":30
}
3.DSL使用案例:
3.1.查询所有:match_all
a.语法:
b.使用示例
# 示例 使用时不要
GET student/_search
{
"query": { # 查询的字段
"match_all": {}
},
"from": 0, # 从第几条文档开始查
"size": 5, # 查几条文档
"_source":["balance"], # 返回的数据字段, 这里只查balance
"sort": [
{
"account_number": { # 返回结果按哪个列排序
"order": "desc" # 降序
}
}
]
}
“query”:这里的 query 代表一个查询对象,里面可以有不同的查询属性
“match_all”:查询类型,例如:match_all(代表查询所有), match,term , range 等等
{查询条件}:查询条件会根据类型的不同,写法也有差异
b.query说明:
- 1.match_all查询类型【代表查询所有的索引】,es中可以在query中组合非常多的查询类型完成复杂查询;
- 2.除了query参数之外,我们可也传递其他的参数以改变查询结果,如sort,size;
- 3.from+size限定,完成分页功能;
- 4.sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准;
c.响应结果字段含义:
{
"took【查询花费时间,单位毫秒】" : 1116,
"timed_out【是否超时】" : false,
"_shards【分片信息】" : {
"total【总数】" : 1,
"successful【成功】" : 1,
"skipped【忽略】" : 0,
"failed【失败】" : 0
},
"hits【搜索命中结果】" : {
"total"【搜索条件匹配的文档总数】: {
"value"【总命中计数的值】: 3,
"relation"【计数规则】: "eq" # eq 表示计数准确, gte 表示计数不准确
},
"max_score【匹配度分值】" : 1.0,
"hits【命中结果集合】" : [
。。。
}
]
}
}
3.2.全文检索查询:match 及 multi_math
a.match匹配查询:
1.查询特点说明
match 匹配
类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系
,匹配度越高,排名就越靠前
2.案例演示:
- 1.案例1:在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
,其请求体内容如下:
{
"query": {
"match": {
"name":"zhangsan"
}
}
}
- 2.案例2:
b.multi_math【多字段匹配】
案例演示:
- 1.案例1:
state或者address中包含mill
,并且在查询过程中,会对于查询条件进行分词
GET bank/_search
{
"query": {
"multi_match": { # 前面的match仅指定了一个字段。
"query": "mill",
"fields": [ # state和address有mill子串 不要求都有
"state",
"address"
]
}
}
}
- 2.案例2:在 Postman 中,向 ES 服务器发
GET
请求http://127.0.0.1:9200/student/_search
{
"query": {
"multi_match": {
"query": "zhangsan",
"fields": ["name","nickname"]
}
}
}
注意:
查询的时候,使用的字段越多,查询的效率就会越低
,在需要查询多字段的时候,建议是使用cop_to方式来实现多字段查询
3.3.精确查询:term、terms、range、
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词,常见的有term、range
a.关键字精确查询
- 1.
term
查询:精确的关键词匹配查询
,不对查询条件进行分词。和 match 一样。匹配某个属性的值。全文检索字段用 match
,其他非 text 字段匹配用 term
。 - 2.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "term": { "name": { "value": "zhangsan" } } } }
b.多关键字精确查询
- 1.
terms 查询
和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in - 2.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "terms": { "name": ["zhangsan","lisi"] } } }
c.范围查询:
- 1.range 查询找出那些落在指定区间内的数字或者时间。range 查询允许以下字符
- 2.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "range": { "age": { "gte": 30, "lte": 35 } } } }
d.指定查询字段
- 1.默认情况下,Elasticsearch 在搜索的结果中,会把文档中保存在
_source
的所有字段都返回。如果我们只想获取其中的部分字段
,我们可以添加_source 的过滤
,在 Postman 中,向 ES 服务器发GET
请求 :http://127.0.0.1:9200/student/_search
,其请求体是:
{
"_source": ["name","nickname"],
"query": {
"terms": {
"nickname": ["zhangsan"]
}
}
}
e.过滤显示或不显示的字段
- 1.我们也可以通过如下方式过滤字段
- 1.includes:来指定想要显示的字段
- 2.excludes:来指定不想要显示的字段
- 2.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{
"_source": {
"includes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
}
}
- 3.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{
"_source": {
"excludes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
}
}
3.4.地理位置查询:
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:
a.常见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
- 微信:搜索我附近的人
- 附近的酒店:
- 附近的车:
b.矩形范围查询
- 1.矩形范围查询,也就是
geo_bounding_box
查询,查询坐标落在某个矩形范围的所有文档:
- 2.查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点
- 3.语法如下:
// geo_bounding_box查询 GET /indexName/_search { "query": { "geo_bounding_box": { "FIELD": { "top_left": { // 左上点 "lat": 31.1, "lon": 121.5 }, "bottom_right": { // 右下点 "lat": 30.9, "lon": 121.7 } } } } }
c.附近查询
-
1.附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
-
2.换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:
-
3.语法说明:
// geo_distance 查询 GET /indexName/_search { "query": { "geo_distance": { "distance": "15km", // 半径 "FIELD": "31.21,121.5" // 圆心 } } }
-
4.案例:我们先搜索陆家嘴附近15km的酒店:发现共有47家酒店。
-
5.然后把半径缩短到3公里:发现共有5家酒店。
3.5.复合查询:
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
a.相关性算分介绍:
- 1.当我们利用match查询时,
文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列
,例如,我们搜索 “虹桥如家”,结果如下:[ { "_score" : 17.850193, "_source" : { "name" : "虹桥如家酒店真不错", } }, { "_score" : 12.259849, "_source" : { "name" : "外滩如家酒店真不错", } }, { "_score" : 11.91091, "_source" : { "name" : "迪士尼如家酒店真不错", } } ]
- 2.在Elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:
- 3.在后来的5.1版本升级中,Elasticsearch将算法改进为
BM25
算法,公式如下:
- 4.TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:
小结:Elasticsearch会根据词条和文档的相关度做打分,算法由两种:
- TF-IDF算法
- BM25算法,Elasticsearch5.1版本后采用的算法
b.算分函数查询:
b1.说明:
- 1.根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。所以我们需要实现可以自己控制排名]
b2.语法案例说明
- 1.语法中字段的含义介绍:
- 2.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
- 3.function score的运行流程如下:
- 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- 根据过滤条件,过滤文档
- 符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
- 将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
- 4.因此,其中的关键点是:
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
b3.示例
- 1.需求:给“如家”这个品牌的酒店排名靠前一些,翻译一下这个需求,转换为之前说的四个要点:
- 原始条件:不确定,可以任意变化
- 过滤条件:brand = “如家”
- 算分函数:可以简单粗暴,直接给固定的算分结果,weight
- 运算模式:比如求和
- 2.因此最终的DSL语句如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查询,可以是任意条件
"functions": [ // 算分函数
{
"filter": { // 满足的条件,品牌必须是如家
"term": {
"brand": "如家"
}
},
"weight": 2 // 算分权重为2
}
],
"boost_mode": "sum" // 加权模式,求和
}
}
}
- 3.测试,在未添加算分函数时,如家得分如下:
- 4.添加了算分函数后,如家得分就提升了:
b4.小结
- 1.function score query定义的三要素是什么?
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
c.布尔查询
c1.布尔查询组合方式
- 1.布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
- 2.比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤:
- 3.每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用bool查询了。
- 4.需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:
- 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
- 其它过滤条件,采用filter查询。不参与算分
c2.语法示例:
- 1.案例1:
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "华美达" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }}
]
}
}
}
- 2.案例2:在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
,请求体是:
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "zhangsan"
}
}
],
"must_not": [
{
"match": {
"age": "40"
}
}
],
"should": [
{
"match": {
"sex": "男"
}
}
]
}
}
}
c3.案例示例
需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
- 1.分析:
- 名称搜索,属于全文检索查询,应该参与算分。放到must中
- 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
- 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中
c4.小结:
- 1.bool查询有几种逻辑关系?
- must:必须匹配的条件,可以理解为“与”
- should:选择性匹配的条件,可以理解为“或”
- must_not:必须不匹配的条件,不参与打分
- filter:必须匹配的条件,不参与打分
3.6.模糊查询
- 1.返回包含与搜索字词相似的字词的文档。
- 2.
编辑距离
是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:- 更改字符(box → fox)
- 删除字符(black → lack)
- 插入字符(sic → sick)
- 转置两个相邻字符(act → cat)
- 3.为了找到相似的术语,
fuzzy
查询会在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展
。然后查询返回每个扩展的完全匹配。 - 4.通过
fuzziness
修改编辑距离。一般使用默认值 AUTO
,根据术语的长度生成编辑距离。在 Postman 中,向 ES 服务器发GET
请求 :http://127.0.0.1:9200/student/_search
,请求体如下:
{
"query": {
"fuzzy": {
"title": {
"value": "zhangsan"
}
}
}
}
- 5.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"fuzzy": {
"title": {
"value": "zhangsan",
"fuzziness": 2
}
}
}
}
3.7.聚合查询
- 1.聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很多其他的聚合,例如取最大值、平均值等等。
- 2.对某个字段取
最大值 max
,在 Postman 中,向 ES 服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{
"aggs":{
"max_age":{
"max":{"field":"age"}
}
},
"size":0
}
- 3.对某个字段取最
小值 min
:在 Postman 中,向 ES 服务器发GET
请求 :http://127.0.0.1:9200/student/_search
,请求体如下:
{
"aggs":{
"min_age":{
"min":{"field":"age"}
}
},
"size":0
}
- 4.对某个字段的值进行
去重之后再取总数
:Postman 中,向 ES 服务器发GET
请求 :http://127.0.0.1:9200/student/_search2
{
"aggs":{
"distinct_age":{
"cardinality":{"field":"age"}
}
},
"size":0
}
- 5.State 聚合:stats 聚合,对某个字段一次性返回 count,max,min,avg 和 sum 五个指标。在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{
"aggs":{
"stats_age":{
"stats":{"field":"age"}
}
},
"size":0
}
3.8.桶聚合查询
- 1.桶聚和相当于 sql 中的 group by 语句
- terms 聚合,分组统计
- 2.在 Postman 中,向 ES 服务器发
GET 请求
:http://127.0.0.1:9200/student/_search
{
"aggs":{
"age_groupby":{
"terms":{"field":"age"}
}
},
"size":0
}
3.9.query/match_phrase
【短语匹配】
- 1.将需要匹配的值当成一整个单词(不分词)进行检索
match
:拆分字符串进行检索。 包含mill 或 road 或 mill roadmatch_phrase
:不拆分字符串进行检索。 包含mill road字段.keyword
:必须全匹配上才检索成功。
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill road" # 就是说不要匹配只有mill或只有road的,要匹配mill road一整个子串
# "address.keyword": "990 Mill" # 字段后面加上 .keyword, 必须完全匹配
}
}
}
3.10.聚合查询再举例:
- 1.例:搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
GET bank/_search
{
"query": { # 查询出包含mill的
"match": {
"address": "Mill"
}
},
"aggs": { #基于查询聚合
"ageAgg": { # 聚合的名字,随便起
"terms": { # 看值的可能性分布
"field": "age",
"size": 10
}
},
"ageAvg": {
"avg": { # 看age值的平均
"field": "age"
}
},
"balanceAvg": {
"avg": { # 看balance的平均
"field": "balance"
}
}
},
"size": 0 # 不看详情
}
- 返回结果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4, // 命中4条
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : { // 第一个聚合的结果
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 38, # age为38的有2条
"doc_count" : 2
},
{
"key" : 28,
"doc_count" : 1
},
{
"key" : 32,
"doc_count" : 1
}
]
},
"ageAvg" : { // 第二个聚合的结果
"value" : 34.0 # balance字段的平均值是34
},
"balanceAvg" : {
"value" : 25208.0
}
}
}
- 例2:按照年龄聚合,并且求这些年龄段的这些人的平均薪资
aggs/aggName/aggs/aggName
子聚合- 写到一个聚合里是基于上个聚合进行子聚合。
- 下面求每个age分布的平均balance
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": { # 看分布
"field": "age",
"size": 100
},
"aggs": { # 与terms并列
"ageAvg": { #平均
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
- 例3:复杂子聚合:查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": { # 看age分布
"field": "age",
"size": 100
},
"aggs": { # 子聚合
"genderAgg": {
"terms": { # 看gender分布
"field": "gender.keyword" # 注意这里,文本字段应该用.keyword
},
"aggs": { # 子聚合
"balanceAvg": {
"avg": { # 男性的平均
"field": "balance"
}
}
}
},
"ageBalanceAvg": {
"avg": { #age分布的平均(男女)
"field": "balance"
}
}
}
}
},
"size": 0
}
3.搜索结果处理:
搜索的结果可以按照用户指定的方式去处理或展示
3.1.排序:
- Elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序
- 可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
a.单字段排序
a1.语法:
- 1.sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式,desc 降序,asc升序
- 2.在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
,请求体如下:
a2.案例说明与实现:
b.多字段排序
- 1.案例1:需求与实现:
3.2.分页查询
a.基本分页:
a1.分页举例
- 1.Elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。Elasticsearch中通过
修改from、size参数来控制要返回的分页结果
:- from:从第几个文档开始
- size:总共查询几个文档
a2.深度分页问题:
- 1.假设:这里是查询990开始的数据,也就是 第990~第1000条 数据。不过,Elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:
- 2.查询TOP1000,如果es是单点模式,这并无太大影响。但是Elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。因为节点A的TOP200,在另一个节点可能排到10000名以外了。因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。
- 3.那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?
当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力
,因此elasticsearch会禁止from+ size 超过10000的请求
。
a3.深度分页问题解决方案:
- 1.针对深度分页,ES提供了两种解决方案,官方文档:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。
官方推荐使用的方式
。 - scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。
b.小结
分页查询的常见实现方案以及优缺点:
from + size
:- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search
:- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll
:- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
3.3.高亮查询
a.说明:
- 1.在进行关键字搜索时
,搜索出的内容中的关键字会显示不同的颜色
,称之为高亮
。如在百度搜索"京东 - 2.Elasticsearch 可以对查询内
容中的关键字部分
,进行标签和样式(高亮)的设置
。在使用 match 查询的同时,加上一个 highlight 属性
:- pre_tags:前置标签
- post_tags:后置标签
- fields:需要高亮的字段
b.案例1:
- 在 Postman 中,向 ES 服务器发
GET
请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name": "zhangsan"
}
},
"highlight": {
"pre_tags": "<font color='red'>",
"post_tags": "</font>",
"fields": {
"name": {}
}
}
}
c.案例2:
如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
3.4.filter【结果过滤】
- 1.上面的must和should影响相关性得分,而must_not仅仅是一个filter ,不贡献得分,must改为filter就使must不贡献得分,如果只有filter条件的话,我们会发现得分都是0
- 2.并不是所有的查询都需要产生分数,特别是哪些仅用于filtering过滤的文档。不参与评分更快, 为了不计算分数,elasticsearch会自动检查场景并且优化查询的执行。
GET bank/_search
{
"query": {
"bool": {
"must": [
{ "match": {"address": "mill" } }
],
"filter": { # query.bool.filter
"range": {
"balance": { # 哪个字段
"gte": "10000",
"lte": "20000"
}
}
}
}
}
}
这里先是查询所有匹配address包含mill的文档,然后再根据10000<=balance<=20000进行过滤查询结果