背景: 接上篇Easy-Es核心功能深度介绍,本篇带大家深入源码和架构,一起探索Easy-Es(简称EE)的高阶语法是如何被设计和实现的.
这里所谓的"高阶语法"并不一定就真的高阶,仅作为区别于MySQL语法,Es独有的一些语法,比如得分排序,聚合,分词查询,权重,高亮及Geo地理位置查询等.
对于MySQL中已经有的方法,在Mybatis(简称MP)中也基本都已经有设计,我可以参考其API风格,但这部分"高阶语法"并无API可参考,到底如何设计才能做到简洁好用呢?
要解决这个问题,除了要了解用户的使用习惯,还要对整个EE的API设计和RestHighLevelClient十分熟悉,才能在设计API的时候做到游刃有余.
比如地理位置查询,我可以把geo相关功能新增方法并置入BaseEsMapperService中,但这样做的代价就是在进行地理位置查询时无法同时使用别的常规查询条件作为过滤项了,比如我想实现一个查询附近3km所有单身美女...时就无法调用已经实现的查询功能了. 故而将geo相关的所有查询都置入LambdaEsQueryWrapper中,这样就可以鱼与熊掌兼得了.
@Test public void testQuery(){
// 当前位置(圆心)
GeoPoint centerPoint = new GeoPoint(41.187328D, 115.498353D);
LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle,"美女")
.eq(Document::getContent,"单身")
.geoDistance(Document::getLocation,3.0,centerPoint);
}
整个EE框架中,个人认为写得最好的也就是Geo相关的API,强烈建议感兴趣的同学去阅读,之所以这么说,主要是因为和RestHighLevelClient还原度极高,达到了100%,不仅简化了其操作,功能上还没有任何折扣,对4种经纬度的表达方式和各种用户习惯都支持,而且用了极少的代码就实现了,这得益于我事先阅读了RestHighLevelClient的源码,利用了源码中提供的GeoPoint类和Jdk8提供的接口default方法,否则对如此多参数的方法重载可以排列组合出N多种方法,实现代价不可估量.
default Children geoDistance(R column, Double distance, GeoPoint centralGeoPoint) {
return geoDistance(true, column, distance, DistanceUnit.KILOMETERS, centralGeoPoint, DEFAULT_BOOST);
}
default Children geoDistance(R column, Double distance, DistanceUnit distanceUnit, GeoPoint centralGeoPoint) {
return geoDistance(true, column, distance, distanceUnit, centralGeoPoint, DEFAULT_BOOST);
}
default Children geoDistance(R column, Double distance, DistanceUnit distanceUnit, GeoPoint centralGeoPoint, Float boost) {
return geoDistance(true, column, distance, distanceUnit, centralGeoPoint, boost);
}
default Children geoDistance(R column, Double distance, DistanceUnit distanceUnit, String centralGeoPoint) {
GeoPoint geoPoint = new GeoPoint(centralGeoPoint);
return geoDistance(true, column, distance, distanceUnit, geoPoint, DEFAULT_BOOST);
}
default Children geoDistance(boolean condition, R column, Double distance, DistanceUnit distanceUnit, String centralGeoPoint) {
GeoPoint geoPoint = new GeoPoint(centralGeoPoint);
return geoDistance(condition, column, distance, distanceUnit, geoPoint, DEFAULT_BOOST);
}
default Children geoDistance(R column, Double distance, DistanceUnit distanceUnit, String centralGeoPoint, Float boost) {
GeoPoint geoPoint = new GeoPoint(centralGeoPoint);
return geoDistance(true, column, distance, distanceUnit, geoPoint, boost);
}
default Children geoDistance(boolean condition, R column, Double distance, DistanceUnit distanceUnit, String centralGeoPoint, Float boost) {
GeoPoint geoPoint = new GeoPoint(centralGeoPoint);
return geoDistance(condition, column, distance, distanceUnit, geoPoint, boost);
}
/**
* 距离范围查询 以给定圆心和半径范围查询 距离类型为双精度
*
* @param condition 条件
* @param column 列
* @param distance 距离
* @param distanceUnit 距离单位
* @param centralGeoPoint 中心点 GeoPoint/字符串/哈希/wkt均支持
* @param boost 权重值
* @return 泛型
*/
Children geoDistance(boolean condition, R column, Double distance, DistanceUnit distanceUnit, GeoPoint centralGeoPoint, Float boost);
以上便是geoDistance的相关API设计,除了最下面这一个方法需要写实现类,其余均不需要,但可以兼容用户的各种习惯,比如有些用户喜欢距离指定为字符串"3.0km",有些用户喜欢指定成3.0,然后再指定距离单位,有些用户喜欢用字符串来表示经纬度,有些喜欢用数组,有些喜欢用Double,有些喜欢用WKT坐标,有些喜欢用GeoHash...总之各种用户习惯都被兼容进来了,想怎么用怎么用,给你绝对自由! 但对开发者而言,满足这么多用户习惯,仅需实现其中一种,其它皆可几乎零代码复用. Geo其它几类API设计也采取类似的策略,有着异曲同工之妙,感兴趣的读者可以自行深入阅读.
权重API的设计主要考虑到权重值通常都是绑定具体的查询条件上,最终指向都是具体的字段,故而将权重值设置在具体的字段上,方便用户使用,比如我想让文档的内容得分权重5,作者权重得分2,只需要在原来查询条件wrapper中加权重值即可,用起来可以说非常方便了,如果我将此权重值设计到BaseEsMapperImpl的方法入参中,灵活性就大大降低了,还会增加用户使用时需要指定字段的代码量,同样如果我将此设计到wrapper的方法中,就可能出现同一个字段多次指定,最终不知道使用哪个的情况,所以悉以为将权重值紧跟在查询条件作用的字段上最为妥当,如果您有更好的方案也可以给留言告诉我去改进.
@Test
public void testWeight() throws IOException {
// 测试权重
LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Document::getContent,"过硬", 5.0f;);
wrapper.eq(Document::getCreator, "老汉", 2.0f);
SearchResponse response = documentMapper.search(wrapper);
System.out.println(response);
}
高亮的话,通常是不需要区分字段的,只需要可以由用户自定义指定高亮字段显示的tag即可,所以高亮的api设计支持单字段,多字段高亮,当用户不指定tag时,使用 作为默认的tag.
@Test
public void testHighlight() throws IOException {
LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>();
String keyword = "过硬";
wrapper.match(Document::getContent, keyword);
wrapper.highLight(Document::getContent);
SearchResponse response = documentMapper.search(wrapper);
System.out.println(response);
}
EE好用不仅是因为MP好用,MP中没有的API,在EE中也是花大量时间精心设计的,如果您也觉得EE的API设计比较优雅的话,不妨帮作者点个赞,如果您有更好的意见或建议,也欢迎在评论区留言告诉我们,让我们一起把EE建设得更好!