谷粒商城高级篇-商品检索

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(&quot;brandId&quot;,'+brand.brandId+')'}

由于是在${}中操作,所以参数以引号的形式输入。

&quot;代表"=>

<a href="#" th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+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(&quot;brandId&quot;,'+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(&quot;catalogId&quot;,'+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(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}">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>页&nbsp;&nbsp;到第</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(&quot;brandId&quot;,'+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(&quot;catalog3&quot;,'+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(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
                               th:text="${val}">5.56英寸及以上</a></li>

                    </ul>

                </div>

            </div>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值