当前位置:   article > 正文

Springboot+ElasticSearch 项目实战(三)_springboot elasticsearch-sql

springboot elasticsearch-sql

这篇接着上一篇(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 的配置,如下图:

  1. /**
  2. * es配置文件
  3. * es注入主要依靠一个 TransportClient
  4. */
  5. @Configuration
  6. public class myconfig {
  7. @Value("${elasticsearch.host}")
  8. private String esHost;
  9. @Value("${elasticsearch.port}")
  10. private int esPort;
  11. @Value("${elasticsearch.cluster.name}")
  12. private String esName;
  13. @Bean
  14. public TransportClient esClient() throws UnknownHostException {
  15. Settings settings = Settings.builder()
  16. .put("cluster.name", this.esName)
  17. .put("client.transport.sniff", true)
  18. .build();
  19. InetSocketTransportAddress master = new InetSocketTransportAddress(
  20. InetAddress.getByName(esHost), esPort
  21. );
  22. TransportClient client = new PreBuiltTransportClient(settings)
  23. .addTransportAddress(master);
  24. return client;
  25. }
  26. }

三、通过搜索框搜索

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

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

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

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

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

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

  1. /**
  2. * 通过 ES 查询(通过搜索框查询)
  3. */
  4. @Override
  5. public List<YearbookResult> query(YearbookSearch yearbookSearch){
  6. SearchResponse response = searchResponse(yearbookSearch);
  7. List<YearbookResult> result=new ArrayList<YearbookResult>();
  8. //将搜索到的数据放入List
  9. for(SearchHit hit:response.getHits()){
  10. YearbookResult pointMap = new YearbookResult();
  11. pointMap.setCitytr(String.valueOf(hit.getSource().get(YearbookIndexKey.CITYTR)));
  12. pointMap.setDistinct(String.valueOf(hit.getSource().get(YearbookIndexKey.DISTINCT)));
  13. pointMap.setCountytr(String.valueOf(hit.getSource().get(YearbookIndexKey.COUNTYTR)));
  14. pointMap.setProvincetr(String.valueOf(hit.getSource().get(YearbookIndexKey.PROVINCETR)));
  15. pointMap.setStatisticNum(String.valueOf(hit.getSource().get(YearbookIndexKey.STATISTIC_NUM)));
  16. pointMap.setUnit(String.valueOf(hit.getSource().get(YearbookIndexKey.UNIT)));
  17. pointMap.setIndexName(String.valueOf(hit.getSource().get(YearbookIndexKey.INDEX_NAME)));
  18. pointMap.setYear(String.valueOf(hit.getSource().get(YearbookIndexKey.YEAR)));
  19. pointMap.setFrom(String.valueOf(hit.getSource().get(YearbookIndexKey.FROM)));
  20. result.add(pointMap);
  21. }
  22. return result;
  23. }

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

  1. @Override
  2. public List<YearbookResult> queryByYearCityIndex(YearbookSearch yearbookSearch){
  3. StringBuffer CityArr = new StringBuffer();
  4. StringBuffer YearArr = new StringBuffer();
  5. StringBuffer IndexArr = new StringBuffer();
  6. String[] Cityarr = yearbookSearch.getCitytr().split("\\s+");
  7. for (int i=0;i<Cityarr.length;i++){
  8. if(i>0){
  9. CityArr.append(",");
  10. }
  11. CityArr.append("'").append(Cityarr[i]).append("'");
  12. }
  13. String[] Yeararr = yearbookSearch.getYear().split("\\s+");
  14. for (int j=0;j<Yeararr.length;j++){
  15. if(j>0){
  16. YearArr.append(",");
  17. }
  18. YearArr.append("'").append(Yeararr[j]).append("'");
  19. }
  20. String[] Indexarr = yearbookSearch.getIndex_name().split("\\s+");
  21. for (int k=0;k<Indexarr.length;k++){
  22. if(k>0){
  23. IndexArr.append(",");
  24. }
  25. IndexArr.append("'").append(Indexarr[k]).append("'");
  26. }
  27. String whereExpress = "citytr in(" + CityArr +")" +
  28. " and " + "year in(" + YearArr +")" +
  29. " and " + "index_name in(" + IndexArr +")";
  30. SearchResponse response = searchBySql(whereExpress);
  31. List<YearbookResult> result=new ArrayList<YearbookResult>();
  32. //将搜索到的数据放入List
  33. for(SearchHit hit:response.getHits()){
  34. YearbookResult pointMap = new YearbookResult();
  35. pointMap.setCitytr(String.valueOf(hit.getSource().get(YearbookIndexKey.CITYTR)));
  36. pointMap.setDistinct(String.valueOf(hit.getSource().get(YearbookIndexKey.DISTINCT)));
  37. pointMap.setCountytr(String.valueOf(hit.getSource().get(YearbookIndexKey.COUNTYTR)));
  38. pointMap.setProvincetr(String.valueOf(hit.getSource().get(YearbookIndexKey.PROVINCETR)));
  39. pointMap.setStatisticNum(String.valueOf(hit.getSource().get(YearbookIndexKey.STATISTIC_NUM)));
  40. pointMap.setUnit(String.valueOf(hit.getSource().get(YearbookIndexKey.UNIT)));
  41. pointMap.setIndexName(String.valueOf(hit.getSource().get(YearbookIndexKey.INDEX_NAME)));
  42. pointMap.setYear(String.valueOf(hit.getSource().get(YearbookIndexKey.YEAR)));
  43. pointMap.setFrom(String.valueOf(hit.getSource().get(YearbookIndexKey.FROM)));
  44. result.add(pointMap);
  45. }
  46. return result;
  47. }
  48. //es搜索方法
  49. public SearchResponse searchResponse(YearbookSearch yearbookSearch){
  50. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  51. //通过城市查询
  52. if(yearbookSearch.getCitytr()!=null){
  53. boolQuery.must(
  54. QueryBuilders.matchQuery(YearbookIndexKey.CITYTR,
  55. yearbookSearch.getCitytr()
  56. )
  57. );
  58. }
  59. //通过年份查询
  60. if(yearbookSearch.getYear()!=null){
  61. boolQuery.must(
  62. QueryBuilders.matchQuery(YearbookIndexKey.YEAR,yearbookSearch.getYear())
  63. );
  64. }
  65. //通过指标值查询
  66. if(yearbookSearch.getIndex_name()!=null){
  67. boolQuery.must(
  68. QueryBuilders.matchQuery(YearbookIndexKey.INDEX_NAME,yearbookSearch.getIndex_name())
  69. );
  70. }
  71. //通过搜索框关键字查询
  72. if(yearbookSearch.getKeywords() != null && !yearbookSearch.getKeywords().isEmpty()){
  73. boolQuery.must(
  74. QueryBuilders.multiMatchQuery(yearbookSearch.getKeywords(),
  75. YearbookIndexKey.CITYTR,
  76. YearbookIndexKey.INDEX_NAME,
  77. YearbookIndexKey.YEAR,
  78. YearbookIndexKey.CODE,
  79. YearbookIndexKey.PROVINCETR,
  80. YearbookIndexKey.STATISTIC_NUM,
  81. YearbookIndexKey.TAG,
  82. YearbookIndexKey.UNIT
  83. ).operator(Operator.AND).type("cross_fields"));
  84. }
  85. SearchRequestBuilder requestBuilder = this.esClient.prepareSearch(INDEX_NAME)
  86. .setTypes(INDEX_TYPE)
  87. .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) //当数据量足够多时可以用 QUERY_THEN_FETCH
  88. .setQuery(boolQuery)
  89. .setFrom(yearbookSearch.getStart())
  90. .setSize(20000);
  91. logger.debug(requestBuilder.toString());
  92. SearchResponse response = requestBuilder.get();
  93. return response;
  94. }

四、精确操作

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

  1. /**
  2. * 精确查找
  3. * 通过 ES 查询(通过城市、年份、指标查询,不通过搜索框)
  4. */
  5. @Override
  6. public List<YearbookResult> queryByYearCityIndex(YearbookSearch yearbookSearch){
  7. StringBuffer CityArr = new StringBuffer();
  8. StringBuffer YearArr = new StringBuffer();
  9. StringBuffer IndexArr = new StringBuffer();
  10. String[] Cityarr = yearbookSearch.getCitytr().split("\\s+");
  11. for (int i=0;i<Cityarr.length;i++){
  12. if(i>0){
  13. CityArr.append(",");
  14. }
  15. CityArr.append("'").append(Cityarr[i]).append("'");
  16. }
  17. String[] Yeararr = yearbookSearch.getYear().split("\\s+");
  18. for (int j=0;j<Yeararr.length;j++){
  19. if(j>0){
  20. YearArr.append(",");
  21. }
  22. YearArr.append("'").append(Yeararr[j]).append("'");
  23. }
  24. String[] Indexarr = yearbookSearch.getIndex_name().split("\\s+");
  25. for (int k=0;k<Indexarr.length;k++){
  26. if(k>0){
  27. IndexArr.append(",");
  28. }
  29. IndexArr.append("'").append(Indexarr[k]).append("'");
  30. }
  31. //拼接 sql 语句
  32. String whereExpress = "citytr in(" + CityArr +")" +
  33. " and " + "year in(" + YearArr +")" +
  34. " and " + "index_name in(" + IndexArr +")";
  35. SearchResponse response = searchBySql(whereExpress);
  36. List<YearbookResult> result=new ArrayList<YearbookResult>();
  37. //将搜索到的数据放入List
  38. for(SearchHit hit:response.getHits()){
  39. YearbookResult pointMap = new YearbookResult();
  40. pointMap.setCitytr(String.valueOf(hit.getSource().get(YearbookIndexKey.CITYTR)));
  41. pointMap.setDistinct(String.valueOf(hit.getSource().get(YearbookIndexKey.DISTINCT)));
  42. pointMap.setCountytr(String.valueOf(hit.getSource().get(YearbookIndexKey.COUNTYTR)));
  43. pointMap.setProvincetr(String.valueOf(hit.getSource().get(YearbookIndexKey.PROVINCETR)));
  44. pointMap.setStatisticNum(String.valueOf(hit.getSource().get(YearbookIndexKey.STATISTIC_NUM)));
  45. pointMap.setUnit(String.valueOf(hit.getSource().get(YearbookIndexKey.UNIT)));
  46. pointMap.setIndexName(String.valueOf(hit.getSource().get(YearbookIndexKey.INDEX_NAME)));
  47. pointMap.setYear(String.valueOf(hit.getSource().get(YearbookIndexKey.YEAR)));
  48. pointMap.setFrom(String.valueOf(hit.getSource().get(YearbookIndexKey.FROM)));
  49. result.add(pointMap);
  50. }
  51. return result;
  52. }

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

  1. /**
  2. *sql查询
  3. * whereExpress 查询条件
  4. * whereExpress-查询条件:(f1=2 and f2=1) or (f3=1 and f4=1)
  5. */
  6. public SearchResponse searchBySql(String whereExpress){
  7. try{
  8. // 转换Elasticsearch格式的查询条件
  9. QueryBuilder queryBuilder = createQueryBuilderByWhere(whereExpress);
  10. //查询具体信息
  11. SearchRequestBuilder requestBuilder = this.esClient.prepareSearch(INDEX_NAME)
  12. .setTypes(INDEX_TYPE)
  13. .setQuery(queryBuilder)
  14. .setFrom(0)
  15. .setSize(20000);
  16. SearchResponse response = requestBuilder.get();
  17. logger.debug(requestBuilder.toString());
  18. return response;
  19. }catch (Exception e){
  20. logger.warn("EsQueryUtil.seatchTotalByApi-Exception{}", e);
  21. }
  22. return null;
  23. }

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

  1. /**
  2. * sql转化公共方法--主要针对mysql
  3. * 根据表达式组装 ES 的 query 查询语句
  4. * whereExpress-查询条件:(f1=2 and f2=1) or (f3=1 and f4=1)
  5. */
  6. public static QueryBuilder createQueryBuilderByWhere(String whereExpress){
  7. BoolQueryBuilder boolQuery = null;
  8. try{
  9. String sql = "select * from " + INDEX_NAME;
  10. String whereTemp = "";
  11. if(!StringUtils.isEmpty(whereExpress)){
  12. whereTemp = " where " + whereExpress;
  13. }
  14. SQLQueryExpr sqlExpr = (SQLQueryExpr)toSqlExpr(sql + whereTemp);
  15. SqlParser sqlParser = new SqlParser();
  16. MySqlSelectQueryBlock query = (MySqlSelectQueryBlock) sqlExpr.getSubQuery().getQuery();
  17. WhereParser whereParser = new WhereParser(sqlParser,query);
  18. Where where = whereParser.findWhere();
  19. if(where!=null){
  20. boolQuery = QueryMaker.explan(where);
  21. }
  22. }catch (SqlParseException e){
  23. logger.warn("EsQueryUtil.createQueryBuilderByExpress-Exception", e);
  24. }
  25. return boolQuery;
  26. }
  27. /**
  28. * 验证sql
  29. * @param sql sql查询语句
  30. * @return
  31. */
  32. private static SQLExpr toSqlExpr(String sql){
  33. SQLExprParser parser = new ElasticSqlExprParser(sql);
  34. SQLExpr expr = parser.expr();
  35. if (parser.getLexer().token() != Token.EOF) {
  36. throw new ParserException("illegal sql expr : " + sql);
  37. }
  38. return expr;
  39. }

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

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

五、总结

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

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

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

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/200651
推荐阅读
相关标签
  

闽ICP备14008679号