这篇接着上一篇(https://blog.csdn.net/Ocean111best/article/details/86092862),本来准备上一篇写完,但是发现还是太多,故再写一篇。
发现刚开始写博客时,千言万语,最后确不知道怎么说,只有直接简单粗暴地贴代码了,希望以后坚持多写,慢慢地有所提高。
一、首先配置整个项目的 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 result = yearbookService.query(yearbookSearch);
return new ResponseEntity(result, HttpStatus.OK);
}
在 service 层中,由于在网上看到的操作很多都是或操作,但是在搜索框中搜索的是并操作。之后查找了很久的资料,发现可以设置如下参数即可达到搜索的是并操作的目标:
.operator(Operator.AND).type("cross_fields")
则在 service 里面可以进行如下操作:
/**
* 通过 ES 查询(通过搜索框查询)
*/
@Override
public List query(YearbookSearch yearbookSearch){
SearchResponse response = searchResponse(yearbookSearch);
List result=new ArrayList();
//将搜索到的数据放入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 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;i0){
CityArr.append(",");
}
CityArr.append("'").append(Cityarr[i]).append("'");
}
String[] Yeararr = yearbookSearch.getYear().split("\\s+");
for (int j=0;j0){
YearArr.append(",");
}
YearArr.append("'").append(Yeararr[j]).append("'");
}
String[] Indexarr = yearbookSearch.getIndex_name().split("\\s+");
for (int k=0;k0){
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 result=new ArrayList();
//将搜索到的数据放入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 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;i0){
CityArr.append(",");
}
CityArr.append("'").append(Cityarr[i]).append("'");
}
String[] Yeararr = yearbookSearch.getYear().split("\\s+");
for (int j=0;j0){
YearArr.append(",");
}
YearArr.append("'").append(Yeararr[j]).append("'");
}
String[] Indexarr = yearbookSearch.getIndex_name().split("\\s+");
for (int k=0;k0){
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 result=new ArrayList();
//将搜索到的数据放入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 技术,加油!!