19.检索服务
Search模块
1.整理前端内容
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
html放入template目录下,静态资源放到nginx/search目录下
配置域名转发
想实现的效果是查询search.gulimall能访问到检索页面,在nginx中配置,不管是gulimall.com还是前面有前缀的都应该交给网关处理
网关配置:
配置热部署依赖并且配置缓存为false
<!-- 热部署依赖,使页面实时生效--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency>
spring:
thymeleaf:
cache: false
以后ctrl+f9就可以重新构建了
2.后台的实现(前端内容不介绍了)
实现点击三级分类跳转到list页面--检索页面
@Controller public class SearchController { //转交给检索页面 @GetMapping("/list.html") public String listPage(){ return "list"; } }
之前映射的有错误,现在在es处迁移映射
PUT gulimall_product { "mappings":{ "properties": { "skuId":{ "type": "long" }, "spuId":{ "type": "keyword" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "skuPrice": { "type": "keyword" }, "skuImg" : { "type": "keyword" }, "saleCount":{ "type":"long" }, "hasStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catalogId": { "type": "long" }, "brandName": {"type": "keyword"}, "brandImg":{ "type": "keyword" }, "catalogName": {"type": "keyword" }, "attrs": { "type": "nested", "properties": { "attrId": {"type": "long" }, "attrName": { "type": "keyword" }, "attrValue": {"type": "keyword" } } } } } }
POST _reindex { "source": { "index": "product" }, "dest": { "index": "gulimall_product" } }
构建的查询条件
GET gulimall_product/_search { "query": { "bool": { "must": [ {"match": { "skuTitle": "华为" }} ], # 检索出华为 "filter": [ # 过滤 { "term": { "catalogId": "225" } }, { "terms": {"brandId": [ "2"] } }, { "term": { "hasStock": "false"} }, { "range": { "skuPrice": { # 价格1K~7K "gte": 1000, "lte": 7000 } } }, { "nested": { "path": "attrs", # 聚合名字 "query": { "bool": { "must": [ { "term": { "attrs.attrId": { "value": "6"} } } ] } } } } ] } }, "sort": [ {"skuPrice": {"order": "desc" } } ], "from": 0, "size": 5, "highlight": { "fields": {"skuTitle": {}}, # 高亮的字段 "pre_tags": "<b style='color:red'>", # 前缀 "post_tags": "</b>" }, "aggs": { # 查完后聚合 "brandAgg": { "terms": { "field": "brandId", "size": 10 }, "aggs": { # 子聚合 "brandNameAgg": { # 每个商品id的品牌 "terms": { "field": "brandName", "size": 10 } }, "brandImgAgg": { "terms": { "field": "brandImg", "size": 10 } } } }, "catalogAgg":{ "terms": { "field": "catalogId", "size": 10 }, "aggs": { "catalogNameAgg": { "terms": { "field": "catalogName", "size": 10 } } } }, "attrs":{ "nested": {"path": "attrs" }, "aggs": { "attrIdAgg": { "terms": { "field": "attrs.attrId", "size": 10 }, "aggs": { "attrNameAgg": { "terms": { "field": "attrs.attrName", "size": 10 } } } } } } } }
请求的构建
private SearchRequest buildSearchRequest(SearchPara searchPara) { SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); //1.构建bool的query---》检索 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //1.1)must模糊匹配 对应语句 "must": [ {"match": { "skuTitle": "华为" }} ] if (!StringUtils.isEmpty(searchPara.getKeyword())) { boolQuery.must(QueryBuilders.matchQuery("skuTitle", searchPara.getKeyword())); } //1.2)filter 与must的区别是filter的项目不参与评分 //"filter": [ # 过滤{ "term": { "catalogId": "225" } }, if (searchPara.getCatalog3Id() != null) { boolQuery.filter(QueryBuilders.termQuery("catalogId", searchPara.getCatalog3Id())); } //1.3) { "terms": {"brandId": [ "2"] } }, { "term": { "hasStock": "false"} } if (searchPara.getBrandId() != null && searchPara.getBrandId().size() > 0) { boolQuery.filter(QueryBuilders.termQuery("brandId", searchPara.getBrandId())); } boolQuery.filter(QueryBuilders.termQuery("hasStock", searchPara.getHasStock() == 1)); //1.4) "range": {"skuPrice": { # 价格1K~7K"gte": 1000, "lte": 7000}} 1_500表示大于1小于500 RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice"); if (!StringUtils.isEmpty(searchPara.getSkuPrice())) { String[] split = searchPara.getSkuPrice().split("_"); if (split.length == 2) {//表示是一个区间 rangeQuery.gte(split[0]).lte(split[1]); } else { if (searchPara.getSkuPrice().startsWith("_")) { rangeQuery.lte(split[0]); } else { rangeQuery.gte(split[0]); } } boolQuery.filter(rangeQuery); } //传入的值 attrs=1_5寸:8存 attrs=2_16G:8G 前面的1_ 2_表示几号属性 //1.5) {"nested": {"path": "attrs", # 聚合名字"query": {"bool": {"must": [{"term": { "attrs.attrId": { "value": "6" if (searchPara.getAttrs() != null && searchPara.getAttrs().size() > 0) { for (String attr : searchPara.getAttrs()) { BoolQueryBuilder boolQuery1 = QueryBuilders.boolQuery(); String[] s = attr.split("_"); String attrId = s[0];//检索的属性id String[] attrValue = s[1].split(":");//检索的值 boolQuery1.must(QueryBuilders.termQuery("attrs.attrId", attrId)); boolQuery1.must(QueryBuilders.termsQuery("attrs.attrValue", attrValue)); NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolQuery1, ScoreMode.None);//none表示不参与评分 boolQuery.filter(nestedQuery);//每个属性都要生成一个嵌入式的查询条件 } } sourceBuilder.query(boolQuery); //2.sort--》排序 //sort=hostScore_asc/desc String sort = searchPara.getSort(); if (!StringUtils.isEmpty(sort)) { String[] s = sort.split("_"); SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC; sourceBuilder.sort(s[0], order); } //3.分页 //pageNum:1 from 0 size 5 [0,1,2,3,4] //pageNum:2 from 5 size 5 [5,6,7,8,9] //from=(pageNum-1)*size if (searchPara.getPageNum() != null) { sourceBuilder.from((searchPara.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE); sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE); } //4.高亮 /* "highlight": { "fields": {"skuTitle": {}}, # 高亮的字段 "pre_tags": "<b style='color:red'>", # 前缀 "post_tags": "</b>" }, */ if (!StringUtils.isEmpty(searchPara.getKeyword())) { HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("skuTitle"); highlightBuilder.preTags("<b style='color:red>"); highlightBuilder.postTags("</b>"); sourceBuilder.highlighter(highlightBuilder); } //5.聚合分析 //5.1)聚合品牌 /* "aggs": { "brandAgg": {"terms": {"field": "brandId","size": 10 */ TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg"); brandAgg.field("brandId").size(10); //5.2) 品牌子聚合 /* "aggs": { "brandNameAgg": { "terms": { "field": "brandName", "size": 10 "brandImgAgg": { "terms": { "field": "brandImg", "size": 10 */ brandAgg.subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName").size(10)); brandAgg.subAggregation(AggregationBuilders.terms("brandImgAgg").field("brandImg").size(10)); sourceBuilder.aggregation(brandAgg); //5.3)分类聚合 /* "catalogAgg":{ "terms": { "field": "catalogId", "size": 10 }, "aggs": { "catalogNameAgg": { "terms": { "field": "catalogName", "size": 10 */ TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId").size(10); catalogAgg.subAggregation(AggregationBuilders.terms("catalogNameAgg").field("catalogName").size(1)); sourceBuilder.aggregation(catalogAgg); //5.4)属性聚合 /* "attrs":{ "nested": {"path": "attrs" }, "aggs": { "attrIdAgg": { "terms": { "field": "attrs.attrId", "size": 10 }, "aggs": { "attrNameAgg": { "terms": { "field": "attrs.attrName", "size": 10 "aggs": { "attrValueAgg": { "terms": { "field": "attrs.attrValue", "size": 10 */ NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attr_agg", "attrs"); //聚合出id TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId").size(10); //id里聚合出名字 attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(10)); attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(10)); //小聚合放入大聚合 attrsAgg.subAggregation(attrIdAgg); sourceBuilder.aggregation(attrsAgg); //客户端打印 String s = sourceBuilder.toString(); System.out.println(s); SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder); return searchRequest; }
结果的返回
private SearchResult buildSearchResult(SearchResponse response, SearchPara para) { SearchResult result = new SearchResult(); SearchHits hits = response.getHits(); //1.返回查询到的所有商品 List<SkuEsModel> list = new ArrayList<>(); SearchHit[] searchHits = hits.getHits(); if(searchHits!=null&&searchHits.length>0){ for (SearchHit hit : searchHits) { String sourceAsString = hit.getSourceAsString(); //解析数据 SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class); //如果有高亮的信息 if(!StringUtils.isEmpty(para.getKeyword())){//按关键字检索了就设置高亮的字段 HighlightField skuTitle = hit.getHighlightFields().get("skuTitle"); String s = skuTitle.getFragments()[0].toString(); esModel.setSkuTitle(s); } list.add(esModel); } } result.setProducts(list); //2.商品分类信息 ParsedLongTerms catalogAgg = response.getAggregations().get("catalogAgg");//ParsedLongTerms就是Long类型的Term聚合 List<? extends Terms.Bucket> buckets = catalogAgg.getBuckets(); ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>(); for (Terms.Bucket bucket : buckets) { SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(); catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString())); //名字在子聚合里面 ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");//ParsedStringTerms就是String类型的Term聚合 String catalogName = catalogAgg.getBuckets().get(0).getKeyAsString(); catalogVo.setCatalogName(catalogName); catalogVos.add(catalogVo); } //3.当前商品的品牌信息 List<SearchResult.BrandVo> brandVos = new ArrayList<>(); Aggregations aggregations = response.getAggregations(); //ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据 ParsedLongTerms brandAgg = aggregations.get("brand_agg"); for (Terms.Bucket bucket : brandAgg.getBuckets()) { // 得到品牌id Long brandId = bucket.getKeyAsNumber().longValue(); Aggregations subBrandAggs = bucket.getAggregations(); //得到品牌图片 ParsedStringTerms brandImgAgg=subBrandAggs.get("brandImgAgg"); String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString(); // 得到品牌名字 Terms brandNameAgg=subBrandAggs.get("brandNameAgg"); String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString(); SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg); brandVos.add(brandVo); } result.setBrands(brandVos); //4.属性信息 List<SearchResult.AttrVo> attrVos = new ArrayList<>(); //ParsedNested用于接收内置属性的聚合 ParsedNested parsedNested=aggregations.get("attr_agg"); ParsedLongTerms attrIdAgg=parsedNested.getAggregations().get("attrIdAgg"); for (Terms.Bucket bucket : attrIdAgg.getBuckets()) { //查询属性id Long attrId = bucket.getKeyAsNumber().longValue(); Aggregations subAttrAgg = bucket.getAggregations(); //查询属性名 ParsedStringTerms attrNameAgg=subAttrAgg.get("attrNameAgg"); String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString(); //查询属性值 ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg"); List<String> attrValues = new ArrayList<>(); for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) { String attrValue = attrValueAggBucket.getKeyAsString(); attrValues.add(attrValue); List<SearchResult.NavVo> navVos = new ArrayList<>(); } SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues); attrVos.add(attrVo); } result.setAttrs(attrVos); //5.分页页码 result.setPageNum(para.getPageNum()); //6.总记录数,总页码 long totalRecord = hits.getTotalHits().value; result.setTotal(totalRecord); int total=(int)totalRecord; int totalPages=total%EsConstant.PRODUCT_PAGESIZE==0?total/EsConstant.PRODUCT_PAGESIZE:(total/EsConstant.PRODUCT_PAGESIZE)+1; result.setTotalPages(totalPages); return result; }
整体方法
@Override public SearchResult search(SearchPara searchPara) { //构建检索请求 SearchRequest request = buildSearchRequest(searchPara); SearchResult result =null; try { //执行检索请求 SearchResponse search = client.search(request, ESConfig.COMMON_OPTIONS); //分析响应数据封装成我们需要的格式 result = buildSearchResult(search,searchPara); } catch (IOException e) { e.printStackTrace(); } return result; }
Controller
//转交给检索页面 @GetMapping("/list.html") public String listPage(SearchPara searchPara, Model model){ SearchResult result=mallSearchService.search(searchPara); model.addAttribute("result",result); return "list"; }
页面渲染前端内容略
面包屑导航功能
search模块导入远程调用依赖,同时要导入springcloud的依赖管理
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
开启远程调用功能
search模块的远程接口
@FeignClient("gulimall-product") public interface ProductFeignService { @GetMapping("/product/attr/info/{attrId}") public R attrInfo(@PathVariable("attrId") Long attrId);//通过id获得att的信息 }
新增common模块utils的R方法
//利用fastJson进行逆转 public<T> T getData(String key,TypeReference<T> typeReference){ Object data=get(key); String s = JSON.toJSONString(data); T t=JSON.parseObject(s,typeReference); return t; }
@GetMapping("/list.html") public String listPage(SearchPara searchPara, Model model, HttpServletRequest request){ String queryString=request.getQueryString(); searchPara.set_queryString(queryString);//设置url SearchResult result=mallSearchService.search(searchPara); model.addAttribute("result",result); return "list"; }
private SearchResult buildSearchResult(SearchResponse response, SearchPara para) { SearchResult result = new SearchResult(); SearchHits hits = response.getHits(); //1.返回查询到的所有商品 List<SkuEsModel> list = new ArrayList<>(); SearchHit[] searchHits = hits.getHits(); if (searchHits != null && searchHits.length > 0) { for (SearchHit hit : searchHits) { String sourceAsString = hit.getSourceAsString(); //解析数据 SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class); //如果有高亮的信息 if (!StringUtils.isEmpty(para.getKeyword())) {//按关键字检索了就设置高亮的字段 HighlightField skuTitle = hit.getHighlightFields().get("skuTitle"); String s = skuTitle.getFragments()[0].toString(); esModel.setSkuTitle(s); } list.add(esModel); } } result.setProducts(list); //2.商品分类信息 ParsedLongTerms catalogAgg = response.getAggregations().get("catalogAgg");//ParsedLongTerms就是Long类型的Term聚合 List<? extends Terms.Bucket> buckets = catalogAgg.getBuckets(); ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>(); for (Terms.Bucket bucket : buckets) { SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(); catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString())); //名字在子聚合里面 ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogNameAgg");//ParsedStringTerms就是String类型的Term聚合 String catalogName = catalogAgg.getBuckets().get(0).getKeyAsString(); catalogVo.setCatalogName(catalogName); catalogVos.add(catalogVo); } //3.当前商品的品牌信息 List<SearchResult.BrandVo> brandVos = new ArrayList<>(); Aggregations aggregations = response.getAggregations(); //ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据 ParsedLongTerms brandAgg = aggregations.get("brand_agg"); for (Terms.Bucket bucket : brandAgg.getBuckets()) { // 得到品牌id Long brandId = bucket.getKeyAsNumber().longValue(); Aggregations subBrandAggs = bucket.getAggregations(); //得到品牌图片 ParsedStringTerms brandImgAgg = subBrandAggs.get("brandImgAgg"); String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString(); // 得到品牌名字 Terms brandNameAgg = subBrandAggs.get("brandNameAgg"); String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString(); SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg); brandVos.add(brandVo); } result.setBrands(brandVos); //4.属性信息 List<SearchResult.AttrVo> attrVos = new ArrayList<>(); //ParsedNested用于接收内置属性的聚合 ParsedNested parsedNested = aggregations.get("attr_agg"); ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg"); for (Terms.Bucket bucket : attrIdAgg.getBuckets()) { //查询属性id Long attrId = bucket.getKeyAsNumber().longValue(); Aggregations subAttrAgg = bucket.getAggregations(); //查询属性名 ParsedStringTerms attrNameAgg = subAttrAgg.get("attrNameAgg"); String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString(); //查询属性值 ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg"); List<String> attrValues = new ArrayList<>(); for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) { String attrValue = attrValueAggBucket.getKeyAsString(); attrValues.add(attrValue); List<SearchResult.NavVo> navVos = new ArrayList<>(); } SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues); attrVos.add(attrVo); } result.setAttrs(attrVos); //5.分页页码 result.setPageNum(para.getPageNum()); //6.总记录数,总页码 long totalRecord = hits.getTotalHits().value; result.setTotal(totalRecord); int total = (int) totalRecord; int totalPages = total % EsConstant.PRODUCT_PAGESIZE == 0 ? total / EsConstant.PRODUCT_PAGESIZE : (total / EsConstant.PRODUCT_PAGESIZE) + 1; result.setTotalPages(totalPages); //7.构建面包屑 导航功能 if (para.getAttrs() != null && para.getAttrs().size() > 0) { List<SearchResult.NavVo> navVos = para.getAttrs().stream().map(attr -> { SearchResult.NavVo navVo = new SearchResult.NavVo(); //attrs=2_5寸:6寸 //分析每一个传过来的查询参数值 String[] s = attr.split("_"); navVo.setNavValue(s[1]); R r = productFeignService.attrInfo(Long.parseLong(s[0])); if (r.getCode() == 0) { new TypeReference<SearchResult.AttrVo>() { }; AttrResponseVo vo = r.getData("attr", new TypeReference<AttrResponseVo>() { }); navVo.setNavValue(vo.getAttrName());//拿到属性的名字 } else { navVo.setName(s[0]); } //取消了面包屑以后我们要跳转到哪--》将请求地址的url里面的当前置空 //拿到所有查询条件,去掉当前--》在controller里拿到值 String encode = ""; try { encode = URLEncoder.encode(attr, "UTF-8");//前端的数据是编码过的 encode.replace("+", "%20");//空格会被浏览器解析为%20然后被java解析成+ } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String replace = para.get_queryString().replace("&attrs=" + attr, "").replace("attrs=" + attr + "&", "").replace("attrs=" + attr, ""); navVo.setLink("http://search.gulimall.com/list.html?" + replace); return navVo; }).collect(Collectors.toList()); result.setNavs(navVos); } return result; }