document
API test

跳转搜索页并复杂检索

GET
http://search.bitmall.com/list.html?keyword=%E5%8D%8E&catalog3Id=225&sort=skuPrice_asc&hasStock=0&brandId=9&attrs=7_A2217%3A123&pageNum=1&skuPrice=_12345

API description

复杂检索

Request Parameters

parameter
type
description
required
keyword
string
optional
catalog3Id
long
225
optional
sort
string
skuPrice_asc
optional
hasStock
integer
0
optional
brandId
long
9
optional
attrs
List<String>
7_A2217:123
optional
pageNum
integer
1
optional
skuPrice
string
_12345
optional

Description or Example

# 配置步骤 1. 配置相关依赖 2. 取消thymeleaf缓存 3. 导入资源 4. 配置子域名 4. 修改NGINX以及网关微服务 # BUG修复 ## 改变Product模块的controller, 微服务启动失败 > 将product模块的`IndexController`改为`ProductIndexController`, 发现启动的时候报错, 仔细研读, 大概说的是映射冗余, 原因是`IndexController`有了该映射, 但是`IndexController`已经被修改, IOC中仍存在该组件 ## 400 > 400错误这里归结到的原因一般是没有任何的查询条件, 导致参数为空, 这种情况不需要解决, 因为, 没有查询条件就不能来到检索页, 因此, 必须要有查询条件 > ***解决方法: 重新构建maven*** ## `Maven`所有的操作都报错 > 仔细研读报错信息, 发现父工程的GAV坐标出现了错误, 原因是`<relativepath/>`这个标签, 子模块都不允许有这个标签 ## `Maven`构建找不到common中的包 > 这里的报错信息是没有对应的符号或找不到对应的包 > 在common包中, 把插件配置成如下形式即可 ```xml <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <classifier>exec</classifier> </configuration> </plugin> </plugins> </build> ``` ## 二级目录始终无法跳转到正常目录的解决 > 修改`catalogLoader.js`该文件的路径即可 # 核心代码 ```java /** * <b>跳转首页并通过查询条件搜素出所有符合条件的记录</b> * @param mv 视图对象 * @param esSearchParamVO 该对象封装了所有的查询条件, 避免因为过多的参数导致不好维护 * @return 视图对象 */ @GetMapping("/list.html") public ModelAndView searchIndex(ModelAndView mv , @Validated(SearchGroup.class) EsSearchParamVO esSearchParamVO , HttpServletRequest servletRequest) throws IOException { // 进一步封装页面请求参数字段 String queryString = servletRequest.getQueryString(); // 编码后的查询条件 // 对查询条件进行解码, 避免浏览器的URL编码和后端编码不一致问题, 直接从解码解决所有的问题 queryString = URLUtil.decode(queryString); esSearchParamVO.setSearchParam(queryString); EsSearchRespVO esSearchRespVO = esSearchService.search(esSearchParamVO); mv.setViewName("list"); mv.addObject("result", esSearchRespVO); return mv; } ``` ```java @Service public class EsSearchServiceImpl implements EsSearchService { @Autowired private RestHighLevelClient esClient; @Override public EsSearchRespVO search(EsSearchParamVO esSearchParamVO) throws IOException { // 1. 构建请求及其参数 SearchRequest searchRequest = buildSearchReqAndParam(esSearchParamVO); // 2. 通过客户端, 调用es进行查询 SearchResponse response = esClient.search(searchRequest, SearchConfig.COMMENT_OPTIONS); // 3. 构建响应结果并返回 return buildSearchRespVos(response, esSearchParamVO); } /** * 构建响应信息 * 当前方法用于构建分页信息, 其他构建调用了其他方法 * @param response * @param esSearchParamVO * @return */ private EsSearchRespVO buildSearchRespVos(SearchResponse response, EsSearchParamVO esSearchParamVO) { // =======================封装分页==================================== SearchHits outerHits = response.getHits(); long total = outerHits.getTotalHits().value; // 总记录数 // 总页数采取了向上取整的策略 Integer totalPage = (int) ((total + ElasticSearchConstant.PAGE_NUM - 1) / ElasticSearchConstant.PAGE_NUM); Integer pageNum = esSearchParamVO.getPageNum(); // 当前页码号 SearchHit[] hits = outerHits.getHits(); List<SkuESModel> skuESModels = new ArrayList<>(); // 默认初始化一个空集合, 避免渲染的时候空指针 // 封装记录 skuESModels = getSkuESModelsFromResp(hits, skuESModels, esSearchParamVO.getKeyword()); // 从聚合中获取数据 RespAggToObject result = getRespAggToObject(response); // 获取页面数据 WebVO webVO = getWebVO(esSearchParamVO, pageNum, totalPage, result); return new EsSearchRespVO() .setTotalPage(totalPage) .setTotal(total) .setPageNum(pageNum) .setCatalogs(result.catalogs) .setBrands(result.brands) .setAttrs(result.attrs) .setSkuESModels(skuESModels) .setPageNavs(webVO.pageNavs) .setNavs(webVO.navs); } @Deprecated private WebVO getWebVO(EsSearchParamVO esSearchParamVO, Integer pageNum, Integer totalPage, RespAggToObject result) { // 封装页面数据(渲染页面的方法, 若进行前后端分离, 该方法可以直接删除), 分页信息一定存在 List<String> pageNavs = new ArrayList<String>() { { int cur = pageNum - 2; while (this.size() != 5 && cur <= totalPage) { // 如果还没有6个或没有遍历完 if (cur < 1) { cur++; }else { this.add(cur+""); cur++; } } } }; List<Nav> navs = new ArrayList<>(); // 初始化面包屑集合, 避免控制很 List<String> attrs = esSearchParamVO.getAttrs(); // 获取所有的规格参数查询条件 Long catalog3Id = esSearchParamVO.getCatalog3Id(); List<Long> brandIds = esSearchParamVO.getBrandId(); if (attrs != null && !attrs.isEmpty()) { // 存在规格参数查询条件时 navs.addAll( attrs.stream().map(attr -> { Nav nav = new Nav(); String[] navBody = attr.split("_"); String navValue = navBody[1]; Long attrId = Long.valueOf(navBody[0]); String navName = result.attrs.stream().filter(attrVO -> attrVO.getAttrId().equals(attrId)) .collect(Collectors.toList()).get(0).getAttrName(); // 不可能查询条件只有一个规格参数, 进来一定会有keyword或catalogId String oldStr = "&attrs=" + attrId + "_" + navValue; // 这里需要改一下前端 String link = esSearchParamVO.getSearchParam().replace(oldStr, ""); return nav.setNavName(navName).setNavValue(navValue).setLink(link); }).collect(Collectors.toList()) ); } // 规格参数的面包屑 if (catalog3Id != null) { // 存在分类 String navValue = result.catalogs.stream().filter(catalogVO -> catalogVO.getCatalogId().equals(catalog3Id)) .collect(Collectors.toList()).get(0).getCatalogName(); // 面包屑的值就是分类的名字 String navName = "分类"; // 面包屑的名字是分类 String oldStr = "catalog3Id=" + catalog3Id; String link = esSearchParamVO.getSearchParam().replace(oldStr, ""); link = link.replace("&&", "&"); // 处理不是分类开头的情况 navs.add(new Nav().setNavName(navName).setNavValue(navValue).setLink(link)); } // 分类的面包屑 if (brandIds != null && !brandIds.isEmpty()) { navs.addAll( brandIds.stream().map(brandId -> { String navValue = result.brands.stream().filter(brandVO -> brandVO.getBrandId().equals(brandId)).collect(Collectors.toList()) .get(0).getBrandName(); String navName = "品牌"; String oldStr = "&brandId=" + brandId; String link = esSearchParamVO.getSearchParam().replace(oldStr, ""); return new Nav().setNavName(navName).setNavValue(navValue).setLink(link); }).collect(Collectors.toList()) ); } return new WebVO(pageNavs, navs); } @Deprecated private static class WebVO { public final List<String> pageNavs; public final List<Nav> navs; public WebVO(List<String> pageNavs, List<Nav> navs) { this.pageNavs = pageNavs; this.navs = navs; } } /** * 在结果中抽速SkuEsModel对象 * * @param hits * @param skuESModels * @param keyword * @return */ private List<SkuESModel> getSkuESModelsFromResp(SearchHit[] hits, List<SkuESModel> skuESModels, String keyword) { // ========================封装记录=================================== if (hits != null) { // 有可能没有记录, 所以需要判断一下 skuESModels = Arrays.stream(hits).map(hit -> { String skuESModelJsonString = hit.getSourceAsString(); // SkuEsModel的json串 SkuESModel skuESModel = JSON.parseObject(skuESModelJsonString, SkuESModel.class); // 将底层价格前面的0删除 skuESModel.setSkuPrice(skuESModel.getSkuPrice().replaceAll("^0+", "")); if (StringUtils.isNotBlank(keyword))// 判断是否存在高亮, 动态添加高亮 skuESModel.setSkuTitle(hit.getHighlightFields() .get("skuTitle").getFragments()[0].toString()); return skuESModel; }).collect(Collectors.toList()); } return skuESModels; } /** * 在结果中抽取聚合中的数据 * @param response * @return */ private RespAggToObject getRespAggToObject(SearchResponse response) { // =========================封装聚合================================== Aggregations aggregations = response.getAggregations(); // 获取所有的聚合 ParsedLongTerms brandAgg = aggregations.get("brand_agg"); // 获取品牌聚合 List<? extends Terms.Bucket> brandAggBuckets = brandAgg.getBuckets(); List<BrandVO> brands = new ArrayList<>(); // 给品牌集合来个默认值, 避免空指针 if (brandAggBuckets != null && !brandAggBuckets.isEmpty()) { // 非空判断 brands = brandAggBuckets.stream().map(bucket -> { // 品牌可能有多个, 所以循环 BrandVO brandVO = new BrandVO(); long brandId = bucket.getKeyAsNumber().longValue(); Aggregations subAgg = bucket.getAggregations(); ParsedStringTerms brandImgAgg = subAgg.get("brand_img_agg"); String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString(); // 一个品牌的图片只能有一个 ParsedStringTerms brandNameAgg = subAgg.get("brand_name_agg"); String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString(); // 一个品牌的名字只能有一个 return brandVO.setBrandId(brandId).setBrandImg(brandImg).setBrandName(brandName); }).collect(Collectors.toList()); } // =========================================================== ParsedLongTerms categoryAgg = aggregations.get("category_agg"); // 获取分类聚合 List<? extends Terms.Bucket> categoryAggBuckets = categoryAgg.getBuckets(); List<CatalogVO> catalogs = new ArrayList<>(); // 给分类集合来个默认值, 避免空指针 if (categoryAggBuckets != null && !categoryAggBuckets.isEmpty()) { // 非空判断 catalogs = categoryAggBuckets.stream().map(bucket -> { // 分类可能有多个 CatalogVO catalogVO = new CatalogVO(); long catalogId = bucket.getKeyAsNumber().longValue(); // 分类id Aggregations subAgg = bucket.getAggregations(); ParsedStringTerms categoryNameAgg = subAgg.get("category_name_agg"); String catalogName = categoryNameAgg.getBuckets().get(0).getKeyAsString(); // 一个分类中只可能有一个名字 return catalogVO.setCatalogId(catalogId).setCatalogName(catalogName); }).collect(Collectors.toList()); } // =========================================================== ParsedNested attrAgg = aggregations.get("attr_agg"); // 获取规格参数聚合 ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attr_id_agg"); List<? extends Terms.Bucket> attrAggBuckets = attrIdAgg.getBuckets(); List<AttrVO> attrs = new ArrayList<>(); // 给一个默认值, 避免空指针异常 if (attrAggBuckets != null && !attrAggBuckets.isEmpty()) { attrs = attrAggBuckets.stream().map(bucket -> { // 规格参数可能有多个 AttrVO attrVO = new AttrVO(); long attrId = bucket.getKeyAsNumber().longValue(); Aggregations subAgg = bucket.getAggregations(); ParsedStringTerms attrNameAgg = subAgg.get("attr_name_agg"); String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString(); // 一个规格参数只有一个值 ParsedStringTerms attrValueAgg = subAgg.get("attr_value_agg"); List<? extends Terms.Bucket> attrValueAggBuckets = attrValueAgg.getBuckets(); // 一个规格参数有多个值 List<String> attrValues = new ArrayList<>(); // 给默认值, 避免空指针异常 if (attrValueAggBuckets != null && !attrValueAggBuckets.isEmpty()) { attrValues = attrValueAggBuckets.stream().map(valueBucket -> { return valueBucket.getKeyAsString(); }).collect(Collectors.toList()); } return attrVO.setAttrId(attrId).setAttrName(attrName).setAttrValue(attrValues); }).collect(Collectors.toList()); } RespAggToObject result = new RespAggToObject(brands, catalogs, attrs); return result; } private static class RespAggToObject { public final List<BrandVO> brands; public final List<CatalogVO> catalogs; public final List<AttrVO> attrs; public RespAggToObject(List<BrandVO> brands, List<CatalogVO> catalogs, List<AttrVO> attrs) { this.brands = brands; this.catalogs = catalogs; this.attrs = attrs; } } /** * 构建查询信息 * @param esSearchParamVO * @return */ private SearchRequest buildSearchReqAndParam(EsSearchParamVO esSearchParamVO) { SearchRequest searchRequest = new SearchRequest(ElasticSearchConstant.PRODUCT_INDEX); // 指定索引 SearchSourceBuilder source = SearchSourceBuilder.searchSource(); String keyword = buildDSLQuery(esSearchParamVO, source); // 构建query信息 this.buildDSLSort(esSearchParamVO, source); this.buildDSLFromSize(esSearchParamVO, source); this.buildDSLHighLight(keyword, source); this.buildDSLAggregations(source); searchRequest.source(source); return searchRequest; } /** * 构建聚合 * @param source */ private void buildDSLAggregations(SearchSourceBuilder source) { source.aggregation( AggregationBuilders.terms("brand_agg").field("brandId").size(127) .subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1)) .subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1)) ); // 品牌聚合 source.aggregation( AggregationBuilders.terms("category_agg").field("catalogId").size(127) .subAggregation(AggregationBuilders.terms("category_name_agg").field("catalogName").size(1)) ); // 分类聚合 source.aggregation( AggregationBuilders.nested("attr_agg", "attrs") .subAggregation(AggregationBuilders.terms("attr_id_agg").field("attrs.attrId").size(127) .subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1)) .subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(1)) ) ); } /** * 构建highLight信息 * @param keyword * @param source */ private void buildDSLHighLight(String keyword, SearchSourceBuilder source) { // =============================== // 高亮相关信息 if (StringUtils.isNotBlank(keyword)) { // 全文检索标题的时候才有高亮 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.preTags("<b style='color:red'><i>").postTags("</i></b>").field("skuTitle"); // 设置高亮前缀, 后缀和字段 source.highlighter(highlightBuilder); } } /** * 构建from size信息 * @param esSearchParamVO * @param source */ private void buildDSLFromSize(EsSearchParamVO esSearchParamVO, SearchSourceBuilder source) { // =============================== // 默认存在分页信息, 不需要条件判断 Integer curPage = esSearchParamVO.getPageNum(); // 获取当前分页 source.from((curPage - 1) * ElasticSearchConstant.PAGE_NUM); // 构建起始偏移量, (当前页 - 1) * 每一页数量 source.size(ElasticSearchConstant.PAGE_NUM); // 默认每页16条 } /** * 构建sort信息 * @param esSearchParamVO * @param source */ private void buildDSLSort(EsSearchParamVO esSearchParamVO, SearchSourceBuilder source) { // =============================== // 排序相关信息 String sort = esSearchParamVO.getSort(); // 获取排序字符串 if (StringUtils.isNotBlank(sort)) { // 不一定有排序条件 String[] sortBody = sort.split("_"); // 根据规则进行拆分, 长度必定为2 if (sortBody.length != 2) { throw new RuntimeException("排序规则有误!"); } String sortColumn = sortBody[0]; String sortRule = sortBody[1]; SortOrder sortOrder = sortRule.equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC; // 注意这里需要忽略大小写, 否则可能会判断错误 source.sort(sortColumn, sortOrder); } } /** * 指定DSL语句 * @param esSearchParamVO * @param source * @return */ private String buildDSLQuery(EsSearchParamVO esSearchParamVO, SearchSourceBuilder source) { // =============================== BoolQueryBuilder outerBool = QueryBuilders.boolQuery(); // ------------------------------- // 全文检索相关信息 String keyword = esSearchParamVO.getKeyword(); // 获取标题检索关键字 if (StringUtils.isNotBlank(keyword)) { // 需要全文检索标题时 outerBool.must(QueryBuilders.matchQuery("skuTitle", keyword)); } // ------------------------------- // 分类相关信息 Long categoryId = esSearchParamVO.getCatalog3Id(); if (categoryId != null) { // 需要通过分类查找 outerBool.filter(QueryBuilders.termQuery("catalogId", categoryId)); } // ------------------------------- // 库存相关信息 Integer hasStock = esSearchParamVO.getHasStock(); if (hasStock != null) { // 需要查找是否有库存 outerBool.filter(QueryBuilders.termQuery("hasStock", hasStock == 1)); // 如果不是1, 就没有库存 } // ------------------------------- // 品牌相关信息 List<Long> brandIds = esSearchParamVO.getBrandId(); if (brandIds != null && !brandIds.isEmpty()) { outerBool.filter(QueryBuilders.termsQuery("brandId", brandIds)); } // ------------------------------- // 价格区间相关信息 String skuPrice = esSearchParamVO.getSkuPrice(); if (StringUtils.isNotBlank(skuPrice)) { String[] rangeBody = skuPrice.split("_"); // 这里可能有空串, 需要去除空串 rangeBody = Arrays.stream(rangeBody) .filter(sb -> !sb.isEmpty()) .toArray(String[]::new); String min, max; if (rangeBody.length > 2 || rangeBody.length < 1) { throw new RuntimeException("价格区间格式错误"); }else if (rangeBody.length == 2) { min = NumberUtil.decimalFormat(NUMBER_FORMAT, new BigDecimal(rangeBody[0])); max = NumberUtil.decimalFormat(NUMBER_FORMAT, new BigDecimal(rangeBody[1])); // 说明是一个区间 outerBool.filter(QueryBuilders.rangeQuery("skuPrice").gte(min).lte(max)); }else { if (skuPrice.startsWith("_")) { // 判断是否为_开头, 如果是, 则 <= 某个值 max = NumberUtil.decimalFormat(NUMBER_FORMAT, new BigDecimal(rangeBody[0])); outerBool.filter(QueryBuilders.rangeQuery("skuPrice").lte(max)); }else { // 不是_开头, 则为 >= 某个值 min = NumberUtil.decimalFormat(NUMBER_FORMAT, new BigDecimal(rangeBody[0])); outerBool.filter(QueryBuilders.rangeQuery("skuPrice").gte(min)); } } } // ------------------------------- // 规格参数相关 List<String> attrs = esSearchParamVO.getAttrs(); if (attrs != null && !attrs.isEmpty()) { // 需要过滤规格参数 attrs.forEach(attr -> { // 解析该规格参数 String[] attrBody = attr.split("_"); // 获取属性体 if (attrBody.length != 2) { throw new RuntimeException("规格参数有误!"); } String attrId = attrBody[0]; // 规格参数的id String attrValue = attrBody[1]; // 规格参数的值 String[] attrValues = attrValue.split(":"); BoolQueryBuilder innerBool = QueryBuilders.boolQuery(); innerBool.filter(QueryBuilders.termQuery("attrs.attrId", attrId)); // id精确匹配 innerBool.filter(QueryBuilders.termsQuery("attrs.attrValue", attrValues)); // value精确匹配 outerBool.filter(QueryBuilders.nestedQuery("attrs", innerBool, ScoreMode.None));// ScoreMode.None 不参与评分 }); } // ------------------------------- source.query(outerBool); // 构建检索参数 return keyword; } } ```