混合查询
# 何为混合查询?
简单理解,就是一半采用EE的语法,一半采用RestHighLevelClient的语法,类似"油电混动",相信你会爱上这种"油电混动"模式,因为它结合了两种模式的优点!
# 为什么要有混合查询?
因为EE目前还没有做到对RestHighLevelClient的功能100%覆盖,目前经过一年多的发展,我们覆盖了RestHighLevelClient约90%左右的API,和99.9%的核心高频使用功能,如此就不可避免的会出现个别场景下,EE不能满足某个特殊需求,此时对EE框架进行二次开发或直接将该需求提给EE作者,在时间上都无法满足开发者需求,有些需求可能产品经理要的比较紧,那么此时,您就可以通过混合查询来解决窘境. 其次,在API设计上其实支持的功能越多越复杂,用户使用起来就越难,这两者是鱼与熊掌,很难兼得,RestHighLevelClient并非有意把API设计的如此难用,他们也非常不容易,复杂的地方光是参数就有上千种排列组合方式,所以我们也无法通过方法重载来简化API使用,即便我们支持了,届时API也会极度臃肿,找一个您想调用的方法更是难上加难,一样会被您无情吐槽,因此我们只简化了99.9%的高频核心API, 其它的低频API,需要您通过混合查询来实现.
# 使用混合查询与直接使用原生查询有什么优势?
混动车相比纯油车最大的优势就是省油,其实混合查询也是如此,能比直接使用原生查询省下不少代码,并且比直接使用原生查询更简单,兼具原生语法的灵活特性和EE的低代码优势,这点其实用过SpringData-Es的用户也清楚,它是混合查询的鼻祖,只不过青出于蓝而胜于蓝,我们后发制人,灵活度和"省油"程度更胜一筹,不信的官人请继续"往下看"!
# 如何使用混合查询?
在我没提供此篇文档时,尽管我提供了混合查询的API和简单介绍,但很多人还不知道有此功能,更不知道该如何使用,所以这里我以一个具体的案例,给大家演示如何使用混合查询,供大家参考,主公们别担心篇幅多,其实非常非常简单,只是我教程写的细.
背景
用户"向阳"微信向我反馈,说目前EE尚不支持查询按照距给定点的位置由近及远排序. 实际开发中,此场景可以被应用到打车时"乘客下单,要求优先派单给周围3公里内离我最近的司机",然后该乘客是个美女,担心自身安全问题,又多加了几个要求,比如司机必须是女性,驾龄大于3年,商务型车子等...
以上面打车的场景为例,我们来看下用EE怎么查询?上面查询可以分为两部分
- EE支持的常规查询:如周围3公里内,司机性别为女,查询驾龄>=3年...
- EE不支持的非常规查询:按照复杂的排序规则排序(写此篇文档时是不支持的,现已支持,但这不重要,本篇仅以此来演示混合查询的使用)
对于支持的部分,我们可以直接调用EE,由EE先构建一个SearchSourceBuilder出来
// 假设该乘客所在位置经纬度为 31.256224D, 121.462311D
LambdaEsQueryWrapper<Driver> wrapper = new LambdaEsQueryWrapper<>();
wrapper.geoDistance(Driver::getLocation, 3.0, DistanceUnit.KILOMETERS, new GeoPoint(31.256224D, 121.462311D))
.eq(Driver::getGender,"女")
.ge(Driver::getDriverAge,3)
.eq(Driver::getCarModel,"商务车");
SearchSourceBuilder searchSourceBuilder = driverMapper.getSearchSourceBuilder(wrapper);
2
3
4
5
6
7
对于不支持的语句,可以继续用RestHighLevelClient的语法进行封装,封装好了,直接调用EE提供的混合查询接口,就可以完成整个查询.
// 此处的searchSourceBuilder由上面EE构建而来,我们继续对其追加排序参数
searchSourceBuilder.sort(
new GeoDistanceSortBuilder("location", 31.256224D, 121.462311D)
.order(SortOrder.DESC)
.unit(DistanceUnit.KILOMETERS)
.geoDistance(GeoDistance.ARC)
);
wrapper.setSearchSourceBuilder(searchSourceBuilder);
List<Driver> drivers = driverMapper.selectList(wrapper);
2
3
4
5
6
7
8
9
如此您便可以既享受到了EE帮您生成好的基本查询,又可完成EE暂未支持的功能,只需要不太多的代码(相比直接RestHighLevelClient,仍能节省大量代码)就可以达成您的目标,和当下纯电动汽车尚未完全发展成熟下的一种折中方案---油电混动有着异曲同工之妙.
当然,如果您不习惯使用这种模式,您仍可以直接使用原生查询,所以您大可以无忧无虑的使用EE,我们已经为您想好了各种兜底的方案和退路,无忧售后!如果您也认可这种模式,不妨给我们点个star吧,为了让EE的用户爽,作者那糟老头子可谓是煞费苦心!
提示
上面的例子没看懂?没关系,其实上面的例子更多地是为了展示何为混合查询?我们在2.0.0-beta2版本后祭出了真正的大招,引入了全新的混合查询方案,使用更加简单,官人可以继续"往下看",下面有个案例"0"就是您想最渴望的!
# 混合查询的几种正确使用姿势
下面的案例虽然多,但用起来却非常简单,只是因为我教程写的细,看官们不要怕
/**
* 正确使用姿势0(最实用,最简单,最推荐的使用姿势):EE满足的语法,直接用,不满足的可以构造原生QueryBuilder,然后通过wrapper.mix传入QueryBuilder
* @since 2.0.0-beta2 2.0.0-beta2才正式引入此方案,此方案为混合查询的最优解决方案,由于QueryBuilder涵盖了ES中全部的查询,所以通过此方案
* 理论上可以处理任何复杂查询,并且可以和EE提供的四大嵌套类型无缝衔接,彻底简化查询,解放生产力!
*/
@Test
public void testMix0(){
// 查询标题为老汉,内容匹配 推*,且最小匹配度不低于80%的数据
// 当前我们提供的开箱即用match并不支持设置最小匹配度,此时就可以自己去构造一个matchQueryBuilder来实现
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
QueryBuilder queryBuilder = QueryBuilders.matchQuery("content", "推*").minimumShouldMatch("80%");
wrapper.eq(Document::getTitle,"老汉").mix(queryBuilder);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 混合查询正确使用姿势1: EE提供的功能不支持某些过细粒度的功能,所有查询条件通过原生语法构造,仅利用EE提供的数据解析功能
*/
@Test
public void testMix1() {
// RestHighLevelClient原生语法
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("content", "推*").minimumShouldMatch("80%"));
// 仅利用EE查询并解析数据功能
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.setSearchSourceBuilder(searchSourceBuilder);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 混合查询正确使用姿势2: 其它都能支持,仅排序器不支持,这种情况可以只按ES原生语法构造所需排序器SortBuilder,其它用EE完成
*/
@Test
public void testMix2() {
// EE满足的语法
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, "老汉")
.match(Document::getContent, "推*");
// RestHighLevelClient原生语法
Script script = new Script("doc['star_num'].value");
ScriptSortBuilder scriptSortBuilder = SortBuilders.scriptSort(script,ScriptSortBuilder.ScriptSortType.NUMBER).order(SortOrder.DESC);
// 利用EE查询并解析数据
wrapper.sort(scriptSortBuilder);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 混合查询正确使用姿势3: 其它功能都能支持,但需要向SearchSourceBuilder中追加非query参数
*/
@Test
public void testMix3() {
// EE满足的语法
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, "老汉")
.match(Document::getContent, "推*");
SearchSourceBuilder searchSourceBuilder = documentMapper.getSearchSourceBuilder(wrapper);
// 追加或者设置一些SearchSourceBuilder支持但EE暂不支持的参数 不建议追加query参数,因为如果追加query参数会直接覆盖上面EE已经帮你生成好的query,以最后set的query为准
searchSourceBuilder.timeout(TimeValue.timeValueSeconds(3L));
wrapper.setSearchSourceBuilder(searchSourceBuilder);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 查询条件中可以利用大多数基本查询,但EE提供的聚合功能不能满足需求的情况下,需要自定义聚合器
*/
@Test
public void textMix4() {
// EE满足的语法
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, "老汉")
.match(Document::getContent, "推*");
SearchSourceBuilder searchSourceBuilder = documentMapper.getSearchSourceBuilder(wrapper);
// RestHighLevelClient原生语法
AggregationBuilder aggregation = AggregationBuilders.terms("titleAgg")
.field("title");
searchSourceBuilder.aggregation(aggregation);
wrapper.setSearchSourceBuilder(searchSourceBuilder);
SearchResponse searchResponse = documentMapper.search(wrapper);
// TODO 聚合后的信息是动态的,框架无法解析,需要用户根据聚合器类型自行从桶中解析,参考RestHighLevelClient官方Aggregation解析文档
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 混合查询错误的使用姿势
讲个故事,从前有个懒汉,懒得抠脚,有一天有个勤奋老汉,看他居无定所,答应给他建造一套豪宅,让他住的爽一点,房子老汉都给他修好了,告诉他门在哪里,可这懒汉就是不听, 非要从天窗往进去爬,认为这样很酷,结果一不小心摔了下去,摔断了腿,去法院告老汉说他修的房子有问题,去大街上拉横幅骂老汉是个糟老头子...
EE支持的混合查询使用方案我已经全部列在上面正确的使用姿势中了,已经能涵盖任何一种使用场景了, 请勿凭空捏造凭空想象然后按自己认为没问题的方式来写,文档和源码都不带看,还非要自己YY使用方案,最后又来答疑群问我为什么不支持,还说代码里有坑,令码保国同志晚节不保...
下面就演示两种典型的不支持场景:
/**
* 不支持的混合查询1: 追加覆盖问题
*/
@Test
public void textNotSupportMix() {
// EE满足的语法
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, "老汉")
.match(Document::getContent, "推*");
SearchSourceBuilder searchSourceBuilder = documentMapper.getSearchSourceBuilder(wrapper);
// 用户又想在上面的基础上,再追加一些个性化的查询参数进去 但实际上此时执行查询时,查询条件仅仅是最后设置的title=隔壁老王,前面的老汉推*会被覆盖
searchSourceBuilder.query(QueryBuilders.matchQuery("title", "隔壁老王"));
wrapper.setSearchSourceBuilder(searchSourceBuilder);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
// 思考: 为什么会被覆盖? 因为目前技术上做不到,查询树已经建立好了,es底层并没有提供向树的指定层级上继续追加查询条件的API
}
/**
* 不支持的混合查询2: 脱裤子放P 自欺欺人系列
*/
@Test
public void testNotSupportMix2() {
// EE满足的语法
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, "老汉")
.match(Document::getContent, "推*");
// SearchSourceBuilder的构造是自己new出来的,不是通过mapper.getSearchSourceBuilder(wrapper)构造 相当于脱裤子放P,那么上面的查询条件老汉推*自然不会生效
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.minScore(10.5f);
wrapper.setSearchSourceBuilder(searchSourceBuilder);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 结语
因为ES官方提供的RestHighLevel支持的功能实在是过于繁多,尽管我目前仍在马不停蹄的集成各种新的功能,以及修复用户反馈的问题,优化既有代码性能,但仍不可避免地会出现有些许功能不能满足您当前需求,请各位主公们见谅,EE才诞生一年,不可能做到十全十美,请给我们一些时间,我们已经在没有任何回报的前提下狂奔了,这一年来取得的巨大进步大家有目共睹,这些所谓的不足,都会被解决的,就像新能源车在未来终会逐步取代燃油车,那些所谓的问题,假以时日都不是问题,乌拉!