1、搭建页面环境
1、gulimall-search的pom.xml添加thymeleaf依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、将静态资源放入到static文件夹下,而将index.html放入到templates文件夹下:
3、在虚拟机mydata/nginx/html/路径下创建search文件夹然后把搜索页的静态资源上传到该文件里
4、修改index.html页面访问静态资源的地址,例如:href,src
5、host添加
C:\Windows\System32\drivers\etc
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
6、配置nginx
7、重启nginx
8、修改网关断言配置
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
gulimall-search的pom.xml添加devtools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
gulimall-search的配置文件关闭thymeleaf缓存
spring.thymeleaf.cache=false
Nginx转发效果
9、测试,访问search.gulimall.com
2、调整页面跳转
2.1、跳转到首页
当点击检索页的 "谷粒商城" 可以跳转到首页
修改gulimall-search的index.html代码:
2.2、商品检索三个入口
1.选择分类进入商品检索
2.输入检索关键字展示检索页
3.选择筛选条件进入
4.最终跳转的页面效果
代码实现:
1、修改gulimall-search的index.html名字为list.html
2、新增“com.atguigu.gulimall.search.controller.SearchController”类,代码如下:
@Controller
public class SearchController {
@GetMapping("/list.html")
public String listPage() {
return "list";
}
}
3、我们看到该html已经有search()函数,作为搜索点击的方法
<script type="text/javascript">
function search() {
var keyword=$("#searchText").val()
window.location.href="http://search.gulimall1.com/list.html?keyword="+keyword;
}
</script>
我们点击搜索无效,发现引用js方法不对,接下来修改gulimall-product的index.html类,代码如下:
我们看到分类的js:catalogLoader.js已经设置好超链接地址,不需要我们调整什么,直接使用就好
3.检索条件&排序条件分析
全文检索:skuTitle -> keyword
排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs
聚合:attrs
完整查询参数:keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏
4、DSL分析
检索时需要进行:模糊匹配、过滤(按照属性,分类,品牌,价格区间,库存)、排序、分页、高亮、聚合分析 。
原先的映射(mapping)不符合我们现在的检索需求,我们需要先调整映射,在进行分析。
1、修改映射
创建一份新的数据,然后进行数据映射
PUT gulimall_product
{
"mappings": {
"properties": {
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
},
"autoGeneratedTimestamp": {
"type": "long"
},
"brandId": {
"type": "long"
},
"brandImg": {
"type": "keyword"
},
"brandName": {
"type": "keyword"
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword"
},
"description": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"parentTask": {
"properties": {
"id": {
"type": "long"
},
"nodeId": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"set": {
"type": "boolean"
}
}
},
"refreshPolicy": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"retry": {
"type": "boolean"
},
"saleCount": {
"type": "long"
},
"shouldStoreResult": {
"type": "boolean"
},
"skuId": {
"type": "long"
},
"skuImg": {
"type": "keyword"
},
"skuPrice": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"spuId": {
"type": "keyword"
}
}
}
}
迁移数据
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "gulimall_product"
}
}
修改gulimall-search的常量
修改“com.atguigu.gulimall.search.constant.EsConstant”类,代码如下
public class EsConstant {
public static final String PRODUCT_INDEX = "gulimall_product"; //sku数据在es中的索引
}
2、使用DSL检索
模糊匹配,过滤(属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析。
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"4"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
"term": {
"attrs.attrId": "5"
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
}
}
3、聚合分析
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"4"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
# // 由于attrs是数组,es默认扁平话处理
# // 因此需要nested
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "1"
}
}
}
]
}
}
}
}
]
}
},
"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": {
"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
}
}
}
}
}
}
}
}
4、结果
{
"took" : 8,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "gulimall_product",
"_type" : "_doc",
"_id" : "12",
"_score" : null,
"_source" : {
"attrs" : [
{
"attrId" : 1,
"attrName" : "入网型号",
"attrValue" : "111"
},
{
"attrId" : 2,
"attrName" : "上市年份",
"attrValue" : "2019"
},
{
"attrId" : 4,
"attrName" : "机身颜色",
"attrValue" : "星空灰"
}
],
"brandId" : 4,
"brandImg" : "https://wts-gulimall.oss-cn-shanghai.aliyuncs.com/2024-04-24/c10918fe-7241-47e9-8e74-c3310e768876_huawei.png",
"brandName" : "华为",
"catalogId" : 225,
"catalogName" : "手机",
"hasStock" : false,
"hotScore" : 0,
"saleCount" : 0,
"skuId" : 12,
"skuImg" : "https://wts-gulimall.oss-cn-shanghai.aliyuncs.com/2024-04-26//9e1f33b9-7eb6-489b-8c0f-2149077a3526_d511faab82abb34b.jpg",
"skuPrice" : 6799.0,
"skuTitle" : "华为(HUAWEI)华为Mate30 Pro 全网通 4G 白色 8G +128G",
"spuId" : 10
},
"highlight" : {
"skuTitle" : [
"<b style='color:red'>华为</b>(HUAWEI)<b style='color:red'>华为</b>Mate30 Pro 全网通 4G 白色 8G +128G"
]
},
"sort" : [
"6799.0"
]
}
]
},
"aggregations" : {
"brandAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 4,
"doc_count" : 1,
"brandImgAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "https://wts-gulimall.oss-cn-shanghai.aliyuncs.com/2024-04-24/c10918fe-7241-47e9-8e74-c3310e768876_huawei.png",
"doc_count" : 1
}
]
},
"brandNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "华为",
"doc_count" : 1
}
]
}
}
]
},
"catalogAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 225,
"doc_count" : 1,
"catalogNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 1
}
]
}
}
]
},
"attrs" : {
"doc_count" : 3,
"attrIdAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 1,
"doc_count" : 1,
"attrNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "入网型号",
"doc_count" : 1
}
]
}
},
{
"key" : 2,
"doc_count" : 1,
"attrNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "上市年份",
"doc_count" : 1
}
]
}
},
{
"key" : 4,
"doc_count" : 1,
"attrNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "机身颜色",
"doc_count" : 1
}
]
}
}
]
}
}
}
}
5、检索代码编写
1、请求参数和返回结果
1)、请求参数的封
新增“com.atguigu.gulimall.search.vo.SearchParam”类,代码如下:
package com.atguigu.gulimall.search.vo;
import lombok.Data;
import java.util.List;
/**
* 封装页面所有可能传递过来的查询条件
* catalog3Id=225&keyword=小米&sort=saleCount_asc
*/
@Data
public class SearchParam {
private String keyword;//页面传递过来的全文匹配关键字
private Long catalog3Id;//三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
private String sort;//排序条件
/**
* 好多的过滤条件
* hasStock(是否有货)、skuPrice区间、brandId、catalog3Id、attrs
* hasStock=0/1
* skuPrice=1_500
*/
private Integer hasStock;//是否只显示有货
private String skuPrice;//价格区间查询
private List<Long> brandId;//按照品牌进行查询,可以多选
/**
&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏
*/
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1;//页码
}
2)、返回结果
package com.atguigu.gulimall.search.vo;
import com.atguigu.common.to.es.SkuEsModel;
import lombok.Data;
import java.util.List;
@Data
public class SearchResult {
/**
* 查询到的商品信息
*/
private List<SkuEsModel> products;
private Integer pageNum;//当前页码
private Long total;//总记录数
private Integer totalPages;//总页码
private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
//=====================以上是返给页面的信息==========================
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
private String brandImg;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
2、主体逻辑
修改“com.atguigu.gulimall.search.controller.SearchController”类,代码如下:
/**
* 自动将页面提交过来的所有请求查询参数封装成指定的对象
*
* @return
*/
@GetMapping("/list.html")
public String listPage(SearchParam searchParam, Model model) {
SearchResult result = mallSearchService.search(searchParam);
System.out.println("====================" + result);
model.addAttribute("result", result);
return "list";
}
新增“com.atguigu.gulimall.search.service.MallSearchService”类,代码如下:
/**
*
* @param param 检索的所有参数
* @return 返回检索的结果,里面包含页面需要的所有信息
*/
SearchResult search(SearchParam param);
主要逻辑在service层进行,service层将封装好的SearchParam组建查询条件,再将返回后的结果封装成SearchResult
新增“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl”类,代码如下:
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.MallSearchService;
import com.atguigu.gulimall.search.vo.SearchParam;
import com.atguigu.gulimall.search.vo.SearchResult;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
RestHighLevelClient restHighLevelClient;
//去es进行检索
@Override
public SearchResult search(SearchParam param) {
// 动态构建出查询需要的DSL语句
SearchResult result = null;
// 1、准备检索请求
SearchRequest searchRequest = buildSearchRequest(param);
try {
// 2、执行检索请求
SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// 分析响应数据封装我们需要的格式
result = buildSearchResult(response, param);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 准备检索请求
* 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存),排序,分页,高亮,聚合分析
*
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
/**
* 1、过滤(按照属性、分类、品牌、价格区间、库存)
*/
// 1、构建bool-query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
sourceBuilder.query(boolQuery);
// 1.1、must-模糊匹配、
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
// 1.2.1、filter-按照三级分类id查询
if (null != param.getCatalog3Id()) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
// 1.2.2、filter-按照品牌id查询
if (null != param.getBrandId() && param.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
// 1.2.3、filter-按照是否有库存进行查询
if (null != param.getHasStock()) { // true/false
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
}
// 1.2.4、filter-按照区间进行查询 1_500/_500/500_
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(param.getSkuPrice())) {
String[] prices = param.getSkuPrice().split("_");
if (prices.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
} else {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
} else if (prices.length == 2) {
// _6000会截取成["","6000"]
if (!prices[0].isEmpty()) {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
}
boolQuery.filter(rangeQueryBuilder);
}
// 1.2.5、filter-按照属性进行查询
List<String> attrs = param.getAttrs();
if (null != attrs && attrs.size() > 0) {
// attrs=1_5寸:8寸&2_16G:8G
attrs.forEach(attr -> {
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
String[] attrSplit = attr.split("_");
queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
String[] attrValues = attrSplit[1].split(":");
queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
// 每一个必须都得生成一个nested查询
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
boolQuery.filter(nestedQueryBuilder);
});
}
// 把以前所有的条件都拿来进行封装
sourceBuilder.query(boolQuery);
/**
* 2、排序,分页,高亮,
*/
// 2.1、排序 eg:sort=saleCount_desc/asc
if (!StringUtils.isEmpty(param.getSort())) {
String[] sortSplit = param.getSort().split("_");
sourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
}
// 2.2、分页
sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
// 2.3、高亮highlight
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
sourceBuilder.highlighter(highlightBuilder);
}
/**
* 3、聚合分析
*/
// 3、聚合
// 3.1、按照品牌聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
// 3.1.1、品牌聚合的子聚合
TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
brand_agg.subAggregation(brand_name_agg);
TermsAggregationBuilder brand_img_agg = AggregationBuilders.terms("brand_img_agg").field("brandImg");
brand_agg.subAggregation(brand_img_agg);
sourceBuilder.aggregation(brand_agg);
// 3.2、按照catalog聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
catalog_agg.subAggregation(catalog_name_agg);
sourceBuilder.aggregation(catalog_agg);
// 3.3、按照attrs聚合
NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attr_agg", "attrs");
// 3.3.1、按照attrId聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
// 3.3.2、按照attrId聚合之后再按照attrName和attrValue聚合
TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
attr_id_agg.subAggregation(attr_name_agg);
TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
attr_id_agg.subAggregation(attr_value_agg);
nestedAggregationBuilder.subAggregation(attr_id_agg);
sourceBuilder.aggregation(nestedAggregationBuilder);
String s = sourceBuilder.toString();
System.out.println("构建的DSL" + s);
SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
return request;
}
/**
* 构建结果数据
*
* @param response
* @return
*/
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
SearchResult result = new SearchResult();
// 1、返回的所有查询到的商品
SearchHits hits = response.getHits();
List<SkuEsModel> esModels = new ArrayList<>();
if (null != hits.getHits() && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
esModel.setSkuTitle(skuTitle.fragments()[0].string());
}
esModels.add(esModel);
}
}
result.setProducts(esModels);
// 2、当前所有商品涉及到的所有属性
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attr_agg = response.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
// 1、得到属性的id;
long attrId = bucket.getKeyAsNumber().longValue();
// 2、得到属性的名字
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
// 3、得到属性的所有值
List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
String keyAsString = item.getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVo.setAttrId(attrId);
attrVo.setAttrName(attrName);
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
// 3、当前所有品牌涉及到的所有属性
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
// 1、得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
// 2、得到品牌的名
String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
// 3、得到品牌的图片
String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
brandVo.setBrandId(brandId);
brandVo.setBrandName(brandName);
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
// 4、当前商品所涉及的分类信息
ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
for (Terms.Bucket bucket : buckets) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
// 得到分类id
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
// 得到分类名
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalog_name);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
//===========以上从聚合信息获取到=============
// 5、分页信息-页码
result.setPageNum(param.getPageNum());
// 6、分页信息-总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
// 7、分页信息-总页码-计算
int totalPages = total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int) total / EsConstant.PRODUCT_PAGESIZE : ((int) total / EsConstant.PRODUCT_PAGESIZE + 1);
result.setTotalPages(totalPages);
return result;
}
}
请求测试效果
请求代码
/**
*准备检索请求
* 模糊匹配,过滤(属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析。
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param){
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
/**
* 1、过滤(按照属性、分类、品牌、价格区间、库存)
*/
// 1、构建bool-query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
sourceBuilder.query(boolQuery);
// 1.1、must-模糊匹配、
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
// 1.2.1、filter-按照三级分类id查询
if (null != param.getCatalog3Id()) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
// 1.2.2、filter-按照品牌id查询
if (null != param.getBrandId() && param.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
// 1.2.3、filter-按照是否有库存进行查询
if (null != param.getHasStock()) { // true/false
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
}
// 1.2.4、filter-按照区间进行查询 1_500/_500/500_
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(param.getSkuPrice())) {
String[] prices = param.getSkuPrice().split("_");
if (prices.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
} else {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
} else if (prices.length == 2) {
// _6000会截取成["","6000"]
if (!prices[0].isEmpty()) {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
}
boolQuery.filter(rangeQueryBuilder);
}
// 1.2.5、filter-按照属性进行查询
List<String> attrs = param.getAttrs();
if (null != attrs && attrs.size() > 0) {
// attrs=1_5寸:8寸&2_16G:8G
attrs.forEach(attr -> {
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
String[] attrSplit = attr.split("_");
queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
String[] attrValues = attrSplit[1].split(":");
queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
// 每一个必须都得生成一个nested查询
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
boolQuery.filter(nestedQueryBuilder);
});
}
// 把以前所有的条件都拿来进行封装
sourceBuilder.query(boolQuery);
/**
* 2、排序,分页,高亮,
*/
// 2.1、排序 eg:sort=saleCount_desc/asc
if (!StringUtils.isEmpty(param.getSort())){
String[] s = param.getSort().split("_");
sourceBuilder.sort(s[0], s[1].equalsIgnoreCase("asc")? SortOrder.ASC : SortOrder.DESC);
}
// 2.2、分页
// 分页计算
// 第几页的第一个从:(当前页码-1)*页码数 开始
sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//2.3、高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
sourceBuilder.highlighter(highlightBuilder);
}
/**
* 聚合分析
*
*/
// 1.品牌聚合
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg");
brandAgg.field("brandId").size(50);
// 品牌聚合的子聚合
brandAgg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
brandAgg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
//TODO 品牌聚合
sourceBuilder.aggregation(brandAgg);
//2.分类聚合
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalogAgg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
//TODO 分类聚合
sourceBuilder.aggregation(catalogAgg);
// 3.属性聚合
NestedAggregationBuilder attrAgg = AggregationBuilders.nested("attr_agg", "attrs");
attrAgg.subAggregation(AggregationBuilders.terms("attr_id_agg").field("attrs.attrId"));
attrAgg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
attrAgg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
//TODO 属性聚合
sourceBuilder.aggregation(attrAgg);
String string = sourceBuilder.toString();
System.out.println("构建的DSL语句"+string);
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},sourceBuilder);
return searchRequest;
}
GET http://localhost:12000/list.html?keyword=%E5%8D%8E%E4%B8%BA&hasStock=0&attrs=5_4:A13&skuPrice=1200_
结果测试效果
6、页面效果
1、基本数据渲染
将商品的基本属性渲染出来
th:utext可以将字符串包含的html元素渲染出来
<div class="rig_tab">
<!-- 遍历各个商品-->
<div th:each="product : ${result.getProduct()}">
<div class="ico">
<i class="iconfont icon-weiguanzhu"></i>
<a href="/static/search/#">关注</a>
</div>
<p class="da">
<a th:href="|http://item.gulimall.com/${product.skuId}.html|" >
<!--图片 -->
<img class="dim" th:src="${product.skuImg}">
</a>
</p>
<ul class="tab_im">
<li><a href="/static/search/#" title="黑色">
<img th:src="${product.skuImg}"></a></li>
</ul>
<p class="tab_R">
<!-- 价格 -->
<span th:text="'¥' + ${product.skuPrice}">¥5199.00</span>
</p>
<p class="tab_JE">
<!-- 标题 -->
<!-- 使用utext标签,使检索时高亮不会被转义-->
<a href="/static/search/#" th:utext="${product.skuTitle}">
Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
</a>
</p>
<p class="tab_PI">已有<span>11万+</span>热门评价
<a href="/static/search/#">二手有售</a>
</p>
<p class="tab_CP"><a href="/static/search/#" title="谷粒商城Apple产品专营店">谷粒商城Apple产品...</a>
<a href='#' title="联系供应商进行咨询">
<img src="/static/search/img/xcxc.png">
</a>
</p>
<div class="tab_FO">
<div class="FO_one">
<p>自营
<span>谷粒商城自营,品质保证</span>
</p>
<p>满赠
<span>该商品参加满赠活动</span>
</p>
</div>
</div>
</div>
</div>
2、筛选条件渲染
将结果的品牌、分类、商品属性进行遍历显示,并且点击某个属性值时可以通过拼接url进行跳转。
a标签点击事件:${'javascript:searchProducts("brandId",'+brand.brandId+')'}
由于是在${}中操作,所以参数以引号的形式输入。
"代表"=>
<a href="#" th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
<div class="JD_nav_logo">
<!--品牌-->
<div class="JD_nav_wrap">
<div class="sl_key">
<span>品牌:</span>
</div>
<div class="sl_value">
<div class="sl_value_logo">
<ul>
<li th:each="brand: ${result.getBrands()}">
<!--替换url-->
<a href="#" th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
<img src="/static/search/img/598033b4nd6055897.jpg" alt="" th:src="${brand.brandImg}">
<div th:text="${brand.brandName}">
华为(HUAWEI)
</div>
</a>
</li>
</ul>
</div>
</div>
<div class="sl_ext">
<a href="#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
</a>
<a href="#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
<!--分类-->
<div class="JD_pre" th:each="catalog: ${result.getCatalogs()}">
<div class="sl_key">
<span>分类:</span>
</div>
<div class="sl_value">
<ul>
<li><a href="#" th:text="${catalog.getCatalogName()}" th:href="${'javascript:searchProducts("catalogId",'+catalog.catalogId+')'}">0-安卓(Android)</a></li>
</ul>
</div>
</div>
<!--价格-->
<div class="JD_pre">
<div class="sl_key">
<span>价格:</span>
</div>
<div class="sl_value">
<ul>
<li><a href="#">0-499</a></li>
<li><a href="#">500-999</a></li>
<li><a href="#">1000-1699</a></li>
<li><a href="#">1700-2799</a></li>
<li><a href="#">2800-4499</a></li>
<li><a href="#">4500-11999</a></li>
<li><a href="#">12000以上</a></li>
<li class="sl_value_li">
<input type="text">
<p>-</p>
<input type="text">
<a href="#">确定</a>
</li>
</ul>
</div>
</div>
<!--商品属性-->
<div class="JD_pre" th:each="attr: ${result.getAttrs()}" >
<div class="sl_key">
<span th:text="${attr.getAttrName()}">系统:</span>
</div>
<div class="sl_value">
<ul>
<li th:each="val: ${attr.getAttrValue()}">
<a href="#"
th:text="${val}"
th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+val+'")'}">0-安卓(Android)</a></li>
</ul>
</div>
</div>
</div>
3、点击某个属性值时可以通过拼接url进行跳转
分类、属性,品牌添加方法searchProducts
拼接参数字符串,存在?则直接拼接
没?则先加?在拼接其他参数
function searchProducts(name, value) {
//原來的页面
location.href = replaceParamVal(location.href,name,value,true)
};
/**
* @param url 目前的url
* @param paramName 需要替换的参数属性名
* @param replaceVal 需要替换的参数的新属性值
* @param forceAdd 该参数是否可以重复查询(attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏)
* @returns {string} 替换或添加后的url
*/
function replaceParamVal(url, paramName, replaceVal,forceAdd) {
var oUrl = url.toString();
var nUrl;
// 有paramName就拼接
if (oUrl.indexOf(paramName) != -1) {
if( forceAdd && oUrl.indexOf(paramName+"="+replaceVal)==-1) {
// 判断是否存在其他请求参数
if (oUrl.indexOf("?") != -1) {
// 有直接替换
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
// 没有加?在拼接
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
} else {
var re = eval('/(' + paramName + '=)([^&]*)/gi');
nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
}
} else {
// 没有paramName
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
}
return nUrl;
};
4、关键字检索查询
给search绑定id事件,然后调用searchProducts进行查询条件拼接。通过$("#keyword_input").val()获取值。
function searchByKeyword() {
searchProducts("keyword", $("#keyword_input").val());
}
5、分页数据渲染
将页码绑定至属性pn,当点击某页码时,通过获取pn值进行url拼接跳转页面
判断条件:
${result.pageNum>1}
<div class="filter_page">
<div class="page_wrap">
<span class="page_span1">
<!-- 不是第一页时显示上一页 -->
<a class="page_a" href="#" th:if="${result.pageNum>1}" th:attr="pn=${result.getPageNum()-1}">
< 上一页
</a>
<!-- 将各个页码遍历显示,并将当前页码绑定至属性pn -->
<a href="#" class="page_a"
th:each="page: ${result.pageNavs}"
th:text="${page}"
th:style="${page==result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
th:attr="pn=${page}"
>1</a>
<!-- 不是最后一页时显示下一页 -->
<a href="#" class="page_a" th:if="${result.pageNum<result.totalPages}" th:attr="pn=${result.getPageNum()+1}">
下一页 >
</a>
</span>
<span class="page_span2">
<em>共<b th:text="${result.totalPages}">169</b>页 到第</em>
<input type="number" value="1" class="page_input">
<em>页</em>
<a href="#">确定</a>
</span>
</div>
</div>
$(".page_a").click(function () {
var pn=$(this).attr("pn");
location.href=replaceParamVal(location.href,"pageNum",pn,false);
console.log(replaceParamVal(location.href,"pageNum",pn,false))
})
6、页面排序和价格区间
页面排序功能需要保证,点击某个按钮时,样式会变红,并且其他的样式保持最初的样子;
点击某个排序时首先按升序显示,再次点击再变为降序,并且还会显示上升或下降箭头
页面排序跳转的思路是通过点击某个按钮时会向其class属性添加/去除desc,并根据属性值进行url拼接
<div class="filter_top">
<div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}">
<!-- 通过判断当前class是否有desc来进行样式的渲染和箭头的显示-->
<a sort="hotScore"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
#strings.endsWith(p,'desc')) ?'↓':'↑' }]]</a>
<a sort="saleCount"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
#strings.endsWith(p,'desc'))?'↓':'↑' }]]</a>
<a sort="skuPrice"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
#strings.endsWith(p,'desc'))?'↓':'↑' }]]</a>
<a sort="hotScore" class="sort_a">评论分</a>
<a sort="hotScore" class="sort_a">上架时间</a>
<!--价格区间搜索-->
<input id="skuPriceFrom" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
style="width: 100px; margin-left: 30px">
-
<input id="skuPriceTo" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
style="width: 100px">
<button id="skuPriceSearchBtn">确定</button>
</div>
<div class="filter_top_right">
<span class="fp-text">
<b>1</b><em>/</em><i>169</i>
</span>
<a href="#" class="prev"><</a>
<a href="#" class="next"> > </a>
</div>
</div>
6.1)、综合排序
// return false禁用默认行为
$(".sort_a").click(function () {
//添加、剔除desc
$(this).toggleClass("desc"); // 加上就是降
//获取sort属性值并进行url跳转
let sort = $(this).attr("sort");
sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
location.href = replaceAndAddParamVal(location.href, "sort", sort,false);
// return false禁用默认行为
return false;
});
6.2)、价格区间搜索函数
$("#skuPriceSearchBtn").click(function () {
var skuPriceFrom = $("#skuPriceFrom").val();
var skuPriceTo = $("#skuPriceTo").val();
location.href = replaceAndAddParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
})
7、是否有库存
<li>
<a href="#" th:with="check = ${param.hasStock}">
<input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')}">
仅显示有货
</a>
</li>
$("#showHasStock").change(function () {
if ($(this).prop('checked')) {
location.href = replaceAndAddParamVal(location.href, "hasStock", 1, false);
} else {
//没选中
var re = eval('/(hasStock=)([^&]*)/gi');
location.href = (location.href + "").replace(re, '');
}
return false;
})
7、面包屑导航
1、修改gulimall-search的pom
定义springcloud的版本
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
</properties>
添加依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、主启动类添加 开启远程调用
3、封装面包屑导航数据
修改com.atguigu.gulimall.search.feign.ProducteFeignService类,代码如下
package com.atguigu.gulimall.search.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("gulimall-product")
public interface ProducteFeignService {
@GetMapping("/product/attr/info/{attrId}")
public R attrInfo(@PathVariable("attrId") Long attrId);
}
修改com.atguigu.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;
}
新增“com.atguigu.gulimall.search.vo.AttrResponseVo” 类,代码如下:
package com.atguigu.gulimall.search.vo;
import lombok.Data;
@Data
public class AttrResponseVo {
/**
* 属性id
*/
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 属性图标
*/
private String icon;
/**
* 可选值列表[用逗号分隔]
*/
private String valueSelect;
/**
* 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
*/
private Integer attrType;
/**
* 启用状态[0 - 禁用,1 - 启用]
*/
private Long enable;
/**
* 所属分类
*/
private Long catelogId;
/**
* 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
*/
private Integer showDesc;
private Long attrGroupId;
private String catelogName;
private String groupName;
private Long[] catelogPath;
}
修改“com.atguigu.gulimall.search.vo.SearchResult” 类,代码如下:
package com.atguigu.gulimall.search.vo;
import com.atguigu.common.to.es.SkuEsModel;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class SearchResult {
/**
* 查询到的商品信息
*/
private List<SkuEsModel> products;
private Integer pageNum;//当前页码
private Long total;//总记录数
private Integer totalPages;//总页码
private List<Integer> pageNavs;//导航页码
private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
//=====================以上是返给页面的信息==========================
//面包屑导航数据
private List<NavVo> navs = new ArrayList<>();
@Data
public static class NavVo{
private String navName;
private String navValue;
private String link;
}
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
private String brandImg;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
修改“com.atguigu.gulimall.search.vo.SearchParam” 类,代码如下:
修改“com.atguigu.gulimall.search.controller.SearchController” 类,代码如下:
@GetMapping("/list.html")
public String listPage(SearchParam searchParam, Model model, HttpServletRequest request) {
String queryString = request.getQueryString();
searchParam.set_queryString(queryString);
SearchResult result = mallSearchService.search(searchParam);
System.out.println("====================" + result);
model.addAttribute("result", result);
return "list";
}
修改“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl” 类,代码如下:
在封装结果时,将查询的属性值进行封装
// 8、构建面包屑导航功能
List<String> attrs = param.getAttrs();
if (attrs != null && attrs.size() > 0) {
List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
String[] split = attr.split("_");
SearchResult.NavVo navVo = new SearchResult.NavVo();
// 8.1 设置属性值
navVo.setNavValue(split[1]);
// 8.2 查询并设置属性名
try {
R r = producteFeignService.attrInfo(Long.parseLong(split[0]));
if (r.getCode() == 0) {
AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
}else {
navVo.setNavName(split[0]);
}
} catch (Exception e) {
}
// 8.3 设置面包屑跳转链接(当点击该链接时剔除点击属性)
// 取消了这个面包屑以后,我们就要跳转到那个地方,将请求地址的url里面的当前置空
// 拿到所有的查询条件,去掉当前
String queryString = param.get_queryString();
String encode = null;
try {
encode = URLEncoder.encode(attr, "UTF-8");
encode = encode.replace("+","20%");//浏览器对空格编码和java不一样
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String replace = queryString.replace("&attrs=" + encode, "");
navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
return result;
因为远程调用可以给他添加缓存
修改“com.atguigu.gulimall.product.service.impl.AttrServiceImpl”类,代码如下:
页面渲染
<div class="JD_ipone_one c">
<!-- 遍历面包屑功能 -->
<a th:href="${nav.link}" th:each="nav:${result.navs}"><span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> x</a>
</div>
5、 条件筛选联动
就是将品牌和分类也封装进面包屑数据中,并且在页面进行th:if的判断,当url有该属性的查询条件时就不进行显示了
修改“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl”类,代码如下:
// 8、构建面包屑导航功能
// 8.1、属性
List<String> attrs = param.getAttrs();
if (attrs != null && attrs.size() > 0) {
List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
String[] split = attr.split("_");
SearchResult.NavVo navVo = new SearchResult.NavVo();
// 8.1.1、设置属性值
navVo.setNavValue(split[1]);
// 8.1.2、查询并设置属性名
try {
R r = producteFeignService.attrInfo(Long.parseLong(split[0]));
result.getAttrIds().add(Long.valueOf(split[0]));
if (r.getCode() == 0) {
AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
} else {
navVo.setNavName(split[0]);
}
} catch (Exception e) {
}
// 8.1.3、设置面包屑跳转链接(当点击该链接时剔除点击属性)
// 取消了这个面包屑以后,我们就要跳转到那个地方,将请求地址的url里面的当前置空
// 拿到所有的查询条件,去掉当前
String replace = replaceQueryString(param, attr, "attrs");
navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty() ? "" : "?" + replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
// 8.2、品牌
if (null != param.getBrandId() && param.getBrandId().size() > 0) {
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌");
// 远程查询所有品牌
R r = producteFeignService.brandInfo(param.getBrandId());
if (0 == r.getCode()) {
List<BrandVo> brands = r.getData("brands", new TypeReference<List<BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (BrandVo brand : brands) {
buffer.append(brand.getName() + ";");
replace = replaceQueryString(param, brand.getBrandId() + "", "brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty() ? "" : "?" + replace));
}
navs.add(navVo);
}
// 8.3、分类,不需要导航取消
return result;
}
/**
* 替换原生的所有的查询条件
*
* @param param
* @param value
* @param key
* @return
*/
private String replaceQueryString(SearchParam param, String value, String key) {
String queryString = param.get_queryString();
String encode = null;
try {
encode = URLEncoder.encode(value, "UTF-8");
encode = encode.replace("+", "20%");//浏览器对空格编码和java不一样
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return queryString.replace("&" + key + "=" + encode, "");
}
attrIds作为记录url已经有的属性id
远程调用获取品牌信息
修改“com.atguigu.gulimall.search.feign.ProducteFeignService”类,代码如下:
@RequestMapping("/product/brand/infos")
R brandInfo(@RequestParam("brandIds") List<Long> brandIds);
修改“com.atguigu.gulimall.search.vo.BrandVo”类,代码如下:
修改“com.atguigu.gulimall.product.app.BrandController”类,代码如下:
/**
* 根据品牌id查询品牌信息
*/
@RequestMapping("/infos")
public R info(@RequestParam("brandIds") List<Long> brandIds){
List<BrandEntity> brands = brandService.getBrandsById(brandIds);
return R.ok().put("brands", brands);
}
修改“com.atguigu.gulimall.product.service.BrandService”类,代码如下:
List getBrandsById(List brandIds);
修改“com.atguigu.gulimall.product.service.impl.BrandServiceImpl”类,代码如下:
@Override
public List<BrandEntity> getBrandsById(List<Long> brandIds) {
List<BrandEntity> brandId = baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id", brandIds));
return brandId;
}
修改gulimall-search的list.html
<!--品牌-->
<div th:if="${#strings.isEmpty(brandid)}" class="JD_nav_wrap">
<div class="sl_key">
<span><b>品牌:</b></span>
</div>
<div class="sl_value">
<div class="sl_value_logo">
<ul>
<li th:each="brand:${result.brands}">
<a href="/static/search/#"
th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
<img th:src="${brand.brandImg}" alt="">
<div th:text="${brand.brandName}">
华为(HUAWEI)
</div>
</a>
</li>
</ul>
</div>
</div>
<div class="sl_ext">
<a href="/static/search/#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
</a>
<a href="/static/search/#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
<!--分类-->
<div class="JD_nav_wrap">
<div class="sl_key">
<span><b>分类:</b></span>
</div>
<div class="sl_value">
<ul>
<li th:each="catalog:${result.catalogs}">
<a href="/static/search/#"
th:href="${'javascript:searchProducts("catalog3",'+catalog.catalogId+')'}"
th:text="${catalog.catalogName}">5.56英寸及以上</a>
</li>
</ul>
</div>
<div class="sl_ext">
<a href="/static/search/#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
</a>
<a href="/static/search/#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
<!--其他所有需要展示的属性-->
<div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">
<div class="sl_key">
<span th:text="${attr.attrName}">屏幕尺寸:</span>
</div>
<div class="sl_value">
<ul>
<li th:each="val:${attr.attrValue}">
<a href="/static/search/#"
th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+val+'")'}"
th:text="${val}">5.56英寸及以上</a></li>
</ul>
</div>
</div>