ebatisElasticsearch ORM 框架
ebatis 是一個(gè)簡單方便上手的 Elasticsearch ORM 框架。
ebatis 基于 Java High Level REST Client 開發(fā),采用和 MyBatis 類似思想,只需要定義接口,便可訪問Elasticsearch,隔離業(yè)務(wù)對Elasticserach底層接口的直接訪問。如此以來,數(shù)據(jù)訪問的時(shí)候,不需要自己手動(dòng)去構(gòu)建DSL語句,同時(shí),當(dāng)升級Elastisearch版本的時(shí)候,只需要升級ebatis到相應(yīng)的版本即可,業(yè)務(wù)可以完全不用關(guān)心底層Elasticsearch驅(qū)動(dòng)接口的變動(dòng),平滑升級,并且在搜索時(shí),以O(shè)RM的形式與思想構(gòu)建我們的條件,極大的提升開發(fā)效率,下面我們用簡單的例子先快速入門ebatis。
創(chuàng)建索引
PUT /recent_order_index
{ "settings": { "number_of_replicas": 0, "number_of_shards": 1 }, "mappings": { "properties": { "cargoId": { "type": "long" }, "driverUserName": { "type": "keyword" }, "loadAddress": { "type": "text" }, "searchable": { "type": "boolean" }, "companyId": { "type": "long" } } } }增加測試數(shù)據(jù)
POST /recent_order_index/_bulk{"index":{}} {"cargoId": 1, "driverUserName":"張三", "loadAddress": "南京市玄武區(qū)", "searchable": true,"companyId": 666} {"index":{}} {"cargoId": 2, "driverUserName":"李四", "loadAddress": "南京市秦淮區(qū)", "searchable": false,"companyId": 667} {"index":{}} {"cargoId": 3, "driverUserName":"王五", "loadAddress": "南京市六合區(qū)", "searchable": true,"companyId": 668} {"index":{}} {"cargoId": 4, "driverUserName":"趙六", "loadAddress": "南京市建鄴區(qū)", "searchable": true,"companyId": 669} {"index":{}} {"cargoId": 5, "driverUserName":"錢七", "loadAddress": "南京市鼓樓區(qū)", "searchable": true,"companyId": 665}POM依賴(目前也支持6.5.1.1.RELEASE)
<dependency> <groupId>io.manbang</groupId> <artifactId>ebatis-core</artifactId> <version>7.5.1.3.RELEASE</version> </dependency>創(chuàng)建集群連接
@AutoService(ClusterRouterProvider.class) public class SampleClusterRouterProvider implements ClusterRouterProvider { public static final String SAMPLE_CLUSTER_NAME = "sampleCluster"; @Override public ClusterRouter getClusterRouter(String name) { if (SAMPLE_CLUSTER_NAME.equalsIgnoreCase(name)) { Cluster cluster = Cluster.simple("127.0.0.1", 9200, Credentials.basic("admin", "123456")); ClusterRouter clusterRouter = ClusterRouter.single(cluster); return clusterRouter; } else { return null; } } }定義POJO對象
@Data public class RecentOrder { private Long cargoId private String driverUserName; private String loadAddress; private Boolean searchable; private Integer companyId; } @Data public class RecentOrderCondition { private Boolean searchable; private String driverUserName; }定義Mapper接口
@Mapper(indices = "recent_order_index") public interface RecentOrderRepository { @Search List<RecentOrder> search(RecentOrderCondition condition); }測試接口
@Slf4j public class OrderRepositoryTest { @Test public void search() { // 組裝查詢條件 RecentOrderCondition condition = new RecentOrderCondition(); condition.setSearchable(Boolean.TRUE); condition.setDriverUserName("張三"); // 映射接口 RecentOrderRepository repository = MapperProxyFactory.getMapperProxy(RecentOrderRepository.class, SampleClusterRouterProvider.SAMPLE_CLUSTER_NAME); // 搜索貨源 List<RecentOrder> orders = repository.search(condition); // 斷言 Assert.assertEquals(3, orders.size()); // 打印輸出 orders.forEach(order -> log.info("{}", order)); } }ebatis版本使用xx.xx.xx.xx.RELEASE表示,前三位代表Elasticsearch適配集群的驅(qū)動(dòng)版本,后一位代表ebatis在此版本上的迭代。例如7.5.1.3.RELEASE表示ebatis在Elasticsearch 7.5.1版本上迭代的第三次版本。
其他Client的對比
目前,主流操作Elasticsearch 的四種驅(qū)動(dòng)方式
| 序號 | 驅(qū)動(dòng)方式 | 官方支持 | 備注 |
|---|---|---|---|
| 1 | Transport Client | 后續(xù)不再支持 | 不做比較 |
| 2 | Java Low Level REST Client | 支持 | 太low,不做比較 |
| 3 | Java High Level REST Client | 支持 | |
| 4 | Spring Data Elasticsearch | 第三方 |
下面,我們用滿幫車貨匹配一個(gè)默認(rèn)排序場景來比較一下,看看不同的驅(qū)動(dòng)方式,如何進(jìn)行復(fù)雜搜索操作。搜索DSL語句如下:
{
"query": {
"bool": {
"must": [
{
"bool": {
"must": [
{
"bool": {
"should": [
{
"terms": {
"startDistrictId": [
684214,
981362
],
"boost": 1.0
}
},
{
"terms": {
"startCityId": [
320705,
931125
],
"boost": 1.0
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
{
"bool": {
"should": [
{
"terms": {
"endDistrictId": [
95312,
931125
],
"boost": 1.0
}
},
{
"terms": {
"endCityId": [
589421,
953652
],
"boost": 1.0
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
{
"range": {
"updateTime": {
"from": 1608285822239,
"to": null,
"include_lower": true,
"include_upper": true,
"boost": 1.0
}
}
},
{
"terms": {
"cargoLabels": [
"水果",
"生鮮"
],
"boost": 1.0
}
}
],
"must_not": [
{
"terms": {
"cargoCategory": [
"A",
"B"
],
"boost": 1.0
}
},
{
"term": {
"featureSort": {
"value": "好貨",
"boost": 1.0
}
}
}
],
"should": [
{
"bool": {
"must_not": [
{
"terms": {
"cargoChannel": [
"長途貨源",
"一口價(jià)貨源"
],
"boost": 1.0
}
}
],
"should": [
{
"bool": {
"must": [
{
"term": {
"searchableSources": {
"value": "ALL",
"boost": 1.0
}
}
},
{
"bool": {
"must": [
{
"terms": {
"cargoChannel": [
"No.1",
"No.2",
"No.3"
],
"boost": 1.0
}
},
{
"term": {
"securityTran": {
"value": "平臺(tái)保證",
"boost": 1.0
}
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
"_source": {
"includes": [
"cargoId",
"startDistrictId",
"startCityId",
"endDistrictId",
"endCityId",
"updateTime",
"cargoLabels",
"cargoCategory",
"featureSort",
"cargoChannel",
"searchableSources",
"securityTran"
],
"excludes": []
},
"sort": [
{
"duplicate": {
"order": "asc"
}
},
{
"_script": {
"script": {
"source": "searchCargo-script",
"lang": "painless",
"params": {
"searchColdCargoTop": 0
}
},
"type": "string",
"order": "asc"
}
}
]
}
直接使用原生Java High Level REST Client接口方式:
final BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery(); final TermsQueryBuilder startCityId = QueryBuilders.termsQuery("startCityId", Lists.newArrayList(320705L, 931125L)); final TermsQueryBuilder startDistrictId = QueryBuilders.termsQuery("startDistrictId", Lists.newArrayList(684214L, 981362L)); final TermsQueryBuilder endCityId = QueryBuilders.termsQuery("endCityId", Lists.newArrayList(589421L, 953652L)); final TermsQueryBuilder endDistrictId = QueryBuilders.termsQuery("endDistrictId", Lists.newArrayList(95312L, 931125L)); final BoolQueryBuilder startBuilder = QueryBuilders.boolQuery(); startBuilder.should(startCityId).should(startDistrictId); final BoolQueryBuilder endBuilder = QueryBuilders.boolQuery(); endBuilder.should(endCityId).should(endDistrictId); final BoolQueryBuilder cityBuilder = QueryBuilders.boolQuery(); cityBuilder.must(startBuilder); cityBuilder.must(endBuilder); queryBuilder.must(cityBuilder); final RangeQueryBuilder rangeBuilder = QueryBuilders.rangeQuery("updateTime"); queryBuilder.must(rangeBuilder.from(1608285822239L)); final TermsQueryBuilder cargoLabelsBuilder = QueryBuilders.termsQuery("cargoLabels", Lists.newArrayList("水果", "生鮮")); queryBuilder.must(cargoLabelsBuilder); final TermsQueryBuilder cargoCategoryBuilder = QueryBuilders.termsQuery("cargoCategory", Lists.newArrayList("A", "B")); final TermQueryBuilder featureSortBuilder = QueryBuilders.termQuery("featureSort", "好貨"); queryBuilder.mustNot(cargoCategoryBuilder); queryBuilder.mustNot(featureSortBuilder); final BoolQueryBuilder cargoChannelBuilder = QueryBuilders.boolQuery(); queryBuilder.should(cargoChannelBuilder); final TermsQueryBuilder channelBuilder = QueryBuilders.termsQuery("cargoChannel", Lists.newArrayList("長途貨源", "一口價(jià)貨源")); cargoChannelBuilder.mustNot(channelBuilder); final BoolQueryBuilder searchableSourcesBuilder = QueryBuilders.boolQuery(); cargoChannelBuilder.should(searchableSourcesBuilder); final TermQueryBuilder sourceBuilder = QueryBuilders.termQuery("searchableSources", "ALL"); searchableSourcesBuilder.must(sourceBuilder); final BoolQueryBuilder securityTranBuilder = QueryBuilders.boolQuery(); searchableSourcesBuilder.must(securityTranBuilder); securityTranBuilder.must(QueryBuilders.termsQuery("cargoChannel", "No.1", "No.2", "No.3")); securityTranBuilder.must(QueryBuilders.termQuery("securityTran", "平臺(tái)保證")); SearchSourceBuilder searchSource = new SearchSourceBuilder(); searchSource.query(queryBuilder); searchSource.fetchSource(new String[]{"cargoId", "startDistrictId", "startCityId", "endDistrictId", "endCityId", "updateTime", "cargoLabels", "cargoCategory", "featureSort", "cargoChannel", "searchableSources", "securityTran"}, new String[0]); searchSource.sort("duplicate", SortOrder.ASC); ScriptSortBuilder sortBuilder = SortBuilders.scriptSort(new org.elasticsearch.script.Script(ScriptType.INLINE, "painless", "searchCargo-script", Collections.emptyMap(), Collections.singletonMap("searchColdCargoTop", 0)), ScriptSortBuilder.ScriptSortType.STRING).order(SortOrder.ASC); searchSource.sort(sortBuilder);使用Spring Data Elasticsearch方式:
@Repository interface CargoRepository extends ElasticsearchRepository<Cargo, String> { @Query("{\"match\": {\"name\": {\"query\": \"?0\.............."}}}") List<Cargo> findByCargoCondition(List<String> startCity, List<String> StartDistrictId /*,...*/); } final List<Cargo> cargos=cargoRepository.findByCargoCondition(Lists.newArrayList(320705L, 931125L),Lists.newArrayList(684214L, 981362L).........);因?yàn)锧Query需要將整個(gè)DSL語句填入,篇幅有限而且長度過長,所以省略展示 。
ebatis// 1. 創(chuàng)建搜貨條件POJO對象 @Data public class CargoCondition implements SortProvider { @Must private City city; @Must private Range<Long> updateTime; @Must(queryType = QueryType.TERMS) private List<String> cargoLabels; @Must private Boolean searchable; @Must private CargoLines cargoLines; @Should private CargoChannel cargoChannel; @MustNot(queryType = QueryType.TERMS) private List<String> cargoCategory; @MustNot private String featureSort; private static final Sort[] SORTS = new Sort[]{Sort.fieldAsc("duplicate"), Sort.scriptStringAsc(Script.inline("searchCargo-script", Collections.singletonMap("searchColdCargoTop", 0)))}; @Override public Sort[] getSorts() { return SORTS; } @Data public static class City { @Must private StartCity startCity; @Must private EndCity endCity; } @Data public static class StartCity { @Should(queryType = QueryType.TERMS) private List<Long> startDistrictId; @Should(queryType = QueryType.TERMS) private List<Long> startCityId; } @Data public static class EndCity { @Should(queryType = QueryType.TERMS) private List<Long> endDistrictId; @Should(queryType = QueryType.TERMS) private List<Long> endCityId; } @Data public static class CargoChannel { @MustNot(queryType = QueryType.TERMS) private List<String> cargoChannel; @Should private Security security; } @Data public static class Security { @Must private String searchableSources; @Must private SecurityChannel securityChannel; } @Data public static class SecurityChannel { @Must(queryType = QueryType.TERMS) private List<String> cargoChannel; @Must private String securityTran; } @Data public static class CargoLines { @Must(queryType = QueryType.TERMS) private List<String> cargoLines; @Must private CargoLabel cargoLabel; } @Data public static class CargoLabel { @Must(queryType = QueryType.TERMS) private List<String> cargoLines; @Must(queryType = QueryType.TERMS) private List<String> cargoLabels; } } // 2. 創(chuàng)建搜索接口 @Mapper(indices = "cargo") public interface CargoMapper { @Search List<Cargo> searchCargo(CargoCondition condition); } // 3. 拼裝搜獲條件 final CargoCondition cargo = new CargoCondition(); CargoCondition cargo = new CargoCondition(); final CargoCondition.City city = new CargoCondition.City(); cargo.setCity(city); final CargoCondition.StartCity startCity = new CargoCondition.StartCity(); city.setStartCity(startCity); startCity.setStartCityId(Lists.newArrayList(320705L,931125L)); startCity.setStartDistrictId(Lists.newArrayList(684214L,981362L)); final CargoCondition.EndCity endCity = new CargoCondition.EndCity(); city.setEndCity(endCity); endCity.setEndCityId(Lists.newArrayList(589421L,953652L)); endCity.setEndDistrictId(Lists.newArrayList(95312L,931125L)); cargo.setUpdateTime(Range.ge(System.currentTimeMillis())); cargo.setCargoLabels(Lists.newArrayList("水果","生鮮")); final CargoCondition.CargoChannel cargoChannel = new CargoCondition.CargoChannel(); cargo.setCargoChannel(cargoChannel); cargoChannel.setCargoChannel(Lists.newArrayList("長途貨源","一口價(jià)貨源")); final CargoCondition.Security security = new CargoCondition.Security(); cargoChannel.setSecurity(security); security.setSearchableSources("ALL"); final CargoCondition.SecurityChannel securityChannel = new CargoCondition.SecurityChannel(); security.setSecurityChannel(securityChannel); securityChannel.setCargoChannel(Lists.newArrayList("No.1","No.2","No.3")); securityChannel.setSecurityTran("平臺(tái)保證"); cargo.setCargoCategory(Lists.newArrayList("A","B")); cargo.setFeatureSort("好貨"); // 4. 執(zhí)行搜索 final List<Cargo> cargos = cargoMapper.searchCargo(condition);從以上對比可以看出ebatis與Spring Data Elasticsearch相對原生Client構(gòu)建搜索條件要方便很多,實(shí)際應(yīng)用中,在復(fù)雜搜索場景條件多變的情況下,如果使用Spring Data Elasticsearch構(gòu)建條件,在條件復(fù)雜場景下,需要自己構(gòu)建原始DSL語句,例如:@Query("{"match": {"name": {"query": "?0.............."}}}"),在復(fù)雜場景下條件的構(gòu)建會(huì)非常復(fù)雜且難以直觀的定位。
使用ebatis最大的優(yōu)點(diǎn)在于可以直觀的以O(shè)RM形式構(gòu)建我們的搜索條件,以面向?qū)ο蟮乃枷朊鎸ξ覀儚?fù)雜的搜索場景,無論是條件的構(gòu)建還是問題的定位,都相比Java High Level REST Clien和Spring Data Elasticsearch方便的多。
還有,搜索條件總是在變的,要調(diào)整的話,如果是原生接口和Spring,需要你不斷的調(diào)整語句,甚至修改接口,但是ebatis,只需要你正常的修改一個(gè)POJO對象的屬性,非常的高效。
ebatis進(jìn)階使用
執(zhí)行類圖
RequestExecutor:請求執(zhí)行器,負(fù)責(zé)整個(gè)Elasticsearch請求的執(zhí)行流程。
RequestFactory:求工廠接口,根據(jù)請求的方法定義和實(shí)參,創(chuàng)建ES請求。
Cluster:集群,負(fù)責(zé)Elasticsearch集群請求。
ResponseExtractor:響應(yīng)提取器,提取Elasticsearch響應(yīng),構(gòu)造返回體。
Interceptor:攔截器,負(fù)責(zé)ebatis調(diào)用過程的攔截。
Cluster
Cluster代表一個(gè)ES集群實(shí)例,ebatis內(nèi)建了兩個(gè)實(shí)現(xiàn):SimpleCluster,F(xiàn)ixWeightedCluster和SimpleFederalCluster。 SimpleCluster和FixedWeightedCluster的區(qū)別在于,后者是帶固定權(quán)值的值,在對集群做負(fù)載均衡的時(shí)候,可以通過權(quán)值來控制負(fù)載的比例。SimpleFederalCluster的特殊地方在于,在一批集群上做批量操作,同步一批集群,一般用于一批集群數(shù)據(jù)的增刪改,不適用于查。
ClusterRouter
ClusterRouter用于路由出一個(gè)可以訪問Cluster,內(nèi)部是通過負(fù)載均衡器ClusterLoadBalancer,來同一組集群中,選中一個(gè)集群的。根據(jù)不同的負(fù)載均衡器,ebatis內(nèi)建了多個(gè)對應(yīng)的路由器,默認(rèn)提供的有隨機(jī)負(fù)載均衡器,輪詢負(fù)載均衡器,單集群均衡器,權(quán)重負(fù)載均衡器,當(dāng)然也可以通過ebatis提供的接口,定制自己的策略均衡器。
接口定義支持的請求類型及響應(yīng)類型
Entity指具體的實(shí)體類型
| 請求類型 | 注解 | 接口聲明返回值 |
|---|---|---|
| GET //_search | @Search | Page |
| List | ||
| Entity[] | ||
| SearchResponse | ||
| Entity | ||
| Long | ||
| long | ||
| Boolean | ||
| boolean | ||
| GET //_msearch | @MultiSearch | List> |
| Page[] | ||
| List> | ||
| Entity[][] | ||
| List | ||
| List[] | ||
| MultiSearchResponse | ||
| List | ||
| Long[] | ||
| long[] | ||
| List | ||
| Boolean[] | ||
| boolean[] | ||
| PUT //_doc/<_id> | @Index | IndexResponse |
| RestStatus | ||
| boolean | ||
| Boolean | ||
| String | ||
| void | ||
| GET /_doc/<_id> | @Get | GetResponse |
| Entity | ||
| Optional | ||
| DELETE //_doc/<_id> | @Delete | RestStatus |
| DeleteResponse | ||
| boolean | ||
| Boolean | ||
| void | ||
| POST //_update/<_id> | @Update | UpdateResponse |
| GetResult | ||
| RestStatus | ||
| boolean | ||
| Boolean | ||
| Result | ||
| void | ||
| POST //_bulk | @Bulk | List |
| BulkResponse | ||
| BulkItemResponse[] | ||
| GET //_mget | @MultiGet | MultiGetResponse |
| MultiGetItemResponse[] | ||
| List | ||
| List | ||
| Entity[] | ||
| List> | ||
| Optional[] | ||
| POST //_update_by_query | @UpdateByQuery | BulkByScrollResponse |
| BulkByScrollTask.Status | ||
| POST //_delete_by_query | @DeleteByQuery | BulkByScrollResponse |
| BulkByScrollTask.Status | ||
| GET /_search/scroll | @SearchScroll | SearchResponse |
| ScrollResponse | ||
| DELETE /_search/scroll | @ClearScroll | ClearScrollResponse |
| boolean | ||
| Boolean | ||
| GET //_search | @Agg(暫時(shí)只支持桶聚合 terms查詢) | SearchResponse |
| Aggregations | ||
| List | ||
| Map |
以上是目前支持的搜索類型,其他的請求類型還需后續(xù)的迭代支持。
異步支持
Mapper搜索方法支持異步操作,只需要將Mapper接口返回結(jié)果定義為CompletableFuture>,這樣異步的調(diào)用不會(huì)阻塞并且立刻返回,業(yè)務(wù)方可以繼續(xù)處理自己的業(yè)務(wù)邏輯,在需要獲取結(jié)果時(shí),提取結(jié)果。
攔截器
ebatis中攔截器的加載通過SPI方式實(shí)現(xiàn),只需要提供的目標(biāo)類實(shí)現(xiàn)io.manbang.ebatis.core.interceptor.Interceptor接口,并且在/META-INF/services目錄下提供io.manbang.ebatis.core.interceptor.Interceptor文件,內(nèi)容為提供的目標(biāo)類的全限定名。也可以在目標(biāo)類上加上注解@AutoService(Interceptor.class),由auto-service替我們生成。攔截器的不同接口在請求的整個(gè)生命周期的不同階段調(diào)用,可以自定符合自己業(yè)務(wù)邏輯的攔截器。
@Slf4j
@AutoService(Interceptor.class)
public class TestInterceptor implements Interceptor {
@Override
public int getOrder() {
return 0;
}
@Override
public void handleException(Throwable throwable) {
log.error("Exception", throwable);
}
@Override
public void preRequest(Object[] args) {
...
//通過ContextHolder可以跨上下文獲取綁定的值
String userId = ContextHolder.getString("userId");
}
@Override
public <T extends ActionRequest> void postRequest(RequestInfo<T> requestInfo) {
...
}
@Override
public <T extends ActionRequest> void preResponse(PreResponseInfo<T> preResponseInfo) {
...
}
@Override
public <T extends ActionRequest, R extends ActionResponse> void postResponse(PostResponseInfo<T, R> postResponseInfo) {
...
}
}
與spring的集成,首先增加POM依賴
<dependency>
<groupId>io.manbang</groupId>
<artifactId>ebatis-spring</artifactId>
<version>7.5.1.3.RELEASE</version>
</dependency>
增加Config
@Configuration
@EnableEasyMapper(basePackages = "io.manbang.ebatis.sample.mapper")
public class EbatisConfig {
@Bean(destroyMethod = "close")
public ClusterRouter clusterRouter() {
Cluster cluster = Cluster.simple("127.0.0.1", 9200, Credentials.basic("admin", "123456"));
ClusterRouter clusterRouter = ClusterRouter.single(cluster);
return clusterRouter;
}
}
