ElasticsearchQueryUtil 查詢工具

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sitech.logengine.commons.page.PageInfo;
import com.sitech.logengine.commons.page.PageResult;
import com.sitech.logengine.commons.util.DateUtils;
import com.sitech.logengine.httpclient.util.HttpClientUtil;
import com.sitech.logengine.httpclient.util.HttpResult;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * es查詢工具類
 * 支持sql語句查詢、分頁查詢
 *  TODO 目前如果用到這個類必須要依賴於 httpclient-utils這個模塊
 *  並且spring必須配置掃描這個類HttpClientUtil所在的包
 *
 */
@Service
@Slf4j
public class ElasticsearchQueryUtil {
    private static final Logger log = LoggerFactory.getLogger(ElasticsearchUtil.class);
    String ES_SQL_QUERY_KEY = "query";
    String ES_SQL_COLUMNS_KEY = "columns";
    String ES_SQL_ROWS_KEY = "rows";

    private static final String FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
    private static final ThreadLocal<SimpleDateFormat> threadLocalDateFormat = new  ThreadLocal<SimpleDateFormat>();

    @Autowired
    private HttpClientUtil httpClientService;

    @Autowired
    private ElasticsearchConfig elasticsearchConfig;

    @Autowired
    private String esSqlQueryUrl;

    @Autowired
    private RestHighLevelClient client;

    public <T> List<T> queryByTemplateSql(String sql, Class<T> className) throws IOException {
        if (StringUtils.isBlank(sql)) {
            return new ArrayList<>();
        }
        log.debug("ElasticSearch final sql  {}", sql);

        Map<String, Object> query = new HashMap<>();
        query.put(ES_SQL_QUERY_KEY, sql);

        HttpResult httpResult = doPostJson(esSqlQueryUrl, JSONObject.toJSONString(query));
        JSONObject response = JSONObject.parseObject(httpResult.getContent());

        List<Columns> columns = JSONObject.parseArray(response.getString(ES_SQL_COLUMNS_KEY), Columns.class);
        List<List> rows = JSONObject.parseArray(response.getString(ES_SQL_ROWS_KEY), List.class);
        if(columns == null || rows == null){
            //如果查詢報錯或者沒有數據則返回 null
            return null;
        }
        ElasticSearchModel esModel = new ElasticSearchModel(columns, rows);

        return esModelToJavaObject(esModel, className);
    }
    /**
     * 目前的sql語句還不支持分頁查詢,只支持限定返回多少條記錄數,如果需要分頁查詢請使用pageQuery或者pageQuery開頭的方法
     * esSql查詢 sql語句中需要用到參數的地方, 請用#{param} 的形式修飾,#{}符號中間的單詞就是第二個參數中的某個鍵
     * @param sql sql語句
     * @param params 參數 一個key對應一個參數
     * @param className 目標類的class
     * @return  如果查詢報錯或者沒有數據則返回 null
     */
    public <T> List<T> queryBySql(String sql, Map<String, Object> params, Class<T> className) throws IOException {
        if (StringUtils.isBlank(sql)) {
            return new ArrayList<>();
        }
        log.debug("ElasticSearch ori sql  {} \n sql params {}", sql,JSONObject.toJSONString(params));
        // 轉換sql參數
        if (params != null && params.size() != 0) {
            for (Map.Entry<String,Object> entry : params.entrySet()){
                sql = sql.replace("#{" + entry.getKey() + "}", entry.getValue().toString());
            }
        }
        log.debug("ElasticSearch final sql  {}", sql);

        Map<String, Object> query = new HashMap<>();
        query.put(ES_SQL_QUERY_KEY, sql);

        HttpResult httpResult = doPostJson(esSqlQueryUrl, JSONObject.toJSONString(query));
        JSONObject response = JSONObject.parseObject(httpResult.getContent());
 
        List<Columns> columns = JSONObject.parseArray(response.getString(ES_SQL_COLUMNS_KEY), Columns.class);
        List<List> rows = JSONObject.parseArray(response.getString(ES_SQL_ROWS_KEY), List.class);
        if(columns == null || rows == null){
            //如果查詢報錯或者沒有數據則返回 null
            return null;
        }
        ElasticSearchModel esModel = new ElasticSearchModel(columns, rows);
 
        return esModelToJavaObject(esModel, className);
    }

    /**
     * 使用sql語句查詢總數
     *  esSql查詢 sql語句中需要用到參數的地方, 請用#{param} 的形式修飾,#{}符號中間的單詞就是第二個參數中的某個鍵
     * @param sql
     * @param params
     * @return
     * @throws IOException
     */
    public Long queryCountBySql(String sql, Map<String, Object> params) throws IOException {
        List<Long> countLong = this.queryBySql(sql,params,Long.class);
        if(countLong == null || countLong.isEmpty()){
            return 0L;
        }
        return countLong.get(0);
    }

    /**
     * 目前es的sql語句還不支持分頁查詢,如果需要分頁查詢,請用此方法
     * @param pageInfo
     * @param queryBuilder 查詢條件builder需要構造好
     * @param searchRequest 查詢索引需要構造好
     * @param clazz
     * @param <T>
     * @return
     * @throws IOException
     */
    public <T> PageResult<T> pageQuery(PageInfo pageInfo, QueryBuilder queryBuilder, SearchRequest searchRequest, Class<T> clazz) throws IOException {

        JSONObject resJSON = new JSONObject();
        JSONArray jsonArr = new JSONArray();
        //最大不能超過 10000條,可以通過設置索引的"max_result_window": "10000000"來解決,不過效率不知如何
        int currentPage = pageInfo.getCurrentPage()-1;
        int pageSize = pageInfo.getPageSize();
        int fromSize = currentPage*pageSize;
        // 創建查詢函數構造對象
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        sourceBuilder.query(queryBuilder); // 把父查詢對象放入函數構造對象中
        sourceBuilder.from(fromSize); // 參數範圍起
        sourceBuilder.size(pageSize); // 參數範圍始
        sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));// 設置超時時間
        sourceBuilder.trackTotalHits(true); // 取消默認最大查詢數量上限(默認10000)

        searchRequest.source(sourceBuilder);// 把查詢函數構造對象注入查詢請求中
        // 創建響應對象
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits searchHits =  searchResponse.getHits();//獲取響應中的列表數據
        long total = searchHits.getTotalHits().value;//獲取響應中的列表數據總數
        pageInfo.calculate((int)total);
        List<T> contents = new ArrayList<>();

        for(SearchHit hit:searchHits.getHits()){// 遍歷構造返回JSON,以下不再多說
            JSONObject dataJSON = new JSONObject();
            String tempRes = hit.getSourceAsString();
            dataJSON = JSONObject.parseObject(tempRes);
            jsonArr.add(dataJSON);
            contents.add(JSON.parseObject(tempRes,clazz));
        }

//        resJSON.put("resArr", jsonArr);
        PageResult pageResult = new PageResult();
        pageResult.setPageInfo(pageInfo);
        pageResult.setContents(contents);

        return pageResult;

    }

    /**
     * 目前es的sql語句還不支持分頁查詢,如果需要分頁查詢,請用此方法
     * @param pageInfo
     * @param queryBuilder 查詢條件builder需要構造好
     * @param clazz
     * @param indices
     * @param <T>
     * @return
     * @throws IOException
     */
    public <T> PageResult<T> pageQuery(PageInfo pageInfo, QueryBuilder queryBuilder, Class<T> clazz, String... indices) throws IOException{
        SearchRequest searchRequest = new SearchRequest(indices);
        return this.pageQuery(pageInfo,queryBuilder,searchRequest,clazz);
    }




    /**
     * 向指定 URL 發送POST方法的請求
     *
     * @param url
     *   發送請求的 URL
     * @param param
     *            請求參數,請求參數應該是 name1=value1&name2=value2 的形式。
     * @return 所代表遠程資源的響應結果
     * @throws Exception
     */
    public HttpResult doPostJson(String url, String param) throws IOException {
       return httpClientService.doPostJson(url,param);
    }


    /**
     * model轉換實體類
     * @param esModel
     * @param className
     * @param <T>
     * @return
     */
    private <T> List<T> esModelToJavaObject(ElasticSearchModel esModel, Class<T> className) {
        Field[] fields = className.getDeclaredFields();
        List<T> result = new ArrayList<>(esModel.getRows().size());
        esModel.getRows().forEach(row -> {
            try {
                // 判斷是不是基本數據類型
                if (!isBasicType(className)) {
                    // 新增實體
                    T t = className.newInstance();
                    for (int i = 0; i < fields.length; i++) {
                        // 設置該屬性可以修改
                        fields[i].setAccessible(true);
                        for (int j = 0; j < esModel.getColumns().size(); j++) {
                            // 判斷屬性名和es返回的列名一致
                            if (fields[i].getName().equals(esModel.getColumns().get(j).getName())) {
                                String type = esModel.getColumns().get(j).getType();
                                Object val = row.get(j);
//                                if(val == null || "null".equals(val)){
//                                    //值爲null字符串的則不設置
//                                    continue;
//                                }
                                // 時間轉換Time  // 這裏的弊端。時間轉換沒有辦法識別。我只能通過這個列名有沒有Time這個單詞來判斷是不是時間
//                                if (esModel.getColumns().get(j).getName().toLowerCase().contains("time"))
//                                    type = "time";
                                Object o = castValue(type, val);
//                                String esColumnType = esModel.getColumns().get(j).getName().toLowerCase();
                                if (type.contains("datetime") || type.contains("date")){
                                    String filedTypeName = fields[i].getType().getTypeName();
                                    if(filedTypeName.contains("String")){
                                        Date date = (Date)o;
                                        fields[i].set(t,DateUtils.dateToStringDefault(date));
                                    }else{
                                        fields[i].set(t, o);
                                    }
                                }else{
                                    fields[i].set(t, o);

                                }
                            }
                        }
                    }
                    result.add(t);
                }else {
                    String type = esModel.getColumns().get(0).type;
                    Object val = row.get(0);
                    // 基本數據類型
                    Object o = castValue(type, val);
 
                    result.add((T) o);
                }
            } catch (InstantiationException | IllegalAccessException e) {
                log.warn("esModel to java object fail ",e);
            }
        });
 
        return result;
    }
 
    /**
     * Es返回的類型轉換成數據類型包裝類
     * @param type es結果集中返回的類型
     * @param val 需要轉換的值
     */
    private Object castValue(String type, Object val) {
        if (StringUtils.isBlank(type) || val == null || StringUtils.isBlank(val.toString())) {
            return null;
        }
        if ("text".equals(type) || "keyword".equals(type)) {
            try {
                return val.toString();
            // 防止時間轉換失敗
            }catch (IllegalArgumentException e) {
                log.warn("type {} cast fail  value {}  ",type,val,e);
            }
        }else if ("long".equals(type)) {
            return Long.parseLong(val + "");
        } else if ("double".equals(type)) {
            return Double.parseDouble(val + "");
        } else if ("float".equals(type)) {
            return Float.parseFloat(val + "");
        }else if ("datetime".equals(type)) {
            // 防止時間轉換錯誤
            try {
                if(val == null || val.toString().isEmpty()){
                    return null;
                }
                return covertDateStrToDate(val.toString()); // 時間戳轉換
            }catch (ParseException e) {
                log.warn("type {} cast fail  value {}  ",type,val,e);
            }
        }
        return null;
    }

    public static Date covertDateStrToDate(String dateStr) throws ParseException {
        SimpleDateFormat sdf = threadLocalDateFormat.get();
        if (sdf == null){
            sdf = new SimpleDateFormat(FORMAT);
        }
        return sdf.parse(dateStr);
    }
 
    /**
     * 判斷是不是基本數據類型包裝類
     * @params
     */
    private static boolean isBasicType(Class className) {
        if (className == null) {
            return false;
        } else if(className.equals(String.class)) {
            return true;
        } else if(className.equals(Integer.class)) {
            return true;
        } else if(className.equals(Long.class)) {
            return true;
        } else if(className.equals(Short.class)) {
            return true;
        } else if(className.equals(Double.class)) {
            return true;
        } else if(className.equals(Float.class)) {
            return true;
        } else if(className.equals(Character.class)) {
            return true;
        } else if(className.equals(Byte.class)) {
            return true;
        }
 
        return false;
    }
 
 

 
    /**
     * ES返回數據Model類
     *
     */
    @Data
    static class ElasticSearchModel {
        private List<Columns> columns;
        private List<List> rows;
 
        public ElasticSearchModel(List<Columns> columns, List<List> rows) {
            this.columns = columns;
            this.rows = rows;
        }

        public List<Columns> getColumns() {
            return columns;
        }

        public void setColumns(List<Columns> columns) {
            this.columns = columns;
        }

        public List<List> getRows() {
            return rows;
        }

        public void setRows(List<List> rows) {
            this.rows = rows;
        }
    }
 
    /**
     * EsQuery中使用sql查詢。他會返回的列
     * @author [email protected]
     * @time 23:13
     */
    @Data
    static class Columns{
        /**
         * 列名
         */
        private String name;
 
        /**
         * 列的類型
         */
        private String type;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }
    }
}
發佈了58 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章