一、 DSL查询文档
1. DSL查询分类
DSL Query的分类
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
利用下面的代码可以对索引库数据进行查询所有的指令,当运行成功以后我们会发现,数据并不是 所有的,是因为对内存和对服务器有压力,只是查出来一部分,当后面学到了分页查询就可以查后面的数据了!!
#在Dev-Tools中
#查询所有
GET /hotel/_search
{
"query": {
"match_all": {
}
}
}
2. 全文检索查询
全文检索查询,会对用户输入内容分词,常用于搜索框搜索:
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:
# match查询
GET /hotel/_search
{
"query": {
"match": {
"all": "如家外滩"
}
}
}
multi_match:与match查询类似,只不过允许同时查询多个字段,语法:
# multi_match查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家外滩",
"fields": ["brand","name"]
}
}
}
总结:
match和multi_match的区别是什么?
- match:根据一个字段查询
- multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
3. 精准查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。是一个不可分割的整体,所以不会对搜索条件分词。常见的有:
- term:根据词条精确值查询
- range:根据值的范围查询
# 精确查询————term
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "上海"
}
}
}
}
# 精确查询————range
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 100, #大于等于,gt:大于
"lte": 1000 # 小于等于 lt 小于
}
}
}
}
4. 地理坐标查询
根据经纬度查询。常见的使用场景包括:
-
携程:搜索我附近的酒店
-
滴滴:搜索我附近的出租车
-
微信:搜索我附近的人
根据经纬度查询。例如:
-
geo_bounding_box:查询geo_point值落在某个矩形范围的所有文档
# geo_bounding_box 查询
GET /hotel/_search
{
"query": {
"geo_bounding_box":{
"location":{
# 以左上角和右下角为长宽 形成一个矩形
"top_left":{
"lat":31.1,
"lon":121.5
},
"bottom_right":{
"lat":30.9,
"lon":121.7
}
}
}
}
}
-
geo_distance:查询到指定中心点小于某个距离值的所有文档
# geo_distance 查询
GET /hotel/_search
{
"query": {
"geo_distance":{
"distance":"10km",
"location":"31.21,121.5"
}
}
}
5. 组合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:
-
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。比如说我们在搜索外滩+如家的时候 会发现这两者都包含的会显示在靠前的位置,得分会比较高,而只包含二者中的一者时候会靠后,得分比较低,这就是相关性。例如百度竞价。允许有广告的排在第一上。
相关性算分
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
总结:
elasticsearch中的相关性打分算法是什么?
- TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
- BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平
Function Score Query
可以修改文档的相关性算分(query score),根据新得到的算分排序。一般来说查出来的都会按照相关性由高向下排,但是比如说人家花了钱了,虽然相关性不高但是也需要给人家的信息提前。比如百度搜索的时候,一般来说首发都是广告...
案例:给“如家”这个品牌的酒店排名靠前一些???
GET /hotel/_search
{
"query": {
"function_score": {
"query": {"match": { # 正常搜索方式
"all": "外滩"
}},
"functions": [
{
"filter": {
"term": {
"brand": "如家" # 过滤条件:
#在正常搜索出的结果对如家酒店+10分 并且与原始分数相乘
}
},
"weight": 10 # 算分函数
}
],
"boost_mode": "multiply" #加权方式 function score与query score如何运算
}
}
}
总结:
function score query定义的三要素是什么?
-
过滤条件:哪些文档要加分
-
算分函数:如何计算function score
-
加权方式:function score 与 query score如何运算
复合查询 Boolean Query
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
不参与算分:只会输出是或者否
比如说下面的图片,当我们在搜索时,比如在点击城市为”上海“,价格在”100-300元的基础上,再搜索栏里面搜索“如家”的时候,前两者就可以用filter 不参与算分,这样可以减少服务器的计算压力。
案例:利用bool查询实现功能
需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
二、 搜索结果处理
1. 排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
案例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
# sort 排序
GET /hotel/_search
{
"query": {
"match_all": {}
}
, "sort": [
{
"score": "desc"
},
{
#当分数相同时,看价格那个便宜
"price": "asc"
}
]
}
案例:实现对酒店数据按照到你的位置坐标的距离升序排序
获取经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
# 找到114.50856,38.083942(自己所在位置) 周围的酒店,距离升序
GET /hotel/_search
{
"query": {
"match_all": {}
}
, "sort": [
{
"_geo_distance": {
"location": {
"lat": 38.083942,
"lon": 114.50856
},
"order": "asc" ,
"unit": "km"
}
}
]
}
当运行完结果就会发现score=null,也就是说当我们在排序的时候就不参与打分了,因为无意义了,这样也会提高效率,减小计算压力!!
2. 分页
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch中通过修改from、size参数来控制要返回的分页结果:
当单点分页的时候会发现很正常可以理解这样的分页逻辑,但是当面对集群时呢?
ES是分布式的,所以会面临深度分页问题。例如按price排序后,获取from = 990,size =10的数据:
- 首先在每个数据分片上都排序并查询前1000条文档。
- 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
- 最后从这1000条中,选取从990开始的10条文档
如果搜索页数过深,或者结果集(from + size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000
那如果我们查询的数据结果集大于10000该怎么办呢?
深度分页解决方案
针对深度分页,ES提供了两种解决方案:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
总结:
from + size:
-
优点:支持随机翻页
-
缺点:深度分页问题,默认查询上限(from + size)是10000
-
场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search:
-
优点:没有查询上限(单次查询的size不超过10000)
-
缺点:只能向后逐页查询,不支持随机翻页
-
场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:
-
优点:没有查询上限(单次查询的size不超过10000)
-
缺点:会有额外内存消耗,并且搜索结果是非实时的
-
场景:海量数据的获取和迁移。
从ES7.1开始不推荐,建议用 after search方案。
3. 高亮
高亮:就是在搜索结果中把搜索关键字突出显示。
原理是这样的:
- 将搜索结果中的关键字用标签标记出来
- 在页面中给标签添加css样式
# 当 match与高亮中的字段保持一致时
GET /hotel/_search
{
"query": {
"match": {
"name": "如家"
}
},
"highlight": {
"fields": {
"name":{
}
}
}
}
# 高亮显示查询,默认情况下,Es搜索字段必须与高亮字段一致
# 当match查找与高亮字段不一致时,我们的all中包含name字段
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name":{
"require_field_match": "false" # 默认为true
}
}
}
}
总结:搜索结果处理整体语法:
三、RestClient查询文档
1. 快速入门
测试:我们新建一个测试类用来实现今天所学内容,名为 HotelSearchTest:
private RestHighLevelClient client;
@Test
void testMatchAll() throws IOException {
//1. 准备request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
request.source().query(QueryBuilders.matchAllQuery());
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
SearchHits searchHits = response.getHits();
//4,1 查询总条数
long total = searchHits.getTotalHits().value;
//4.2 查询的结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit:hits){
// 4.3 得到source
String source = hit.getSourceAsString();
// 4.4 打印结果
System.out.println(source);
}
}
@BeforeEach
void setUp(){
this.client=new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.229.101:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
RestAPI中其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询、排序、分页、高亮等所有功能:
总结:
查询的基本步骤是:
-
创建SearchRequest对象
-
准备Request.source(),也就是DSL。
-
QueryBuilders来构建查询条件
-
传入Request.source() 的 query() 方法
-
-
发送请求,得到结果
-
解析结果(参考JSON结果,从外到内,逐层解析)
2. match查询
只需要在上面的MacthAll代码中将其中一行的代码替换为以下代码即可,其余代码不变:
//2. 准备DSL
request.source().query(QueryBuilders.matchQuery("name","如家"));
3. 精确查询
会发现我们有些步骤只需要复制粘贴就行无需改动,所以利用抽取的方法将相同部分定义成一个方法,每个查询方法中只需要调用即可:抽取快捷键:ctrl+alt+m
同样的道理,只有第二步代码不同:
# 精确查询——term
//2. 准备DSL
request.source().query(QueryBuilders.termQuery("city","上海"));
# 精确查询——range
//2. 准备DSL
request.source().query(QueryBuilders.rangeQuery("price").gte(100).lte(300));
4. 复合查询
/*复合查询*/
@Test
void testBool() throws IOException {
//1. 准备request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
//2.1 准备BoolQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//2.2 添加term
boolQuery.must(QueryBuilders.termQuery("city","北京"));
//2.3 添加range
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(200).lte(1000));
request.source().query(boolQuery);
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleResponse(response);
}
通过这几个查询,可以总结出,我们在构建查询条件时,只要记住一个类:QueryBuilders
5. 排序、分页、高亮
5.1 排序、分页
搜索结果的排序和分页是与query同级的参数,对应的API代码如下:
/*排序和分页*/
@Test
void testPageAndSort() throws IOException {
// 页码,每页大小
int page=2,size=5;
//1. 准备request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
//2.1 准备query
request.source().query(QueryBuilders.matchAllQuery());
//2.2 排序sort
request.source().sort("price", SortOrder.DESC);
//2.3 分页
request.source().from((page-1)*size).size(size);
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleResponse(response);
}
5.2 高亮
高亮API包括请求DSL构建和结果解析两部分。我们先看看请求的DSL构建:
@Test
void testHighLight() throws IOException {
//1. 准备request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
//2.1 准备query
request.source().query(QueryBuilders.matchQuery("all","如家"));
//2.2 高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析
handleResponse(response);
}
//在抽取的共同代码中也就是解析结果的代码中加入高亮解析代码
// 获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!CollectionUtils.isEmpty(highlightFields)) {
// 根据字段名获取高亮结果
HighlightField highlightField = highlightFields.get("name");
//获取高亮值
if (highlightField!=null) {
String name = highlightField.getFragments()[0].string();
// 覆盖高亮结果
doc.setName(name);
}
}
总结:
-
所有搜索DSL的构建,记住一个API:SearchRequest的source()方法。
-
高亮结果解析式参考json结果,逐层解析
本期关于查询文档以及搜索结果处理的操作先写到这哈!