检索服务
封装查询条件
注意请求标识
/*** 封装页面所有可能传递过来的查询条件* 三种点击搜索的方式* 1、点击搜索:keyword 【skuTitle】* 2、点击分类:传catalog3Id【catalogld】* 3、选择筛选条件* 1、全文检索: keyword【skuTitle】* 2、排序: saleCount_asc【销量】、hotScore_asc【综合排序:热度评分】、skuPrice_asc【价格】* 3、过滤: hasStock、skuPrice区间、brandld、catalog3ld、attrs* 4、聚合: attrs* attrs=2_5寸 传参格式,所以直接for循环split("_")就可以得到attrId与attrValue* attrs=1_白色:蓝色 然后值split(":")得到各项值attrValue*/@Datapublic class SearchParam {private String keyword;// 页面传递过来的全文匹配关键字private Long catalog3Id;// 三级分类的id/*** 排序:sort=saleCount_asc sort=hotScore_asc sort=skuPrice_asc*/private String sort;/*** 过滤条件:* hasStock=0/1【有货】* skuPrice=0_500/500_/_500【价格区间】* brandld=1* attrs=1_白色:蓝色&attrs=2_2寸:5寸【属性可多选,值也可多选】*/private Integer hasStock = 1;// 是否只显示有货,默认显示有货,null == 1会NullPoint异常 0/1private String skuPrice;// 按照价格区间查询private List<Long> brandId;// 品牌id,可多选private List<String> attrs;// 三级分类id+属性值private Integer pageNum = 1;// 页码private String _queryString;// 原生的所有查询条件(来自url的请求参数),用于构建面包屑}
返回结果类
显示结果VO类

包括下面商品信息
面包屑导航

@Datapublic class SearchResult {private List<SkuEsModel> products;// es检索到的所有商品信息/*** 分页信息*/private Integer pageNum;// 当前页码private Long total;// 总记录数private Integer totalPages;// 总页码private List<Integer> pageNavs;// 导航页码[1、2、3、4、5]private List<BrandVo> brands;// 当前查询到的结果所有涉及到的品牌private List<CatalogVo> catalogs;// 当前查询到的结果所有涉及到的分类/*** attrs=1_anzhuo&attrs=5_其他:1080P*/private List<AttrVo> attrs = new ArrayList<>();// 当前查询到的结果所有涉及到的属性【符合检索条件的,可检索的属性】// ============================以上是要返回的数据====================================// 面包屑导航数据private List<NavVo> navs = new ArrayList<>();// 封装筛选条件中的属性id集合【用于面包屑,选择属性后出现在面包屑中,下面的属性栏则隐藏】// 该字段是提供前端用的private List<Long> attrIds = new ArrayList<>();/*** 面包屑导航VO*/@Datapublic static class NavVo {private String navName;// 属性名private String navValue;// 属性值private String link;// 回退地址(删除该面包屑筛选条件回退地址)}@Datapublic static class BrandVo {private Long brandId;//private String brandName;//private String brandImg;//}@Datapublic static class CatalogVo {private Long catalogId;// 显示分类idprivate String catalogName;// 显示分类name}@Datapublic static class AttrVo {private Long attrId;// 允许检索的 属性Idprivate String attrName;// 允许检索的 属性名private List<String> attrValue;// 属性值【多个】}}
ES查询的实例语句
{"query": {"bool": {"must": [{"match": {"skuTitle": "华为"}}],"filter": [{"term": {"catalogId": "225"}},{"terms": {"brandId": ["9","5"]}},{"nested": {"path": "attrs","query": {"bool": {"must": [{"term": {"attrs.attrId": {"value": "15"}}},{"terms": {"attrs.attrValue": ["高通(Qualcomm)"]}}]}}}},{"term": {"hasStock": {"value": "true"}}},{"range": {"skuPrice": {"gte": 0,"lte": 7000}}}]}},"sort": [{"saleCount": {"order": "desc"}}],"from": 0,"size": 2,"highlight": {"fields": {"skuTitle": {}},"pre_tags": "<b style='color:red'>","post_tags": "</b>"},"aggs": {"brand_agg": {"terms": {"field": "brandId","size": 10},"aggs": {"brand_name_agg": {"terms": {"field": "brandName","size": 10}},"brand_img_agg": {"terms": {"field": "brandImg","size": 10}}}},"catalog_agg": {"terms": {"field": "catalogId","size": 10},"aggs": {"catalog_name_agg": {"terms": {"field": "catalogName","size": 10}}}},"attr_agg": {"nested": {"path": "attrs"},"aggs": {"attr_id_agg": {"terms": {"field": "attrs.attrId","size": 10},"aggs": {"attr_name_agg": {"terms": {"field": "attrs.attrName","size": 10}},"attr_value_agg": {"terms": {"field": "attrs.attrValue","size": 10}}}}}}}}
请求封装接口
@Overridepublic SearchResult search(SearchParam searchParam) {// 动态构建查询需要的DSL语句SearchResult searchResult = null;// 准备检索请求SearchRequest searchRequest = buildSearchRequest(searchParam);// 执行检索请求try {SearchResponse response = restHighLevelClient.search(searchRequest, GulimallSearchConfig.COMMON_OPTIONS);// 封装返回结果searchResult = buildSearchResponse(response, searchParam);} catch (IOException e) {e.printStackTrace();}return searchResult;}/*** 准备检索请求(参照dsl.json文件)** @param searchParam* @return*/private SearchRequest buildSearchRequest(SearchParam searchParam) {SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 构建DSL语句// 构建 bool-query,根据商品的标题进行检索BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();if (!StringUtils.isEmpty(searchParam.getKeyword())) {boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", searchParam.getKeyword()));}// 构建 bool-filter-term,根据三级分类进行检索if (!StringUtils.isEmpty(searchParam.getCatalog3Id())) {boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", searchParam.getCatalog3Id()));}// 构建 bool-filter-term,根据品牌id进行检索if (!StringUtils.isEmpty(searchParam.getBrandId())) {boolQueryBuilder.filter(QueryBuilders.termQuery("brandId", searchParam.getBrandId()));}// bool-filter,根据属性值进行检索if (!StringUtils.isEmpty(searchParam.getAttrs())) {for (String attr : searchParam.getAttrs()) {BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();// attr格式 attrs=1_白色:蓝色String[] attrs = attr.split("_");String attrId = attrs[0];String[] attrValues = attrs[1].split(":");// 再将多个属性值分开// TODO 此时如果属性只有一个,那么在ES中会封装为[高通],此时会查不到数据nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));if (attrValues.length == 1) {nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrValue", attrValues[0]));} else {// 多个就放入数组nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrValue", attrValues));}// 创建nested查询(注意每一个属性值,都需要一个nested进行查询)NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None); // 最后参数表示不参与评分boolQueryBuilder.filter(nestedQuery);}}// 构建 bool-filter-term,根据是否有库存进行检索boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", searchParam.getHasStock() == 1));// bool-filter,根据价格区间进行查询// skuPrice=0_500/500_/_500【价格区间】if (!StringUtils.isEmpty(searchParam.getSkuPrice())) {RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");String skuPrice = searchParam.getSkuPrice();String[] s = skuPrice.split("_");if (s.length == 2) { // 指定了两边区间 0_500rangeQuery.gte(s[0]).lte(s[1]);} else { // 指定了一边区间 500_/_500if (skuPrice.endsWith("_")) {rangeQuery.gte(s[0]);}if (skuPrice.startsWith("_")) {rangeQuery.lte(s[0]);}}boolQueryBuilder.filter(rangeQuery);}// 把以前所有的条件封装起来searchSourceBuilder.query(boolQueryBuilder);// 排序:sort=saleCount_ascif (!StringUtils.isEmpty(searchParam.getSort())) {String[] s = searchParam.getSort().split("_");SortOrder sortOrder = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;searchSourceBuilder.sort(s[0], sortOrder);}// 分页// "from": 0,// "size": 2,searchSourceBuilder.from((searchParam.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);// 高亮// "highlight": {// "fields": {// "skuTitle": {}// },// "pre_tags": "<b style='color:red'>",// "post_tags": "</b>"// }if (!StringUtils.isEmpty(searchParam.getKeyword())) {HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field("skuTitle");highlightBuilder.preTags("<b style='color:red'>");highlightBuilder.postTags("</b>");searchSourceBuilder.highlighter(highlightBuilder);}// 聚合分析// 品牌聚合TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));searchSourceBuilder.aggregation(brand_agg);// 分类聚合TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));searchSourceBuilder.aggregation(catalog_agg);// 属性聚合(nested属性)NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));attr_agg.subAggregation(attr_id_agg);searchSourceBuilder.aggregation(attr_agg);System.out.println(searchSourceBuilder.toString());SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);return searchRequest;}
ES返回JSON数据
{"took" : 14,"timed_out" : false,"_shards" : {"total" : 1,"successful" : 1,"skipped" : 0,"failed" : 0},"hits" : {"total" : {"value" : 4,"relation" : "eq"},"max_score" : null,"hits" : [{"_index" : "gulimall_product","_type" : "_doc","_id" : "51","_score" : null,"_source" : {"attrs" : [{"attrId" : 15,"attrName" : "CPU品牌","attrValue" : "高通(Qualcomm)"},{"attrId" : 16,"attrName" : "CPU型号","attrValue" : "鸿蒙"}],"brandId" : 9,"brandImg" : "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/de2426bd-a689-41d0-865a-d45d1afa7cde_huawei.png","brandName" : "华为","catalogId" : 225,"catalogName" : "手机","hasStock" : true,"hotScore" : 0,"saleCount" : 0,"skuId" : 51,"skuImg" : "https://project-guli-education.oss-cn-chengdu.aliyuncs.com/2022-05-03/842c391d-6a37-441e-8aa4-ce5041b23ecb_23d9fbb256ea5d4a.jpg","skuPrice" : 6099.0,"skuTitle" : "华为mate40pro 5G手机 夏日胡杨 8+128G全网通(4G版) 黑色 8+256GB 4G","spuId" : 29},"highlight" : {"skuTitle" : ["<b style='color:red'>华为</b>mate40pro 5G手机 夏日胡杨 8+128G全网通(4G版) 黑色 8+256GB 4G"]},"sort" : [0]},{"_index" : "gulimall_product","_type" : "_doc","_id" : "52","_score" : null,"_source" : {"attrs" : [{"attrId" : 15,"attrName" : "CPU品牌","attrValue" : "高通(Qualcomm)"},{"attrId" : 16,"attrName" : "CPU型号","attrValue" : "鸿蒙"}],"brandId" : 9,"brandImg" : "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/de2426bd-a689-41d0-865a-d45d1afa7cde_huawei.png","brandName" : "华为","catalogId" : 225,"catalogName" : "手机","hasStock" : true,"hotScore" : 0,"saleCount" : 0,"skuId" : 52,"skuImg" : "https://project-guli-education.oss-cn-chengdu.aliyuncs.com/2022-05-03/842c391d-6a37-441e-8aa4-ce5041b23ecb_23d9fbb256ea5d4a.jpg","skuPrice" : 6099.0,"skuTitle" : "华为mate40pro 5G手机 夏日胡杨 8+128G全网通(4G版) 黑色 8+256GB 5G","spuId" : 29},"highlight" : {"skuTitle" : ["<b style='color:red'>华为</b>mate40pro 5G手机 夏日胡杨 8+128G全网通(4G版) 黑色 8+256GB 5G"]},"sort" : [0]}]},"aggregations" : {"catalog_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : 225,"doc_count" : 4,"catalog_name_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "手机","doc_count" : 4}]}}]},"attr_agg" : {"doc_count" : 8,"attr_id_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : 15,"doc_count" : 4,"attr_name_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "CPU品牌","doc_count" : 4}]},"attr_value_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "高通(Qualcomm)","doc_count" : 4}]}},{"key" : 16,"doc_count" : 4,"attr_name_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "CPU型号","doc_count" : 4}]},"attr_value_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "鸿蒙","doc_count" : 4}]}}]}},"brand_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : 9,"doc_count" : 4,"brand_img_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-18/de2426bd-a689-41d0-865a-d45d1afa7cde_huawei.png","doc_count" : 4}]},"brand_name_agg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "华为","doc_count" : 4}]}}]}}}
返回结果封装方法
/*** 封装返回结果(参照result.json文件)** @param response* @return*/private SearchResult buildSearchResponse(SearchResponse response, SearchParam searchParam) {SearchResult searchResult = new SearchResult();SearchHits hits = response.getHits();// 封装商品信息// 保存进去就是以SkuEsModel保存的List<SkuEsModel> skuEsModelList = new ArrayList<>();// hits的hits中的source保存的是商品信息for (SearchHit hit : response.getHits()) {// 获取到json数据,转换为对象String sourceAsString = hit.getSourceAsString();SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);// 设置高亮标题if (!StringUtils.isEmpty(searchParam.getKeyword())) {Map<String, HighlightField> highlightFieldMap = hit.getHighlightFields();String skuTitle = highlightFieldMap.get("skuTitle").getFragments()[0].string();skuEsModel.setSkuTitle(skuTitle);}skuEsModelList.add(skuEsModel);}searchResult.setProducts(skuEsModelList);// 当前商品所涉及的品牌信息ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");List<SearchResult.BrandVo> brandVoList = new ArrayList<>();List<? extends Terms.Bucket> brandList = brand_agg.getBuckets();for (Terms.Bucket bucket : brandList) {SearchResult.BrandVo brandVo = new SearchResult.BrandVo();// 获取品牌的idlong brandId = bucket.getKeyAsNumber().longValue();// 获取品牌的名字String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();// 获取品牌的图片String brandImage = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();brandVo.setBrandId(brandId);brandVo.setBrandName(brandName);brandVo.setBrandImg(brandImage);brandVoList.add(brandVo);}searchResult.setBrands(brandVoList);// 当前商品所涉及的属性信息ParsedNested attr_agg = response.getAggregations().get("attr_agg");List<SearchResult.AttrVo> attrVoList = new ArrayList<>();ParsedLongTerms attrIdAgg = attr_agg.getAggregations().get("attr_id_agg");for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {SearchResult.AttrVo attrVo = new SearchResult.AttrVo();// 属性idlong attrId = bucket.getKeyAsNumber().longValue();// 属性名String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();// 属性值List<String> attrValueList = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {String attrValue = ((Terms.Bucket) item).getKeyAsString();return attrValue;}).collect(Collectors.toList());attrVo.setAttrId(attrId);attrVo.setAttrName(attrName);attrVo.setAttrValue(attrValueList);attrVoList.add(attrVo);}searchResult.setAttrs(attrVoList);// 当前商品所涉及到的所有分类信息(从聚合中取出)ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");List<SearchResult.CatalogVo> catalogVoList = new ArrayList<>();List<? extends Terms.Bucket> catalogList = catalog_agg.getBuckets();for (Terms.Bucket bucket : catalogList) {SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();// 得到分类idString catalogId = bucket.getKeyAsString();catalogVo.setCatalogId(Long.parseLong(catalogId));// 得到分类名,子聚合ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");List<? extends Terms.Bucket> catalogNameList = catalog_name_agg.getBuckets();String catalogName = catalogNameList.get(0).getKeyAsString();catalogVo.setCatalogName(catalogName);catalogVoList.add(catalogVo);}searchResult.setCatalogs(catalogVoList);// 封装分页信息long total = hits.getTotalHits().value;searchResult.setPageNum(searchParam.getPageNum());searchResult.setTotal(total);// 总页码(总页数 / 每页数)有余数加一int totalPage = (int) total % EsConstant.PRODUCT_PAGESIZE == 1 ? (((int) total / EsConstant.PRODUCT_PAGESIZE) + 1) : (int) total / EsConstant.PRODUCT_PAGESIZE;searchResult.setTotalPages(totalPage);// 导航页码[1、2、3、4、5]List<Integer> pageNavList = new ArrayList<>();for (int i = 1; i <= totalPage; i++) {pageNavList.add(i);}searchResult.setPageNavs(pageNavList);// 构建面包屑导航功能if (searchParam.getAttrs() != null && searchParam.getAttrs().size() > 0) {List<SearchResult.NavVo> navVoList = searchParam.getAttrs().stream().map(attr -> {SearchResult.NavVo navVo = new SearchResult.NavVo();// attr=2_5寸:6寸(此时只有属性id和属性值)String[] split = attr.split("_");// 此时需要调用product服务查询属性名navVo.setNavValue(split[1]);R r = productFeignClient.attrInfo(Long.parseLong(split[0]));searchResult.getAttrIds().add(Long.parseLong(split[0]));if (r.getCode() == 0) {AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {});// 设置属性名navVo.setNavName(data.getAttrName());} else {navVo.setNavName(split[0]);}// 取消了这个面包屑以后,我们要跳转到那个地方.将请求地址的urL里面的当前置空拿到所有的查询条件,去掉当前等条件// attrs=15_海思(Hisilicon)String replace = replaceQueryString(searchParam, attr, "attrs");navVo.setLink("http://search.gulimalls.com/list.html?" + replace);return navVo;}).collect(Collectors.toList());searchResult.setNavs(navVoList);}if (!StringUtils.isEmpty(searchParam.getBrandId())) {SearchResult.NavVo navVo = new SearchResult.NavVo();navVo.setNavName("品牌");// 远程查询所有品牌R r = productFeignClient.getInfoByBrandIds(searchParam.getBrandId());if (r.getCode() == 0) {List<BrandVo> brandVo = r.getData("brand", new TypeReference<List<BrandVo>>() {});StringBuffer sb = new StringBuffer();String replace = null;for (BrandVo brand : brandVo) {sb.append(brand.getBrandName() + ";");replace = replaceQueryString(searchParam, brand.getBrandId().toString(), "brandId");}navVo.setNavValue(sb.toString());navVo.setLink("http://search.gulimalls.com/list.html?" + replace);}searchResult.getNavs().add(navVo);}// TODO 分类的面包屑,不需要导航取消return searchResult;}/*** 替换地址字符串** @param searchParam* @param attr* @return*/private String replaceQueryString(SearchParam searchParam, String attr, String key) {// TODO 请求地址参数转义有问题String encode = null;try {encode = URLEncoder.encode(attr, "UTF-8");encode = encode.replace("+", "%20"); // 处理空格encode = encode.replace("%28", "("); // 处理)encode = encode.replace("%29", ")"); // 处理(} catch (UnsupportedEncodingException e) {e.printStackTrace();}String replace = searchParam.get_queryString().replace("&" + key + "=" + encode, " ");return replace;}
Controller层
@Controllerpublic class SearchController {@Autowiredprivate ElasticSearchService elasticSearchService;@GetMapping("/list.html")public String list(SearchParam searchParam, Model model, HttpServletRequest httpServletRequest) {searchParam.set_queryString(httpServletRequest.getQueryString()); // 获取查询请求SearchResult searchResult = elasticSearchService.search(searchParam);model.addAttribute("result", searchResult);return "list";}}
