1. DSL查询分类和基本语法
ElasticSearch提供了基于Json的DSL来定义查询,常见的查询类型包括:
• 查询所有:查询出所有数据,一般测试用,一般不是查出所有,一次性查询20条。例如 match_all
• 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引中匹配,例如:
match_query、mutil_match_query
• 精确查询:根据精确词条查找数据,一般查找keyword、数值、日期、boolean等类型字段。例如:
ids、range(数字或者日期范围)、term(具体的某一个数字);
• 地理(geo)查询:根据经纬度查询。例如:
geo_diatance、geo_bounding_box
• 复合查询,复合查询可以将上述各种查询条件组合几来,合并查询条件,例如:
bool、function_score。
2. DSL全文检索查询语句
会对用户输入内容分词,常用于搜索框搜索。
match:根据一个字段查询,会对用户输入内容分词,然后去倒排索引库检索,语法:
GET/indexName/_search{
"query":{"match"{ # 查询方式,全文检索"FIELD":"TEXT" # 字段:查询文本}}
}
multi_match:根据多个字段查询,参与查询字段越多,查询性能越差,语法:
Get/indexName/_search
{
"query":{"multi_match":{"query": "text","fields": ["field1", "field2"]}}
}
如果想要查询多个字段,建议使用match,将多个字段使用copy to的方式拷贝到一个字段中去,创建一个新的字段名。
3. DSL精确查询语句
精确查询一般是查找keyword、数值、日期、boolean等类型字段,所以不会对搜索条件分词,常见的有:
term:根据词条精确值查询,语法如下:
Get/indexName/_search
{"query":{"term"{"FIELD"{"value": "value"}}}
}
range:根据值范围查询,例如根据日期,数值类型查询,语法如下:
Get/indexName/_search
{“query”:{"range":{"FIELD":{"gte": 10,"lte": 20}}}
}
4 .DSL地理查询语句
根据经纬度查询。常见得场景有:
• 携程:搜索我附近的酒店。
• 滴滴:搜索我附近的滴滴。
• 微信:收缩我附近的人。
地理查询的方式有很多总,如下:
• geo_bounding_box:查询geo_point 值落在某个矩形范围的所有文档,语法如下:
GET /indexName/search
{"query":{"geo_bounding_box":{"FIELD":{"top_left":{"lat": 31.1,"lon": 121.5},"bottom_right":{"lat": 30.9,"lon": 121.7}}}}
}
top_left 和bottom_right代表地图上的两个点,以两个点为准,画出一个矩形,查询的是在矩形范围内的文档,如下图所示:
geo_distance:查询到指定中心点小于某个距离值的所有文档,语法如下:
GET /indexName/_search
{"query":{"geo_distance":{"distance:"15km", ## 距离"FIELD": "31.21,121.5" ## 中心点}}
}
如下图所示,查询的就是以31.21,121.5为中心点,小于15Km范围内的所有文档,适合用于查询附近的人的场景:
5. DSL复合查询
复合查询:可以将其他简单查询组合起来,实现更加复杂的搜索逻辑。
5.1 function score
算分函数查询,可以控制文档相关性算分,控制文档排名。例如,当我们利用match查询时,文件结果会根据与搜索词条的关联度打分,返回结果时按照分值降序排列。
如下图所示,我们搜索 “虹桥如家”,结果如下,会发现包含虹桥如家的分数最高,排在最前面。
打分的算法如下图所示:
ES中使用的是BM25算法,BM25算法相对于TF-IDF算法的优势就是,随着词频不断增加,相关性算分不会无限增加,而TF-IDF算法随着词频不断增加,相关性算分会不断无限增加,如下图所示:
总结:ES中的相关性打分算法在ES5.0之前采用的是TF-IDF,会随着词频增加而越来越大,在ES5.0之后,采用BM25,会随着词频增加而不断增大,但增长曲线会趋于水平。
上面介绍了ES的相关性打分,但是我们可以根据需求修改文档的相关性算分,人为控制排名,根据新得到的算分排序,就需要讲解function score query
5.2 复合查询- function score query
语法:
GET /hotel/_search
{"query":{"function score":{"query":{"match":{"all":"外滩"}}, ## 原始查询条件,搜索文档并根据相关性打分(query score)"functions":[{"filter": {"term":{"id":"1"}}, ## 过滤条件,复合条件的文档才会被重新算分,例如这里是id为1的会重新算分"weight": 10 ## 算分函数,算分函数的结果称为function score,将来会与query score 运算,得到新的算分,常见的算分函数有## 1.weight:给一个常量值,作为函数结果(function score);## 2.field_value_factor:用文档中的某个字段值作为函数结果;## 3. random_score:随机生成一个值,作为函数结果;## 4. script_socre:自定义计算公式,公式结果作为函数结果}],"boost mode":"multiply"## 加权模式,定义function score与query score的运算方式,包括:## 1.multiply:两者相乘。默认就是这个;## 2.replace:用function score 替换 query score;## 3.其他:sum、avg、max、min。}}}
案例:给“如家”这个品牌的酒店排名靠前一些。我们需要考虑function score的三要素:
• 哪些文档需要算分加权?这里就是对应的filter,品牌为如家的需要过滤。
• 算分函数是什么?这里我们可以选择weight算分函数。
• 加权模式是什么?这里对应的就是boost_mode,我们可以选择multiply。
查询语句如下:
GET /hotel/_search
{"query":{"function_score":{"query":{//..·},"functions":[ // 算分函数"filter":{ // 满足的条件,品牌必须是如家"term":{"brand":"如家"}},"weight":2 // 算分权重为2}},"boost_mode": "sum"}}
}
4.5.3 复合查询-Boolean Query
复合查询中的布尔查询是一个或多个查询子句的组合。子句的组合方式有:
• must:必须匹配每个子查询,类似“与”;
• should:选择性匹配子查询,类似“或”;
• must_not : 必须不匹配,不参与算分,类似 “非”;
• filter:必须匹配,不参与算分。
案例,如下图所示:
6. DSL搜索结果处理
6.1 排序
ES支持对搜索结果的排序,默认是根据相关度算分来排序。一旦指定自己的排序字段,ES就放弃相关度算分,查询的效率也会提升。可以排序的字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
keyword类型、数值类型、日期类型排序语法:
GET /indexName/_search
{
"query":{"match_all":{}},
"sort":[{"FIELD1": "desc"//排序字段和排序方式ASC、DESC},{"FIELD2": "asc"//排序字段和排序方式ASC、DESC}]}
根据地理坐标排序语法:
GET /indexName/search
{"query":{"match_all":{}},"sort":[{"_geo_distance" :{"FIELD1":"纬度,经度","order" : "asc","unit" :"km"}}]
}
6.2 分页
ES默认情况下只返回top 10条数据。而如果要查询更多数据就需要修改分页参数了。
ES中通过修改from 、size参数来控制要返回的分页结果,语法如下:
GET /hotel/_search
{"query": {"match_all": {}},"from":990,## 分页开始的位置,默认为0;"size":10,## 期望获取的文档总数"sort":[{"price": "asc"}]
}
ES分页使用虽然跟mysql很像,只需要指定from,size即可,但原理和mysql存在较大的差异,ES分页的原理采用的是一种逻辑分页,假如现在
需要查990到1000的文档数据,如下图所示,ES会查出从0-1000的所有数据,然后截出990-1000的文档数据:
上面这种截取方式对于单个节点的ES是没有问题的,但是在实际情况下,为了能够存储更多的数据,那么ES是会进行集群部署的,一旦进行集群,ES就会对文档数据进行拆分,放到不同的机器上,如下图所示:
ES是分布式的,所以会面临深度分页的问题,例如现在按照price字段排序后,获取from = 990, size = 10的数据,那么就会获取0到1000的文档数据,然后截取,但是现在有多个ES集群部署了,如果只是截取某一台0-1000数据文档的10条数据那么就存在问题。ES集群情况下分页截取的原理如下:
• 首先在每个集群数据分片上都排序并查询前1000条文档;
• 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档;
• 最后从这1000条中,选取从990开始的10条文档。
如果搜索页数过深,或者结果集(from+size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000,一旦超过10000,就会报错,一般我们在实际情况中,从业务层面就限定查询不能超过10000条,如果实际的业务就要查询超过10000条,ES提供了两种解决办法:
• search after:分页时需要排序,原理是从上一次的排序字段后的末尾值开始,走位查询条件,查询下一页数据,分页只能往后翻,不能往前翻。官方推荐使用的方式;
• scroll:原理将排序数据形成快照,保存在内存,对内存消耗非常大。官方已经不推荐使用。
总结:
from + size:
• 优点:支持随机翻页;
• 缺点:深度分页问题,默认查询上限(from+size)是10000场景;
• 百度、京东、谷歌、淘宝这样的随机翻页搜索。
after search :
• 优点:没有查询上限(单次查询的size不超过10000);
• 缺点:只能向后逐页查询,不支持随机翻页;
• 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页。
scroll:
• 优点:没有查询上限(单次查询的size不超过10000);
• 缺点:会有额外内存消耗,并且搜索结果是非实时的
• 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 aftersearch方案。
6.3 高亮
就是在搜索结果中把搜索关键字突出显示。
原理如下:
• 将搜索结果中的关键字用标签标记出来
• 在页面中给标签添加css样式
语法如下:
GET /hotel/_search
{"query":{"match" :{"FIELD": "TEXT"}},
"highlight": {"fields":{ ## 指定要高亮的字段"FIELD":{"pre_tags": "<em>", ## 用来标记高亮字段的前置标签"post_tags":"</em>” ## 用来标记高亮字段的后置标签}}}}