第9章-全文检索(下).md 58 KB

第9章-全文检索-站内搜索(下)

学习目标

  • 基于ES完成电商搜索列表(重点)
  • 完成搜索页面渲染

1、 利用es开发电商的搜索列表功能

img

商品检索流程:

img

1.1 搜索结果预期数据展示

利用DSL 语句查询es 数据-看懂即可

#不仅要给用户响应检索到业务数据,为了提高检索商品用户体验,还需要根据响应业务数据,进行聚合得到过滤项条件:品牌,其他的平台属性
#在前面基础上 增加聚合(类似于MySQL中分组Group By)
#聚合三个要素:  1.聚合名称-获取聚合结果用到  2.聚合字段   3.聚合类型(如何聚合)
GET goods/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": {
              "query": "手机",
              "operator": "and"
            }
          }
        }
      ],
      "filter": [
        {
          "term": {
            "tmId": 3
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "23"
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "8G"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "117"
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "骁龙8 Gen2"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "_source": {
    "includes": [
      "id",
      "title",
      "defaultImg",
      "price"
    ]
  },
  "from": 0,
  "size": 3,
  "highlight": {
    "pre_tags": "<font style='color:red'>",
    "post_tags": "</font>",
    "fields": {
      "title": {}
    }
  },
  "sort": [
    {
      "createTime": {
        "order": "desc"
      }
    },
    {
      "price": {
        "order": "desc"
      }
    }
  ],
  "aggs": {
    "tmIdAgg": {
      "terms": {
        "field": "tmId",
        "size": 10
      },
      "aggs": {
        "tmNameAgg": {
          "terms": {
            "field": "tmName",
            "size": 10
          }
        },
        "tmLogoAgg": {
          "terms": {
            "field": "tmLogoUrl",
            "size": 10
          }
        }
      }
    },
    "attrsAgg": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attrsIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrsNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attrsValueAgg": {
              "terms": {
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

1.2 封装搜索相关实体对象

YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/731

1.2.1 请求参数对象

搜索参数实体:SearchParam

package com.atguigu.gmall.list.model;

import lombok.Data;

// 封装查询条件
// ?category3Id=61&trademark=2:华为&props=23:4G:运行内存&order=1:desc
@Data
public class SearchParam {

    private Long category1Id;;//三级分类id
    private Long category2Id;
    private Long category3Id;
    // trademark=2:华为
    private String trademark;//品牌

    private String keyword;//检索的关键字

    // 排序规则
    // 1:hotScore 2:price
    private String order = ""; // 1:综合排序/热度  2:价格

    // props=23:4G:运行内存
    //平台属性Id 平台属性值名称 平台属性名
    private String[] props;//页面提交的数组

    private Integer pageNo = 1;//分页信息
    private Integer pageSize = 3; // 每页默认显示的条数


}

1.2.2 响应结果对象

检索结果:SearchResponseVo

package com.atguigu.gmall.list.model;

import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// 总的数据
@Data
public class SearchResponseVo implements Serializable {

    //品牌 此时vo对象中的id字段保留(不用写) name就是“品牌” value: [{id:100,name:华为,logo:xxx},{id:101,name:小米,log:yyy}]
    private List<SearchResponseTmVo> trademarkList;
    //所有商品的顶头显示的筛选属性
    private List<SearchResponseAttrVo> attrsList = new ArrayList<>();

    //检索出来的商品信息
    private List<Goods> goodsList = new ArrayList<>();

    private Long total;//总记录数
    private Integer pageSize;//每页显示的内容
    private Integer pageNo;//当前页面
    private Long totalPages;

}

结果集品牌实体:SearchResponseTmVo

package com.atguigu.gmall.list.model;

import lombok.Data;

import java.io.Serializable;

// 品牌数据
@Data
public class SearchResponseTmVo implements Serializable {
    //当前属性值的所有值
    private Long tmId;
    //属性名称
    private String tmName;//网络制式,分类
    //图片名称
    private String tmLogoUrl;//网络制式,分类
}

结果集平台属性实体:SearchResponseAttrVo

package com.atguigu.gmall.list.model;

import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// 平台属性相关对象
@Data
public class SearchResponseAttrVo implements Serializable {

    // 平台属性Id
    private Long attrId;//1
    //当前属性值的集合
    private List<String> attrValueList = new ArrayList<>();
    //属性名称
    private String attrName;//网络制式,分类
}

1.3 控制器ListApiController

/**
 * 商品检索,包含:分页检索商品信息,动态聚合过滤条件。支持多条件过滤
 * @param searchParam
 * @return
 */
@PostMapping({"", "/"})
public Result search(@RequestBody SearchParam searchParam) {
    SearchResponseVo vo = searchService.search(searchParam);
    return Result.ok(vo);
}

1.4 搜索业务接口

package com.atguigu.gmall.list.service;


import com.atguigu.gmall.list.model.SearchParam;
import com.atguigu.gmall.list.model.SearchResponseVo;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;

public interface SearchService {

    /**
     * 商品检索,包含:分页检索商品信息,动态聚合过滤条件。支持多条件过滤
     * @param searchParam
     * @return
     */
    SearchResponseVo search(SearchParam searchParam);

    /**
     * 构建用于检索请求对象,封装请求体参数
     * @param searchParam 查询条件
     * @return
     */
    SearchRequest buildDSL(SearchParam searchParam);


    /**
     * 对ES响应JSON结果进行解析,封装为响应VO对象
     *
     * @param response
     * @param searchParam
     * @return
     */
    SearchResponseVo parseResult(SearchResponse response, SearchParam searchParam);
}

1.5 搜索业务实现类

SearchServiceImpl

package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.*;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-08-04 09:06
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {


    /**
     * 商品索引库名称
     */
    private static final String INDEX_NAME = "goods";

    @Autowired
    private ProductFeignClient productFeignClient;

    //默认链路追踪包含线程池对象
    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;


    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 将来真正实现索引库导入
     * 将指定商品SKU封装商品文档对象,保存到ES索引库
     * 封装Goods对象为相关属性赋值,将Goods存入ES
     *
     * @param skuId
     * @return
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.创建索引库文档对象 Goods
            Goods goods = new Goods();
            //1.1 远程调用商品服务获取商品SKU基本信息(可能读Redis缓存)-封装Goods中商品基本信息
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo != null) {
                    goods.setId(skuId);
                    goods.setTitle(skuInfo.getSkuName());
                    goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                    goods.setCreateTime(skuInfo.getCreateTime());
                    goods.setCreatedDate(skuInfo.getCreateTime());
                }
                return skuInfo;
            }, threadPoolExecutor);

            CompletableFuture<Void> priceCOmCompletableFuture = CompletableFuture.runAsync(() -> {
                BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
                if (skuPrice != null) {
                    goods.setPrice(skuPrice.doubleValue());
                }
            }, threadPoolExecutor);

            //1.2 远程调用商品服务获取品牌信息-封装Goods中品牌信息
            CompletableFuture<Void> baseTrademartCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            }, threadPoolExecutor);

            //1.3 远程调用商品服务获取分类信息-封装Goods中分类信息
            CompletableFuture<Void> categoryViewCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());
                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());
                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            }, threadPoolExecutor);

            //1.4 远程调用商品服务获取平台属性列表-封装Goods中平台属性信息
            CompletableFuture<Void> attrListCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    //将BaseAttrInfo集合转为SearchAttr集合
                    List<SearchAttr> searchAttrList = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrList);
                }
            }, threadPoolExecutor);

            //1.5 汇总所有异步任务执行结果
            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCOmCompletableFuture,
                    categoryViewCompletableFuture,
                    baseTrademartCompletableFuture,
                    attrListCompletableFuture
            ).join();

            //2.将Goods保存到索引库-查询ES提供JavaClient调用SDK方法完成导入
            //2.1 构建创建文档请求对象 封装索引库 // PUT /<index>/_doc/<_id> = PUT goods/_doc/24
            IndexRequest indexRequest = new IndexRequest(INDEX_NAME).id(skuId.toString());
            //2.2 为请求对象设置请求体参数(文档对象JSON)

            String goodsJSON = JSON.toJSONString(goods);
            indexRequest.source(goodsJSON, XContentType.JSON);

            //2.3 执行保存索引库
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("[搜索服务]新增商品文档异常:{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 测试接口:将指定商品下架
     *
     * @param skuId
     * @return
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            //1.构建删除文档请求对象
            DeleteRequest request = new DeleteRequest(
                    INDEX_NAME,
                    skuId.toString());
            //2.执行删除
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]下架商品异常:{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 对指定商品分值(热门商品)进行更新
     *
     * @param skuId
     * @param score
     */
    @Override
    public void incrHotScore(Long skuId, int score) {
        try {
            //1.对存放到Redis中商品热榜进行修改分值
            String hotkey = "hot:goods:score";
            Double newScore = redisTemplate.opsForZSet().incrementScore(hotkey, skuId.toString(), score);
            //2.对ES中分值进行修改(对ES写操作进行“稀释”)
            if (newScore % 10 == 0) {
                //2.1 构建修改文档请求对象
                UpdateRequest updateRequest = new UpdateRequest(INDEX_NAME, skuId.toString());
                //2.2 修改内容
                Goods goods = new Goods();
                goods.setId(skuId);
                goods.setHotScore(newScore.longValue());
                updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);

                //2.3 执行ES文档修改操作
                restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
            }
        } catch (Exception e) {
            log.error("[搜索服务],修改商品分值异常:{}", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * 商品检索,包含:分页检索商品信息,动态聚合过滤条件。支持多条件过滤
     * 检索功能三大步骤:
     * 1.构建检索请求对象-包含请求体参数
     * 2.发送检索请求到ES-ES处理接口响应结果
     * 3.接收ES响应结果,按照接口文档进行解析
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        try {
            //一、构建检索请求对象SearchRequest-封装检索请求以及请求体参数
            SearchRequest searchRequest = this.buildDSL(searchParam);

            //二、调用SDK执行检索
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            //三、解析响应结果
            return this.parseResult(response, searchParam);
        } catch (Exception e) {
            log.error("[搜索服务]检索商品异常:{}", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * 构建检索请求对象(构建发起请求DSL语句)封装请求路径以及请求参数
     *
     * @param searchParam 查询条件
     * @return
     */
    @Override
    public SearchRequest buildDSL(SearchParam searchParam) {
        //1.创建检索请求对象SearchReqeust 封装请求路径,请求方式,要检索索引库  GET goods/_search
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        //2.创建请求体参数对象SearchSourceBuilder 封装请求体所有请求参数 包括:查询方式、字段指定、分页、高亮、排序、聚合等
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //2.1 封装“查询方式”设置请求体参数中"query"属性 包含用户填写,选择的各种查询条件
        //2.1.1 创建封装所有查询条件布尔条件对象
        BoolQueryBuilder allBoolQueryBuilder = QueryBuilders.boolQuery();

        //2.1.2 在布尔条件对象中封装must必须满足条件:关键字匹配/全文检索查询
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            allBoolQueryBuilder.must(QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND));
        }
        //2.1.3 在布尔条件对象中封装filter各种过滤条件
        //2.1.3.1 在filter中设置品牌过滤 品牌过滤提交trademark参数值---3:华为   品牌ID:品牌名称
        if (StringUtils.isNotBlank(searchParam.getTrademark())) {
            String[] split = searchParam.getTrademark().split(":");
            if (split != null && split.length == 2) {
                allBoolQueryBuilder.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }
        //2.1.3.2 在filter中设置分类过滤 可能存在1,2,3级分类ID
        if (searchParam.getCategory1Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", searchParam.getCategory1Id()));
        }
        if (searchParam.getCategory2Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", searchParam.getCategory2Id()));
        }
        if (searchParam.getCategory3Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", searchParam.getCategory3Id()));
        }
        //2.1.3.3 在filter中设置平台属性过滤 可能存在多个平台属性过滤条件 参数值形式  平台属性ID:属性值名称:属性名称
        if (searchParam.getProps() != null && searchParam.getProps().length > 0) {
            // 循环处理每组平台属性过滤条件-每处理一个设置nested平台属性过滤
            for (String prop : searchParam.getProps()) {
                //2.1.4 创建平台属性nested查询对象
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    //2.1.5 创建一组平台属性过滤条件:构建布尔查询
                    BoolQueryBuilder attrsBoolQueryBuilder = QueryBuilders.boolQuery();
                    //2.1.5.1 在平台属性布尔查询中设置平台属性ID条件
                    attrsBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    //2.1.5.2 在平台属性布尔查询中设置平台属性值条件
                    attrsBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    NestedQueryBuilder attrsNestedQueryBuilder = QueryBuilders.nestedQuery("attrs", attrsBoolQueryBuilder, ScoreMode.None);
                    //2.1.6 将平台属性过滤条件nested加入到最大的布尔查询条件中
                    allBoolQueryBuilder.filter(attrsNestedQueryBuilder);
                }
            }
        }
        searchSourceBuilder.query(allBoolQueryBuilder);

        //2.2 封装“分页”条件,设置请求体参数中"from","size"属性 包含:起始位置索引、页大小
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        int from = (pageNo - 1) * pageSize;
        searchSourceBuilder.from(from).size(pageSize);

        //2.3 封装“指定响应字段列表”,设置请求体参数中"_source"属性 包含:ES响应业务参数字段
        searchSourceBuilder.fetchSource(new String[]{"id", "title", "defaultImg", "price"}, null);

        //2.4 封装“高亮显示关键字”,设置请求体参数中"highlight"属性 包含:高亮字段、高亮HTML标签
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.preTags("<font style='color:red'>");
            highlightBuilder.postTags("</font>");
            highlightBuilder.field("title");
            searchSourceBuilder.highlighter(highlightBuilder);
        }

        //2.5 封装“排序字段”,设置请求体参数中"sort"属性 包含:排序字段、排序方式 默认:按照相关度得分排序
        if (StringUtils.isNotBlank(searchParam.getOrder())) {
            String orderField = "";
            String[] split = searchParam.getOrder().split(":");
            if (split != null && split.length == 2) {
                if ("1".equals(split[0])) {
                    orderField = "hotScore";
                }
                if ("2".equals(split[0])) {
                    orderField = "price";
                }
            }
            searchSourceBuilder.sort(orderField, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);
        }

        //2.6 封装“聚合字段”,设置请求体参数中"aggs"属性 包含:对品牌、平台属性聚合 每个聚合包含:聚合名称、聚合字段、聚合类型
        //2.6.1 设置品牌聚合
        //2.6.1.1 创建品牌ID聚合对象 聚合三要素:聚合名称:tmIdAgg .term()方法聚合方式:terms  .field()方法 聚合字段:tmId
        TermsAggregationBuilder tmIdAgg = AggregationBuilders.terms("tmIdAgg").field("tmId");
        //2.6.1.2 基于品牌ID聚合对象创建 品牌名称 子聚合
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"));
        //2.6.1.3 基于品牌ID聚合对象创建 品牌Logo 子聚合
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmLogoAgg").field("tmLogoUrl"));
        searchSourceBuilder.aggregation(tmIdAgg);

        //2.6.2 设置平台属性聚合 关键:attrs字段类型nested
        //2.6.2.1.创建nested平台属性聚合对象
        NestedAggregationBuilder attrsNested = AggregationBuilders.nested("attrsAgg", "attrs");
        //2.6.2.2 创建平台属性id聚合对象-放入到平台属性聚合对象中
        TermsAggregationBuilder attrsIdAgg = AggregationBuilders.terms("attrsIdAgg").field("attrs.attrId");
        //2.6.2.3 基于平台属性id聚合创建 平台属性名称 子聚合对象
        attrsIdAgg.subAggregation(AggregationBuilders.terms("attrsNameAgg").field("attrs.attrName"));
        //2.6.2.4 基于平台属性id聚合创建 平台属性值 子聚合对象
        attrsIdAgg.subAggregation(AggregationBuilders.terms("attrsValueAgg").field("attrs.attrValue"));
        //平台属性id聚合对象 放入到平台属性聚合对象中
        attrsNested.subAggregation(attrsIdAgg);
        searchSourceBuilder.aggregation(attrsNested);

        //3.将请求体参数封装到请求对象中
        System.out.println("本次检索DSL语句中请求体参数:");
        System.err.println(searchSourceBuilder.toString());
        return searchRequest.source(searchSourceBuilder);
    }


    /**
     * 解析结果:将ES响应结果按照接口文档(前端)要求响应VO对象
     *
     * @param response
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo parseResult(SearchResponse response, SearchParam searchParam) {
        SearchResponseVo vo = new SearchResponseVo();

        //1.封装vo中的分页信息
        //1.1 从入参中获取页码、页大小
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        vo.setPageNo(pageNo);
        vo.setPageSize(pageSize);
        //1.2 从ES响应结果中获取总记录数
        long total = response.getHits().getTotalHits().value;
        vo.setTotal(total);
        //1.3 通过总记录数、页大小 计算总页数
        long totalPage = total % pageSize == 0 ? (total / pageSize) : (total / pageSize) + 1;
        vo.setTotalPages(totalPage);

        //2.封装vo中的当前页检索到商品列表(处理高亮字段)
        SearchHits hits = response.getHits();
        SearchHit[] searchHits = hits.getHits();
        if (searchHits != null && searchHits.length > 0) {
            // 2.1 将数组转为集合List
            List<SearchHit> searchHitsList = Arrays.asList(searchHits);
            // 2.2 遍历命中业务数据 将业务数据JSON转为Java对象
            List<Goods> goodsList = searchHitsList.stream().map(searchHit -> {
                String goodsSourceJSON = searchHit.getSourceAsString();
                Goods goods = JSON.parseObject(goodsSourceJSON, Goods.class);
                // 2.3 处理高亮
                Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
                if (!CollectionUtils.isEmpty(highlightFields)) {
                    // 2.3.1 获取高亮对象
                    HighlightField highlightField = highlightFields.get("title");
                    // 2.3.2 获取高亮片段(字符串)
                    String titleHightLight = highlightField.getFragments()[0].toString();
                    goods.setTitle(titleHightLight);
                }
                return goods;
            }).collect(Collectors.toList());
            vo.setGoodsList(goodsList);
        }

        //3.0 获取结果中所有聚合-将聚合结果转为Map
        Map<String, Aggregation> allAggregationMap = response.getAggregations().asMap();
        //3.封装vo中聚合到品牌列表集合
        //问题:Aggregation接口没有提供获取聚合结果“桶”方法
        //解决:Aggregation->MultiBucketsAggregation->Terms->ParsedTerms->ParsedStringTerms/ParsedLongTerms
        //根据当时设置聚合字段类型决定选择哪个实现类
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) allAggregationMap.get("tmIdAgg");
        List<? extends Terms.Bucket> buckets = tmIdAgg.getBuckets();
        if (!CollectionUtils.isEmpty(buckets)) {
            List<SearchResponseTmVo> tmVoList = buckets.stream().map(bucket -> {
                //3.1 遍历聚合到品牌桶,得到当前品牌ID
                long tmId = bucket.getKeyAsNumber().longValue();
                //3.2 获取品牌名称子聚合结果对象,得到品牌名称(该名称结果一定只有一个)
                ParsedStringTerms tmNameAgg = bucket.getAggregations().get("tmNameAgg");
                String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();

                //3.3 获取品牌Logo子聚合结果对象,得到品牌Logo(品牌Logo一定只有一个),
                ParsedStringTerms tmLogoAgg = bucket.getAggregations().get("tmLogoAgg");
                String tmLogoUrl = tmLogoAgg.getBuckets().get(0).getKeyAsString();

                //3.3 封装为SearchResponseTmVo对象
                SearchResponseTmVo tmVo = new SearchResponseTmVo();
                tmVo.setTmId(tmId);
                tmVo.setTmName(tmName);
                tmVo.setTmLogoUrl(tmLogoUrl);
                return tmVo;
            }).collect(Collectors.toList());
            vo.setTrademarkList(tmVoList);
        }

        //4.封装vo中聚合到平台属性列表集合
        //4.1 获取平台属性nested聚合结果对象
        ParsedNested attrsAgg = (ParsedNested) allAggregationMap.get("attrsAgg");
        //4.2 基于平台属性nested聚合结果对象获取平台属性ID子聚合结果
        ParsedLongTerms attrsIdAgg = attrsAgg.getAggregations().get("attrsIdAgg");
        List<? extends Terms.Bucket> attrIdBuckets = attrsIdAgg.getBuckets();
        if(!CollectionUtils.isEmpty(attrIdBuckets)){
            List<SearchResponseAttrVo> attrVoList = attrIdBuckets.stream().map(bucket -> {
                //获取聚合到平台属性ID
                long attrId = bucket.getKeyAsNumber().longValue();
                //4.2.1 基于平台属性ID聚合结果,获取平台属性名称子聚合结果
                ParsedStringTerms attrsNameAgg = bucket.getAggregations().get("attrsNameAgg");
                String attrName = attrsNameAgg.getBuckets().get(0).getKeyAsString();

                //4.2.2 基于平台属性ID聚合结果,获取平台属性值子聚合结果
                ParsedStringTerms attrsValueAgg = bucket.getAggregations().get("attrsValueAgg");
                List<String> attrValueList = attrsValueAgg.getBuckets()
                        .stream().map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());

                //4.2.3 封装平台属性VO对象
                SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
                attrVo.setAttrId(attrId);
                attrVo.setAttrName(attrName);
                attrVo.setAttrValueList(attrValueList);
                return attrVo;
            }).collect(Collectors.toList());
            vo.setAttrsList(attrVoList);
        }
        return vo;
    }
}

说明http://localhost:8203/doc.html,左上角下拉框选择 webApi进行测试

测试参数:

#根据分类检索
{
  "category3Id": 61
}
#根据分类,关键字检索
{
  "keyword":"手机"
  "category3Id": 61
}
#根据分类品牌关键字检索
{
  "keyword":"手机",
  "category3Id": 61,
  "trademark":"2:苹果"
}
#加入排序
{
  "keyword":"手机",
  "category3Id": 61,
  "trademark":"2:苹果",
  "order":"2:desc"
}
#加入平台属性过滤条件 tips:每个同学平台属性不用 
{
	"keyword": "手机",
	"pageNo":1,
    "category3Id": 61,
	"trademark": "3:华为",
	"props": ["23:8G:运行内存","111:6.0英寸以下:屏幕尺寸"]
}

image-20221206111530729

2、在service-list-client模块添加接口

service-list-client模块ListFeignClient远程调用Feign接口中增加方法

package com.atguigu.gmall.list.client;


import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.impl.ListDegradeFeignClient;
import com.atguigu.gmall.list.model.SearchParam;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Map;

@FeignClient(value = "service-list", fallback = ListDegradeFeignClient.class)
public interface ListFeignClient {

    /**
     * 提供给详情服务调用:更新商品热度分值
     * @param skuId
     */
    @GetMapping("/api/list/inner/incrHotScore/{skuId}")
    public void incrHotScore(@PathVariable("skuId") Long skuId);


    /**
     * 提供给前端服务/移动端,商品检索
     *
     * @param searchParam
     * @return
     */
    @PostMapping({"/api/list"})
    public Result<Map> search(@RequestBody SearchParam searchParam);
}

远程调用接口的服务降级类

package com.atguigu.gmall.list.client.impl;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.ListFeignClient;
import com.atguigu.gmall.list.model.SearchParam;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2023-03-03 14:36
 */
@Component
public class ListDegradeFeignClient implements ListFeignClient {
    @Override
    public void incrHotScore(Long skuId) {

    }

    @Override
    public Result search(SearchParam searchParam) {
        return null;
    }
}

3、修改web-all模块

3.1 修改pom.xml文件

web-all模块中的pom.xml中增加依赖

<dependency>
    <groupId>com.atguigu.gmall</groupId>
    <artifactId>service-list-client</artifactId>
    <version>1.0</version>
</dependency>

3.2 在ListController控制器调用接口

web-all模块中处理前端搜索请求

package com.atguigu.gmall.all.controller;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.ListFeignClient;
import com.atguigu.gmall.list.model.SearchParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Map;

/**
 * <p>
 * 产品列表接口
 * </p>
 *
 */
@Controller
public class ListController {

    @Autowired
    private ListFeignClient listFeignClient;

    /**
     * 门户页面中商品检索页面渲染
     *
     * @param searchParam
     * @param model
     * @return
     */
    @GetMapping("/list.html")
    public String search(SearchParam searchParam, Model model) {
        Result<Map> result = listFeignClient.list(searchParam);
        model.addAllAttributes(result.getData());
        return "list/index";
    }
}

3.3 配置网关

gmall-gateway 网关模块,增加web-all服务的动态路由,需要在Nacos配置列表中,对server-gateway-dev.yaml配置进行编辑增加以下信息

#==================web-all服务==========================
- id: web-comment
uri: lb://web-all
predicates:
- Host=comment.gmall.com

3.4 页面渲染

列表显示

<li class="yui3-u-1-5" th:each="goods: ${goodsList}">
    <div class="list-wrap">
        <div class="p-img">
            <a th:href="@{http://item.gmall.com/{id}.html(id=${goods.id})}" target="_blank"><img th:src="${goods.defaultImg}"/></a>
        </div>
        <div class="price">
            <strong>
                <em>¥</em>
                <i th:text="${goods.price}">6088.00</i>
            </strong>
        </div>
        <div class="attr">
            <a th:href="@{http://item.gmall.com/{id}.html(id=${goods.id})}" target="_blank" th:utext="${goods.title}">Apple苹果iPhone 6s (A1699)Apple苹果iPhone 6s (A1699)Apple苹果iPhone 6s (A1699)Apple苹果iPhone 6s (A1699)</a>
        </div>
        <div class="commit">
            <i class="command">已有<span>2000</span>人评价</i>
        </div>
        <div class="operate">
            <a href="javascript:void(0);" class="sui-btn btn-bordered btn-danger">自营</a>
            <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
        </div>
    </div>
</li>

3.5 搜索条件处理

3.5.1 根据搜索对象SearchParam拼接url

ListController

package com.atguigu.gmall.web;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.ListFeignClient;
import com.atguigu.gmall.list.model.SearchAttr;
import com.atguigu.gmall.list.model.SearchParam;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author: atguigu
 * @create: 2023-03-06 10:45
 */
@Controller
public class ListHtmlController {

    @Autowired
    private ListFeignClient listFeignClient;

   /**
     * 渲染搜索页面
     * http://list.gmall.com/list.html?keyword=手机&pageNo=2&order=1:asc&props=23:8G:运行内存&props=24:128G:机身存储
     *
     * @param model
     * @return
     */
    @GetMapping("/list.html")
    public String listHtml(Model model, SearchParam searchParam) {
        //1.远程调用搜索服务进行检索
        Result<Map> result = listFeignClient.search(searchParam);

        //2.为渲染页面模板进行赋值
        model.addAllAttributes(result.getData());

        //3.以下所有参数本应该由前端记录提交提交
        //3.1 用户提交请求参数 ${searchParam}
        model.addAttribute("searchParam", searchParam);

        //3.2 用户回显地址栏中请求 包含搜索条件完整链接 ${urlParam}
        model.addAttribute("urlParam", this.makeUrlParam(searchParam));

        //3.3 回显用户已选中品牌面包屑 ${trademarkParam}

        //3.4 回显用户已选中平台属性 ${propsParamList}

        //3.5 回显页面排序效果${orderMap}   type:排序方式 sort:升降序

        return "list/index";
    }


    /**
     * 制作用户已选中所有条件URl请求
     *
     * @param searchParam
     * @return
     */
    private String makeUrlParam(SearchParam searchParam) {
        //1.在列表页面默认地址前置
        StringBuilder stringBuilder = new StringBuilder("/list.html?");
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            stringBuilder.append("&keyword=" + searchParam.getKeyword());
        }
        if (StringUtils.isNotBlank(searchParam.getTrademark())) {
            stringBuilder.append("&trademark=" + searchParam.getTrademark());
        }
        if (searchParam.getCategory1Id() != null) {
            stringBuilder.append("&category1Id=" + searchParam.getCategory1Id());
        }
        if (searchParam.getCategory2Id() != null) {
            stringBuilder.append("&category2Id=" + searchParam.getCategory2Id());
        }
        if (searchParam.getCategory3Id() != null) {
            stringBuilder.append("&category3Id=" + searchParam.getCategory3Id());
        }
        String[] props = searchParam.getProps();
        if (props != null && props.length > 0) {
            for (String prop : props) {
                stringBuilder.append("&props=").append(prop);
            }
        }
        return stringBuilder.toString();
    }
   
}

页面处理属性:平台属性处理 web-all/resources/templates/list/index.html

<div class="type-wrap" th:each="baseAttrInfo:${attrsList}" th:unless="${#strings.contains(urlParam, 'props='+baseAttrInfo.attrId)}">
    <div class="fl key" th:text="${baseAttrInfo.attrName}">网络制式</div>
    <div class="fl value">
        <ul class="type-list">
            <li th:each="attrValue:${baseAttrInfo.attrValueList}">
                <a href="" th:text="${attrValue}" >属性值111</a>
            </li>
        </ul>
    </div>
    <div class="fl ext"></div>
</div>

说明:

1,这样平台属性就拼接到url中,并且能保持参数了

2,点击平台属性,改平台属性就不在列表中显示了,控制如下:

th:unless="${#strings.contains(urlParam, 'props='+baseAttrInfo.attrId)}"

页面处理品牌显示

说明:th:if="${searchParam.trademark == null}" 控制品牌是否显示

<div class="type-wrap logo" th:if="${searchParam.trademark == null}">
    <div class="fl key brand">品牌</div>
    <div class="value logos">
        <ul class="logo-list">
            <li th:each="trademark:${trademarkList}">
                <a href="" th:text="${trademark.tmName}">属性值</a>
            </li>
        </ul>
    </div>
</div>

说明:目前页面已经渲染,但是搜索条件我们怎么处理,搜索条件值如何保持等问题还没解决,如图:

img

img

说明:所有的搜索条件都拼接到了一个url上面,除分页参数与排序

页面处理分页

<div class="sui-pagination pagination-large">
    <ul>
        <li class="prev" th:if="${pageNo != 1}">
            <a th:href="${urlParam}+'&pageNo='+${pageNo - 1}">上一页</a>
        </li>
        <li class="prev disabled" th:if="${pageNo == 1}">
            <a href="javascript:">上一页</a>
        </li>

        <li th:each="i : ${#numbers.sequence(1,totalPages)}" th:class="${i == pageNo} ? 'active' : ''">
            <a th:href="${urlParam}+'&pageNo='+${i}"><span th:text="${i}"></span></a>
        </li>

        <li class="next" th:if="${pageNo < totalPages}">
            <a th:href="${urlParam}+'&pageNo='+${pageNo + 1}">下一页</a>
        </li>
        <li class="next disabled" th:if="${pageNo == totalPages}">
            <a href="javascript:">下一页</a>
        </li>
    </ul>
    <div><span>共<span th:text="${totalPages }"></span>页&nbsp;</span><span></div>
</div>

3.5.2 面包屑处理

品牌与平台属性

ListController 修改 search方法

package com.atguigu.gmall.web;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.ListFeignClient;
import com.atguigu.gmall.list.model.SearchAttr;
import com.atguigu.gmall.list.model.SearchParam;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author: atguigu
 * @create: 2023-03-06 10:45
 */
@Controller
public class ListHtmlController {

    @Autowired
    private ListFeignClient listFeignClient;

   /**
     * 渲染搜索页面
     * http://list.gmall.com/list.html?keyword=手机&pageNo=2&order=1:asc&props=23:8G:运行内存&props=24:128G:机身存储
     *
     * @param model
     * @return
     */
    @GetMapping("/list.html")
    public String listHtml(Model model, SearchParam searchParam) {
        //1.远程调用搜索服务进行检索
        Result<Map> result = listFeignClient.search(searchParam);

        //2.为渲染页面模板进行赋值
        model.addAllAttributes(result.getData());

        //3.以下所有参数本应该由前端记录提交提交
        //3.1 用户提交请求参数 ${searchParam}
        model.addAttribute("searchParam", searchParam);

        //3.2 用户回显地址栏中请求 包含搜索条件完整链接 ${urlParam}
        model.addAttribute("urlParam", this.makeUrlParam(searchParam));

        //3.3 回显用户已选中品牌面包屑 ${trademarkParam}
        model.addAttribute("trademarkParam", this.makeTrademarkParam(searchParam.getTrademark()));

        //3.4 回显用户已选中平台属性 ${propsParamList}
        model.addAttribute("propsParamList", this.mekePropsParamList(searchParam.getProps()));

        //3.5 回显页面排序效果${orderMap}   type:排序方式 sort:升降序

        return "list/index";
    }

    /**
     * 用于回显用户选中品牌面包屑   品牌: 用户选择品牌名称
     *
     * @param trademark
     * @return
     */
    private String makeTrademarkParam(String trademark) {
        if (StringUtils.isNotBlank(trademark)) {
            String[] split = trademark.split(":");
            if (split != null && split.length == 2) {
                return "品牌: " + split[1];
            }
        }
        return null;
    }
    
    
    /**
     * 根据用户已选择平台属性,封装集合对象用户回显平台属性面包屑
     *
     * @param props
     * @return
     */
    private List<SearchAttr> mekePropsParamList(String[] props) {
        if (props != null && props.length > 0) {
            List<String> propsList = Arrays.asList(props);
            List<SearchAttr> searchAttrList = propsList.stream().map(prop -> {
                SearchAttr searchAttr = new SearchAttr();
                //形式   属性id:属性值:属性名称
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    searchAttr.setAttrId(Long.valueOf(split[0]));
                    searchAttr.setAttrValue(split[1]);
                    searchAttr.setAttrName(split[2]);
                }
                return searchAttr;
            }).collect(Collectors.toList());
            return searchAttrList;
        }
        return null;
    }
}

前台页面数据展示:

image-20221206113610043

页面处理

1,关键字

<ul class="fl sui-breadcrumb">
    <li>
        <a href="#">全部结果</a>
    </li>
    <li  class="active">
        <span th:text="${searchParam.keyword}"></span>
    </li>
</ul>

2,品牌处理

<ul class="fl sui-tag">
    <li th:if="${searchParam.trademark != null}" class="with-x">
        <span th:text="${trademarkParam}"></span>
        <a th:href="@{${#strings.replace(urlParam,'trademark='+searchParam.trademark,'')}}">×</a>
    </li>
    </ul>

说明:urlParam里面已经包含品牌参数,该链接必须去除该参数,所以我们可以使用thymeleaf 字符串替换函数,把品牌参数替换了就可以了,

${#strings.replace(urlParam,'trademark='+searchParam.trademark,'')}

3,平台属性处理

<ul class="fl sui-tag">
        <li th:if="${searchParam.props != null}" th:each="prop : ${propsParamList}" class="with-x">
        <span th:text="${prop.attrName}+':'+${prop.attrValue}"></span>
        <a th:href="@{${#strings.replace(urlParam+'&order='+searchParam.order,'props='+prop.attrId+':'+prop.attrValue+':'+prop.attrName,'')}}">×</a>
    </li>
</ul>

说明:与品牌一样,替换掉对应的平台属性值即可

${#strings.replace(urlParam,'props='+prop.attrId+':'+prop.attrValue+':'+prop.attrName,'')}

3.5.3 排序处理

注意:只要做了排序回显,业务数据就可以渲染

ListController

package com.atguigu.gmall.web.controller;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.ListFeignClient;
import com.atguigu.gmall.list.model.SearchAttr;
import com.atguigu.gmall.list.model.SearchParam;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-06-19 11:50
 */
@Controller
public class ListHtmlController {

    @Autowired
    private ListFeignClient listFeignClient;

    /**
     * 渲染搜索页面
     * http://list.gmall.com/list.html?keyword=手机&pageNo=2&order=1:asc&props=23:8G:运行内存&props=24:128G:机身存储
     *
     * @param model
     * @return
     */
    @GetMapping("/list.html")
    public String listHtml(Model model, SearchParam searchParam) {
        //1.远程调用搜索服务进行检索
        Result<Map> result = listFeignClient.search(searchParam);

        //2.为渲染页面模板进行赋值
        model.addAllAttributes(result.getData());

        //3.以下所有参数本应该由前端记录提交提交
        //3.1 用户提交请求参数 ${searchParam}
        model.addAttribute("searchParam", searchParam);

        //3.2 用户回显地址栏中请求 包含搜索条件完整链接 ${urlParam}
        model.addAttribute("urlParam", this.makeUrlParam(searchParam));

        //3.3 回显用户已选中品牌面包屑 ${trademarkParam}
        model.addAttribute("trademarkParam", this.makeTrademarkParam(searchParam.getTrademark()));

        //3.4 回显用户已选中平台属性 ${propsParamList}
        model.addAttribute("propsParamList", this.mekePropsParamList(searchParam.getProps()));

        //3.5 回显页面排序效果${orderMap}   type:排序方式 sort:升降序
        model.addAttribute("orderMap", this.makeOrderMap(searchParam.getOrder()));

        return "list/index";
    }

    /**
     * 根据用户选择排序方式 回显页面排序效果
     *
     * @param order 形式:     (综合降序)order=1:desc    (价格升序)order=2:asc
     * @return
     */
    private Map<String, String> makeOrderMap(String order) {
        HashMap<String, String> orderMap = new HashMap<>();
        if (StringUtils.isNotBlank(order)) {
            String[] split = order.split(":");
            if (split != null && split.length == 2) {
                orderMap.put("type", split[0]);
                orderMap.put("sort", split[1]);
                return orderMap;
            }
        }
        //默认排序方式 按照ES文档相关性算分进行排序
        orderMap.put("type", "3");
        orderMap.put("sort", "desc");
        return orderMap;
    }
}

页面

<ul class="sui-nav">
    <li th:class="${orderMap.type == '1' ? 'active': ''}">
        <a th:href="${urlParam}+'&order=1:'+${orderMap.sort == 'asc' ? 'desc' : 'asc'}">
            综合<span th:if="${orderMap.type == '1'}" th:text="${orderMap.sort == 'asc' ? '↑' : '↓'}"></span>
        </a>
    </li>
    <li th:class="${orderMap.type == '2' ? 'active': ''}">
        <a th:href="${urlParam}+'&order=2:'+${orderMap.sort == 'asc' ? 'desc' : 'asc'}">
            价格<span th:if="${orderMap.type == '2'}" th:text="${orderMap.sort == 'asc' ? '↑' : '↓'}"></span>
        </a>
    </li>
    <li>
        <a href="#">新品</a>
    </li>
    <li>
        <a href="#">评价</a>
    </li>
</ul>

说明:

​ 1,排序没有拼接到urlParam中,原因:如果拼接会重复出现

​ 2,为了保持排序条件,所以其他所有链接都需加上排序参数

改造其他连接

分页

<a th:href="${urlParam}+'&pageNo='+${i}+'&order='+${searchParam.order}"><span th:text="${i}"></span></a>

1,平台属性

<a th:href="${urlParam}+'&props='+${baseAttrInfo.attrId}+':'+${attrValue}+':'+${baseAttrInfo.attrName}+'&order='+${searchParam.order}" th:text="${attrValue}" >属性值111</a>
</li>

2,品牌

<a th:href="${urlParam}+'&trademark='+${trademark.tmId}+':'+${trademark.tmName}+'&order='+${searchParam.order}" th:text="${trademark.tmName}">属性值111</a>
</li>

3,面包屑

<ul class="fl sui-tag">
    <li th:if="${searchParam.trademark != null}" class="with-x">
        <span th:text="${trademarkParam}"></span>
        <a th:href="@{${#strings.replace(urlParam+'&order='+searchParam.order,'trademark='+searchParam.trademark,'')}}">×</a>
    </li>
    <li th:if="${searchParam.props != null}" th:each="prop : ${propsParamList}" class="with-x">
        <span th:text="${prop.attrName}+':'+${prop.attrValue}"></span>
        <a th:href="@{${#strings.replace(urlParam+'&order='+searchParam.order,'props='+prop.attrId+':'+prop.attrValue+':'+prop.attrName,'')}}">×</a>
    </li>
</ul>

4、在service-list模块中配置logstash

4.1 安装logstash

看电商软件环境安装.md

ELK/EFK

  • E ElasticSearch作用:检索存储分析/分词
  • K Kibana作用:操作ES的图形化客户端,各种数据报表仪表盘
  • L Logstash作用
  • F FileBeats 作用跟logstash相同

4.2 在service模块中添加依赖

<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>5.1</version>
</dependency>

4.3 将日志配置文件放入到resources目录下!

创建logback-spring.xml配置文件,内存如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <contextName>logback</contextName>

    <!-- 日志的输出目录 -->
    <property name="log.path" value="D:/logs/gmall/list" />

    <!--控制台日志格式:彩色日志-->
    <!-- magenta:洋红 -->
    <!-- boldMagenta:粗红-->
    <!-- cyan:青色 -->
    <!-- white:白色 -->
    <!-- magenta:洋红 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) %highlight([%-5level]) %green(%logger) %msg%n"/>

    <!--文件日志格式-->
    <property name="FILE_LOG_PATTERN"
              value="%date{yyyy-MM-dd HH:mm:ss} [%-5level] %thread %file:%line %logger %msg%n" />

    <!--编码-->
    <property name="ENCODING"
              value="UTF-8" />


    <!-- 控制台日志 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 临界值过滤器 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <!--CONSOLE_LOG_PATTERN 控制台格式  FILE_LOG_PATTERN 文件格式-->
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>${ENCODING}</charset>
        </encoder>
    </appender>

    <!-- 文件日志 -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <!--日志输出的目录是在哪?-->
        <file>${log.path}/log.log</file>
        <append>true</append>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>${ENCODING}</charset>
        </encoder>
    </appender>

    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!-- 级别过滤器 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch><!-- 当前要输出的日志如果是ERROR级别,则输出 -->
            <onMismatch>DENY</onMismatch><!-- 当前要输出的日志如果不是ERROR级别,则拒绝输出 -->
        </filter>

        <!--  要区别于其他的appender中的文件名字  -->
        <file>${log.path}/log-rolling-error.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>${ENCODING}</charset>
        </encoder>


        <!-- 设置滚动日志记录的滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info/log-rolling-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!--归档日志文件保留的最大数量-->
            <maxHistory>15</maxHistory>

            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
    </appender>

    <!-- logstash日志 -->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <!-- logstash ip和暴露的端口,logback就是通过这个地址把日志发送给logstash -->
        <destination>192.168.200.128:5044</destination>
        <encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder" />
    </appender>


    <!-- 开发环境 -->
    <springProfile name="dev">
        <!-- com.atguigu日志记录器:业务程序INFO,debug,warn,error级别  -->
        <logger name="com.atguigu" level="INFO" />
        <!-- 根日志记录器:INFO级别  -->
        <root level="INFO">
            <!--控制台输出模式-->
            <appender-ref ref="CONSOLE" />
            <!--FILE 文件基本输出-->
            <!--<appender-ref ref="FILE" />-->
            <!--有回滚日志记录-->
            <appender-ref ref="ROLLING_FILE" />
            <!--配置logstash 日志!-->
            <appender-ref ref="LOGSTASH" />
        </root>
    </springProfile>

    <!-- 生产或和试环境 -->
    <!--    <springProfile name="test,prod">-->
    <!--        <logger name="com.atguigu" level="INFO" additivity="false">-->
    <!--            <appender-ref ref="CONSOLE" />-->
    <!--        </logger>-->

    <!--        <root level="ERROR">-->
    <!--            <appender-ref ref="CONSOLE" />-->
    <!--            <appender-ref ref="ROLLING_FILE" />-->
    <!--        </root>-->
    <!--    </springProfile>-->

</configuration>