Springboot+ElasticSearch 项目实战(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Ocean111best/article/details/86095392

这篇接着上一篇(https://blog.csdn.net/Ocean111best/article/details/86092862),本来准备上一篇写完,但是发现还是太多,故再写一篇。

发现刚开始写博客时,千言万语,最后确不知道怎么说,只有直接简单粗暴地贴代码了,希望以后坚持多写,慢慢地有所提高。

四、Springboot 操作 ElasticSearch

一、首先配置整个项目的 application.properties(当时把 properties 文件和 yml 文件混着配置,被一个大神取笑了):

之所以要配置 Mysql ,是因为精确搜索中,需要选择省份对应的市,再选择市对应的区,这个数据没有放到 ES 中,因为 ES6.5 中每个索引只能有一个 type,如果放到 ES 中需要再弄一个索引,不想这么麻烦,于是把省市区的数据放到了 Mysql 数据库中(貌似这样好像更麻烦,以后会再改)

二、ES 的配置

配置 ES ,使 Springboot 可以操作它。这个地方注意,ES6.5 的配置和 ES5.6 的配置不一样,暂时只知道 ES5.6 的配置,还不太知道 ES6.5 的配置,如下图:

/**
 * es配置文件
 * es注入主要依靠一个 TransportClient
 */
@Configuration
public class myconfig {
    @Value("${elasticsearch.host}")
    private String esHost;

    @Value("${elasticsearch.port}")
    private int esPort;

    @Value("${elasticsearch.cluster.name}")
    private String esName;

    @Bean
    public TransportClient esClient() throws UnknownHostException {
        Settings settings = Settings.builder()
                .put("cluster.name", this.esName)
                .put("client.transport.sniff", true)
                .build();

        InetSocketTransportAddress master = new InetSocketTransportAddress(
                InetAddress.getByName(esHost), esPort
        );

        TransportClient client = new PreBuiltTransportClient(settings)
                .addTransportAddress(master);

        return client;
    }
}

三、通过搜索框搜索

在搜索框中输入城市、年份、指标的任意组合和任意顺序,都要搜索出结果,这个是模糊查询,且是并查询,如输入"北京市 2015 人口",则得到北京市2015年与人口相关的指标(如年平均人口、总人口等)的所有数据;

controller层中,如下所示,主要是调用 service 层,业务逻辑主要在 service 中

/**
* 通过 ES 查询(通过搜索框查询)
*/
@ApiOperation(value = "通过搜索框查询详细信息",notes = "通过搜索框查询详细信息(keywords不为空,其他为空)")
@RequestMapping(value = "yearbook/search",method = RequestMethod.POST,produces = { MediaType.APPLICATION_JSON_VALUE },consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity query(@RequestBody YearbookSearch yearbookSearch){
        List<YearbookResult> result = yearbookService.query(yearbookSearch);

        return new ResponseEntity(result, HttpStatus.OK);
}

在 service 层中,由于在网上看到的操作很多都是或操作,但是在搜索框中搜索的是并操作。之后查找了很久的资料,发现可以设置如下参数即可达到搜索的是并操作的目标:

.operator(Operator.AND).type("cross_fields")

则在 service 里面可以进行如下操作:

   /**
     * 通过 ES 查询(通过搜索框查询)
     */
    @Override
    public List<YearbookResult> query(YearbookSearch yearbookSearch){
        SearchResponse response = searchResponse(yearbookSearch);
        List<YearbookResult> result=new ArrayList<YearbookResult>();

        //将搜索到的数据放入List
        for(SearchHit hit:response.getHits()){
            YearbookResult pointMap = new YearbookResult();
            pointMap.setCitytr(String.valueOf(hit.getSource().get(YearbookIndexKey.CITYTR)));
            pointMap.setDistinct(String.valueOf(hit.getSource().get(YearbookIndexKey.DISTINCT)));
            pointMap.setCountytr(String.valueOf(hit.getSource().get(YearbookIndexKey.COUNTYTR)));
            pointMap.setProvincetr(String.valueOf(hit.getSource().get(YearbookIndexKey.PROVINCETR)));
            pointMap.setStatisticNum(String.valueOf(hit.getSource().get(YearbookIndexKey.STATISTIC_NUM)));
            pointMap.setUnit(String.valueOf(hit.getSource().get(YearbookIndexKey.UNIT)));
            pointMap.setIndexName(String.valueOf(hit.getSource().get(YearbookIndexKey.INDEX_NAME)));
            pointMap.setYear(String.valueOf(hit.getSource().get(YearbookIndexKey.YEAR)));
            pointMap.setFrom(String.valueOf(hit.getSource().get(YearbookIndexKey.FROM)));
            result.add(pointMap);
        }
        return result;

    }

由于之后通过 城市、年份 搜索指标列表也是用这个接口,于是可以将相同部分抽离出来,放到如下这个方法中:

   @Override
    public List<YearbookResult> queryByYearCityIndex(YearbookSearch yearbookSearch){
        StringBuffer CityArr = new StringBuffer();
        StringBuffer YearArr = new StringBuffer();
        StringBuffer IndexArr = new StringBuffer();

        String[] Cityarr = yearbookSearch.getCitytr().split("\\s+");
        for (int i=0;i<Cityarr.length;i++){
            if(i>0){
                CityArr.append(",");
            }
            CityArr.append("'").append(Cityarr[i]).append("'");
        }

        String[] Yeararr = yearbookSearch.getYear().split("\\s+");
        for (int j=0;j<Yeararr.length;j++){
            if(j>0){
                YearArr.append(",");
            }
            YearArr.append("'").append(Yeararr[j]).append("'");
        }

        String[] Indexarr = yearbookSearch.getIndex_name().split("\\s+");
        for (int k=0;k<Indexarr.length;k++){
            if(k>0){
                IndexArr.append(",");
            }
            IndexArr.append("'").append(Indexarr[k]).append("'");
        }

        String whereExpress = "citytr in(" + CityArr +")" +
                " and " + "year in(" + YearArr +")" +
                " and " + "index_name in(" + IndexArr +")";
        SearchResponse response = searchBySql(whereExpress);

        List<YearbookResult> result=new ArrayList<YearbookResult>();
        //将搜索到的数据放入List
        for(SearchHit hit:response.getHits()){
            YearbookResult pointMap = new YearbookResult();
            pointMap.setCitytr(String.valueOf(hit.getSource().get(YearbookIndexKey.CITYTR)));
            pointMap.setDistinct(String.valueOf(hit.getSource().get(YearbookIndexKey.DISTINCT)));
            pointMap.setCountytr(String.valueOf(hit.getSource().get(YearbookIndexKey.COUNTYTR)));
            pointMap.setProvincetr(String.valueOf(hit.getSource().get(YearbookIndexKey.PROVINCETR)));
            pointMap.setStatisticNum(String.valueOf(hit.getSource().get(YearbookIndexKey.STATISTIC_NUM)));
            pointMap.setUnit(String.valueOf(hit.getSource().get(YearbookIndexKey.UNIT)));
            pointMap.setIndexName(String.valueOf(hit.getSource().get(YearbookIndexKey.INDEX_NAME)));
            pointMap.setYear(String.valueOf(hit.getSource().get(YearbookIndexKey.YEAR)));
            pointMap.setFrom(String.valueOf(hit.getSource().get(YearbookIndexKey.FROM)));
            result.add(pointMap);
        }
        return result;

    }

    //es搜索方法
    public SearchResponse searchResponse(YearbookSearch yearbookSearch){
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        //通过城市查询
        if(yearbookSearch.getCitytr()!=null){
            boolQuery.must(
                    QueryBuilders.matchQuery(YearbookIndexKey.CITYTR,
                            yearbookSearch.getCitytr()
                    )
            );
        }

        //通过年份查询
        if(yearbookSearch.getYear()!=null){
            boolQuery.must(
                    QueryBuilders.matchQuery(YearbookIndexKey.YEAR,yearbookSearch.getYear())
            );
        }

        //通过指标值查询
        if(yearbookSearch.getIndex_name()!=null){
            boolQuery.must(
                    QueryBuilders.matchQuery(YearbookIndexKey.INDEX_NAME,yearbookSearch.getIndex_name())
            );

        }

        //通过搜索框关键字查询
        if(yearbookSearch.getKeywords() != null && !yearbookSearch.getKeywords().isEmpty()){

            boolQuery.must(
                    QueryBuilders.multiMatchQuery(yearbookSearch.getKeywords(),
                            YearbookIndexKey.CITYTR,
                            YearbookIndexKey.INDEX_NAME,
                            YearbookIndexKey.YEAR,
                            YearbookIndexKey.CODE,
                            YearbookIndexKey.PROVINCETR,
                            YearbookIndexKey.STATISTIC_NUM,
                            YearbookIndexKey.TAG,
                            YearbookIndexKey.UNIT
                    ).operator(Operator.AND).type("cross_fields"));

        }

        SearchRequestBuilder requestBuilder = this.esClient.prepareSearch(INDEX_NAME)
                .setTypes(INDEX_TYPE)
                .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)  //当数据量足够多时可以用 QUERY_THEN_FETCH
                .setQuery(boolQuery)
                .setFrom(yearbookSearch.getStart())
                .setSize(20000);
        logger.debug(requestBuilder.toString());
        SearchResponse response = requestBuilder.get();
        return response;

    }

四、精确操作

需要通过 城市、年份、指标 精确搜索出数据,而之前为了可以通过搜索框搜索出结果,已经设置指标为可以分词了,如果通过 ES 直接搜索的话将会先进行分词再搜索,不会是完全精确查询得到的结果。于是查找了很多资料,决定用 elasticsearch-sql 进行搜索,elasticsearch-sql 官网没有给出 Java API 手册,网上也很少有 elasticsearch-sql 的 Java 相关资料,但是好在找到了一些。在 service 层中,首先将城市、年份、指标值各放到一个数组(因为可以多选),然后拼接 sql 语句,如下图所示:

    /**
     * 精确查找
     * 通过 ES 查询(通过城市、年份、指标查询,不通过搜索框)
     */
    @Override
    public List<YearbookResult> queryByYearCityIndex(YearbookSearch yearbookSearch){
        StringBuffer CityArr = new StringBuffer();
        StringBuffer YearArr = new StringBuffer();
        StringBuffer IndexArr = new StringBuffer();

        String[] Cityarr = yearbookSearch.getCitytr().split("\\s+");
        for (int i=0;i<Cityarr.length;i++){
            if(i>0){
                CityArr.append(",");
            }
            CityArr.append("'").append(Cityarr[i]).append("'");
        }

        String[] Yeararr = yearbookSearch.getYear().split("\\s+");
        for (int j=0;j<Yeararr.length;j++){
            if(j>0){
                YearArr.append(",");
            }
            YearArr.append("'").append(Yeararr[j]).append("'");
        }

        String[] Indexarr = yearbookSearch.getIndex_name().split("\\s+");
        for (int k=0;k<Indexarr.length;k++){
            if(k>0){
                IndexArr.append(",");
            }
            IndexArr.append("'").append(Indexarr[k]).append("'");
        }
        //拼接 sql 语句
        String whereExpress = "citytr in(" + CityArr +")" +
                " and " + "year in(" + YearArr +")" +
                " and " + "index_name in(" + IndexArr +")";
        SearchResponse response = searchBySql(whereExpress);

        List<YearbookResult> result=new ArrayList<YearbookResult>();
        //将搜索到的数据放入List
        for(SearchHit hit:response.getHits()){
            YearbookResult pointMap = new YearbookResult();
            pointMap.setCitytr(String.valueOf(hit.getSource().get(YearbookIndexKey.CITYTR)));
            pointMap.setDistinct(String.valueOf(hit.getSource().get(YearbookIndexKey.DISTINCT)));
            pointMap.setCountytr(String.valueOf(hit.getSource().get(YearbookIndexKey.COUNTYTR)));
            pointMap.setProvincetr(String.valueOf(hit.getSource().get(YearbookIndexKey.PROVINCETR)));
            pointMap.setStatisticNum(String.valueOf(hit.getSource().get(YearbookIndexKey.STATISTIC_NUM)));
            pointMap.setUnit(String.valueOf(hit.getSource().get(YearbookIndexKey.UNIT)));
            pointMap.setIndexName(String.valueOf(hit.getSource().get(YearbookIndexKey.INDEX_NAME)));
            pointMap.setYear(String.valueOf(hit.getSource().get(YearbookIndexKey.YEAR)));
            pointMap.setFrom(String.valueOf(hit.getSource().get(YearbookIndexKey.FROM)));
            result.add(pointMap);
        }
        return result;

    }

然后将拼接后的 sql 语句传入查询方法中,如下所示:

    /**
     *sql查询
     * whereExpress 查询条件
     * whereExpress-查询条件:(f1=2 and f2=1) or (f3=1 and f4=1)
     */
    public SearchResponse searchBySql(String whereExpress){
        try{
            // 转换Elasticsearch格式的查询条件
            QueryBuilder queryBuilder = createQueryBuilderByWhere(whereExpress);
            //查询具体信息
            SearchRequestBuilder requestBuilder = this.esClient.prepareSearch(INDEX_NAME)
                    .setTypes(INDEX_TYPE)
                    .setQuery(queryBuilder)
                    .setFrom(0)
                    .setSize(20000);
            SearchResponse response = requestBuilder.get();
            logger.debug(requestBuilder.toString());
            return response;

        }catch (Exception e){
            logger.warn("EsQueryUtil.seatchTotalByApi-Exception{}", e);
        }
        return null;
    }

调用 sql 转化的公共方法,根据表达式组装 ES 的 query 查询语句,这一步可以真正查询出数据,如下图所示:

    /**
     * sql转化公共方法--主要针对mysql
     * 根据表达式组装 ES 的 query 查询语句
     * whereExpress-查询条件:(f1=2 and f2=1) or (f3=1 and f4=1)
     */
    public static QueryBuilder createQueryBuilderByWhere(String whereExpress){
        BoolQueryBuilder boolQuery = null;
        try{
            String sql = "select * from " + INDEX_NAME;
            String whereTemp = "";
            if(!StringUtils.isEmpty(whereExpress)){
                whereTemp = " where " + whereExpress;
            }
            SQLQueryExpr sqlExpr = (SQLQueryExpr)toSqlExpr(sql + whereTemp);
            SqlParser sqlParser = new SqlParser();
            MySqlSelectQueryBlock query = (MySqlSelectQueryBlock) sqlExpr.getSubQuery().getQuery();
            WhereParser whereParser = new WhereParser(sqlParser,query);
            Where where = whereParser.findWhere();
            if(where!=null){
                boolQuery = QueryMaker.explan(where);
            }

        }catch (SqlParseException e){
            logger.warn("EsQueryUtil.createQueryBuilderByExpress-Exception", e);
        }
        return boolQuery;

    }

    /**
     * 验证sql
     * @param sql sql查询语句
     * @return
     */
    private static SQLExpr toSqlExpr(String sql){
        SQLExprParser parser = new ElasticSqlExprParser(sql);
        SQLExpr expr = parser.expr();

        if (parser.getLexer().token() != Token.EOF) {
            throw new ParserException("illegal sql expr : " + sql);
        }
        return expr;

    }

至此, Springboot 调用 elasticsearch-sql 进行数据的精确查询成功!

其中为了使页面返回的数据尽可能地多,在页面要设置最大返回值max_result_window;我是直接在  elasticsearch-head-master 里面设置的。先关掉索引,然后在复合查询里面,输入以下信息,注意这里是 PUT 请求方式,且 yearbook 指的是我的索引名称:

五、总结

这个项目搞了一个多月(还在搞),遇到了很多问题,也在慢慢地迭代中,期间遇到问题就想着如何解决问题,茶饭不思,也遇到很多简单的问题没想明白,但是大神一分钟就给解决完了,这个真的需要很多经验。

写这个项目的博客中,也写了很多天,很多地方写得不太好,以后会再完善完善。写博客,写文章真的是一项很重要的技能,希望自己以后坚持!!!

未来会继续使用 ElasticSearch 技术和 Springboot 技术,加油!!

猜你喜欢

转载自blog.csdn.net/Ocean111best/article/details/86095392