1.构建商品检索页面
1.1.引入依赖
<!-- thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 热更新 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
1.2.模板页面
- 将 index.html 拷贝到 templates 目录
- 修改 index.html 为 list.html
1.3.静态资源
1)在nginx\html\static\ 文件下创建 search 文件夹,并将所有静态资源文件上传到 search\ 文件夹
2)修改list.html模板,将所有静态资源链接URL加上前置路径 /static/search/
1.4.域名访问配置
本地映射
192.168.139.10 search.gmall.com
Nginx配置 gmall.conf
server {
listen 80;
server_name *.gmall.com gmall.com;
location /static/ {
root /usr/share/nginx/html;
}
location / {
proxy_pass http://gmall;
proxy_set_header Host $host;
}
}
网关路由配置
- id: gmall_host_route
uri: lb://gmall-product
predicates:
- Host=gmall.com
- id: gmall_search_route
uri: lb://gmall-search
predicates:
- Host=search.gmall.com
1.5.页面跳转后台接口
SearchController
package com.atguigu.gmall.search.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 商品检索 {@link SearchController}
*
* @author zhangwen
* @email: 1466787185@qq.com
*/
@Controller
public class SearchController {
/**
* 商品检索页面
* @return
*/
@GetMapping("/list.html")
public String listPage() {
return "list";
}
}
2.商品检索业务分析
2.1.检索业务分析
2.1.1.商品检索三个入口
输入检索关键字展示检索页
选择分类进入商品检索
选择筛选条件进入(复杂)
根据检索关键字进入检索页面
点击三级分类进入检索页面
2.1.2.检索条件分析
- 全文检索:skuTitle(keyword)
- 排序:saleCount(销量)、hotScore(热度评分/综合排序)、skuPrice(价格)
- 过滤:hasStock(仅显示有货)、skuPrice区间、brandId、catalogId、attrs
- 聚合:attrs
完整查询参数
search.gmall.com/list.html?keyword=华为&sort=saleCount_desc
&hasStock=1&skuPrice=1000_5000&brandId=1&catalog3Id=1&attrs=1
_3G:4G:5G&attrs=2_骁龙845&attrs=3_高清屏
2.2.检索条件和检索结果封装
2.2.1.SearchParamVO
package com.atguigu.gmall.search.vo;
import lombok.Data;
import java.util.List;
/**
* 商品检索条件 {@link SearchParamVO}
* ?keyword=华为&sort=saleCount_desc&hasStock=1&skuPrice=1000_5000&brandId=1&catalog3Id=225
* &attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=3_高清屏
* @author zhangwen
* @email: 1466787185@qq.com
*/
@Data
public class SearchParamVO {
{
// 页码默认值
pageNum = 1;
}
/**
* 检索输入框传递过来的检索关键字
*/
private String keyword;
/**
* 三级分类id
*/
private Long catalog3Id;
/**
* 排序条件,三选一
* 销量排序:sort=saleCount_desc/asc
* 综合排序:sort=hasStock_desc/asc
* 价格排序:sort=skuPrice_desc/asc
*/
private String sort;
/**
* 过滤条件
* hasStock(仅显示有货)、skuPrice区间(价格区间)、brandId(品牌id)、catalogId(分类id)、attrs(商品属性)
* hasStock=0/1
* skuPrice=100_500/_500/100_
* brandId=1&brandId=2
* attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=3_高清屏
*/
private Integer hasStock;
private String skuPrice;
private List<Long> brandId;
private List<String> attrs;
/**
* 当前页码
*/
private Integer pageNum;
/**
* 所有查询条件
*/
private String queryString;
}
2.2.2.SearchResponseVO
package com.atguigua.gmall.search.vo;
import com.atguigua.common.to.es.SkuEsModel;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 商品检索结果 {@link SearchResponseVO}
*
* @author zhangwen
* @email: 1466787185@qq.com
*/
@Data
public class SearchResponseVO {
{
navs = new ArrayList<>();
attrIds = new ArrayList<>();
}
/**
* 检索到的所有商品信息
*/
private List<SkuEsModel> products;
/**
* 当前页面
*/
private Integer pageNum;
/**
* 总记录数
*/
private Long totalCount;
/**
* 总页码
*/
private Integer totalPage;
/**
* 导航页
*/
private List<Integer> pageNavs;
/**
* 检索到的结果所涉及的所有品牌
*/
private List<BrandVO> brands;
/**
* 检索结果涉及的所有分类
*/
private List<CatalogVO> catalogs;
/**
* 检索结果涉及的商品属性
*/
private List<AttrVO> attrs;
/**
* 面包屑导航
*/
private List<NavVO> navs;
private List<Long> attrIds;
@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;
}
@Data
public static class AttrVO {
private Long attrId;
private String attrName;
private List<String> attrValues;
}
}
2.3.ElasticSearch数据迁移
2.3.1.创建新的索引及映射
PUT gmall_product
{
"mappings": {
"properties": {
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": true
},
"attrValue": {
"type": "keyword"
}
}
},
"brandId": {
"type": "long"
},
"brandImg": {
"type": "keyword",
"index": false,
"doc_values": true
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": true
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": true
},
"hasScore": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"saleCount": {
"type": "long"
},
"skuId": {
"type": "long"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": true
},
"skuPrice": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"spuId": {
"type": "keyword"
}
}
}
}
2.3.2.迁移数据
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "gmall_product"
}
}
2.3.3.修改检索服务索引名
package com.atguigu.gmall.search.constant;
/**
* ES常量类 {@link EsConstant}
*
* @author zhangwen
* @email: 1466787185@qq.com
*/
public class EsConstant {
/**
* sku数据在es中的索引
*/
public static final String PRODUCT_INDEX = "gmall_product";
}
2.4.分析ES检索DSL
#模糊匹配 must
#过滤 filter
#排序 sort
#分页 from size
#高亮 highlight
#聚合分析 aggs
#如果是嵌入式属性,查询、聚合分析都需要用嵌入式
GET gmall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"4"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "8"
}
}
},
{
"terms": {
"attrs.attrValue": [
"LIO-AN00",
"158.1"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": true
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
3.商品检索业务实现
3.1.检索接口实现
3.1.1.检索接口
/**
* 商品检索
* @param searchParamVO 检索的所有参数
* @return
*/
@Override
public SearchResponseVO search(SearchParamVO searchParamVO) {
SearchResponseVO searchResponseVO = null;
//1.准备检索请求
SearchRequest searchRequest = buildSearchRequest(searchParamVO);
try {
//2.执行检索请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest,ElasticSearchConfig.COMMON_OPTIONS);
//3.分析检索结果,封装成SearchResponseVO
searchResponseVO = buildSearchResponse(searchResponse, searchParamVO);
} catch (IOException e) {
e.printStackTrace();
}
return searchResponseVO;
}
3.1.1.构建检索查询SearchRequest
/**
* 准备检索请求
* 模糊匹配 must
* 过滤 filter
* 排序 sort
* 分页 from size
* 高亮 highlight
* 聚合分析 aggs
* @return
*/
private SearchRequest buildSearchRequest(SearchParamVO param) {
// 动态构建检索的DSL语句
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 查询:模糊匹配、过滤(品牌、分类、属性、价格区间、库存)
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 按照商品名称模糊查询
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
// 按照三级分类id查询
if (param.getCatalog3Id() != null) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
// 按照品牌id查询
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
// 按照所有指定的属性进行查询
// attr=1_3G:4G:5G&attr=2_高通骁龙845
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
for (String attr : param.getAttrs()) {
String[] s = attr.split("_");
String attrId = s[0];
String[] attrValues = s[1].split(":");
BoolQueryBuilder query = QueryBuilders.boolQuery();
query.must(QueryBuilders.termQuery("attrs.attrId", attrId));
query.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
// 每一个属性都必须生成一个 NestedQuery
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", query, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
// 按照是否有库存进行查询
if (param.getHasStock() != null) {
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
}
// 按照价格区间进行查询:1_100/_100/100_
if (!StringUtils.isEmpty(param.getSkuPrice())) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] price = param.getSkuPrice().split("_", 2);
if (StringUtils.isEmpty(price[0])) {
rangeQuery.lte(price[1]);
} else if (StringUtils.isEmpty(price[1])) {
rangeQuery.gte(price[0]);
} else {
rangeQuery.gte(price[0]).lte(price[1]);
}
boolQuery.filter(rangeQuery);
}
searchSourceBuilder.query(boolQuery);
// 排序
// sort=saleCount_desc
if (!StringUtils.isEmpty(param.getSort())) {
String[] s = param.getSort().split("_");
SortOrder sortOrder = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
if ("price".equals(s[0])) {
searchSourceBuilder.sort("skuPrice", sortOrder);
} else {
searchSourceBuilder.sort(s[0], sortOrder);
}
}
// 分页
// from = (pageNum - 1) * pageSize
searchSourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGE_SIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGE_SIZE);
// 高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
searchSourceBuilder.highlighter(highlightBuilder);
}
// 聚合分析
// 品牌聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
// 品牌子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
searchSourceBuilder.aggregation(brand_agg);
// 分类聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
searchSourceBuilder.aggregation(catalog_agg);
// 属性聚合
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId").size(1);
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
attr_agg.subAggregation(attr_id_agg);
searchSourceBuilder.aggregation(attr_agg);
System.out.println("构建DSL:" + searchSourceBuilder.toString());
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
return searchRequest;
}
3.1.2.分析检索结果SearchResponse
/**
* 返回检索结果数据
* @param searchResponse
* @return
*/
private SearchResponseVO buildSearchResponse(SearchResponse searchResponse, SearchParamVO param) {
SearchResponseVO searchResponseVO = new SearchResponseVO();
// 返回所有查询到的商品
SearchHits hits = searchResponse.getHits();
List<SkuEsModel> skuEsModels = new ArrayList<>();
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel skuEsModel = JsonUtils.jsonToPojo(sourceAsString, SkuEsModel.class);
// 设置关键字高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
String skuTitle = hit.getHighlightFields().get("skuTitle").getFragments()[0].string();
skuEsModel.setSkuTitle(skuTitle);
}
skuEsModels.add(skuEsModel);
}
}
searchResponseVO.setProducts(skuEsModels);
// 聚合分析:分类信息、品牌信息、属性信息
// 分类信息
List<SearchResponseVO.CatalogVO> catalogVOS = new ArrayList<>();
ParsedLongTerms catalog_agg = searchResponse.getAggregations().get("catalog_agg");
for (Terms.Bucket bucket : catalog_agg.getBuckets()) {
SearchResponseVO.CatalogVO catalogVO = new SearchResponseVO.CatalogVO();
// 分类ID
catalogVO.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
// 分类名
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVO.setCatalogName(catalogName);
catalogVOS.add(catalogVO);
}
searchResponseVO.setCatalogs(catalogVOS);
// 品牌信息
List<SearchResponseVO.BrandVO> brandVOS = new ArrayList<>();
ParsedLongTerms brand_agg = searchResponse.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResponseVO.BrandVO brandVO = new SearchResponseVO.BrandVO();
// 品牌ID
brandVO.setBrandId(bucket.getKeyAsNumber().longValue());
// 品牌名称
ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
String brandName = brand_name_agg.getBuckets().get(0).getKeyAsString();
brandVO.setBrandName(brandName);
// 品牌图片
ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
String brandImg = brand_img_agg.getBuckets().get(0).getKeyAsString();
brandVO.setBrandImg(brandImg);
brandVOS.add(brandVO);
}
searchResponseVO.setBrands(brandVOS);
// 属性
List<SearchResponseVO.AttrVO> attrVOS = new ArrayList<>();
ParsedNested attr_agg = searchResponse.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResponseVO.AttrVO attrVO = new SearchResponseVO.AttrVO();
// 属性ID
long attrId = bucket.getKeyAsNumber().longValue();
attrVO.setAttrId(attrId);
// 属性名
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg"))
.getBuckets().get(0).getKeyAsString();
attrVO.setAttrName(attrName);
// 属性所有值
List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg"))
.getBuckets().stream().map(item -> {
String keyAsString = item.getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVO.setAttrValues(attrValues);
attrVOS.add(attrVO);
}
searchResponseVO.setAttrs(attrVOS);
// 分页信息
// 当前页码
searchResponseVO.setPageNum(param.getPageNum());
// 总记录数
long totalCount = hits.getTotalHits().value;
searchResponseVO.setTotalCount(totalCount);
// 总页数
int totalPage = (int)totalCount % EsConstant.PRODUCT_PAGE_SIZE == 0
? (int)totalCount / EsConstant.PRODUCT_PAGE_SIZE
: (int)totalCount / EsConstant.PRODUCT_PAGE_SIZE + 1;
searchResponseVO.setTotalPage(totalPage);
// 页码导航数
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPage; i++) {
pageNavs.add(i);
}
searchResponseVO.setPageNavs(pageNavs);
// 构建面包屑导航
// 面包屑-属性
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
List<SearchResponseVO.NavVO> navs = param.getAttrs().stream().map(attr -> {
SearchResponseVO.NavVO navVO = new SearchResponseVO.NavVO();
String[] s = attr.split("_");
searchResponseVO.getAttrIds().add(Long.parseLong(s[0]));
navVO.setNavValue(s[1]);
// 远程调用耗时,远程查询接口结果加入缓存
R r = productFeignService.getAttrInfo(Long.parseLong(s[0]));
if (r.getCode() == 0) {
AttrResponseVO data = r.getData("attr", new TypeReference<AttrResponseVO>() {});
navVO.setNavName(data.getAttrName());
} else {
log.error("调用远程服务 gmall-product 查询属性信息失败");
navVO.setNavName("");
}
// 取消面包屑以后,需要将请求url里面的当前属性值置空
String replace = replaceQueryString(param.getQueryString(), "attrs", attr);
navVO.setLink("http://search.gmall.com/list.html?" + replace);
return navVO;
}).collect(Collectors.toList());
searchResponseVO.setNavs(navs);
}
// 面包屑-品牌
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
List<SearchResponseVO.NavVO> navs = searchResponseVO.getNavs();
SearchResponseVO.NavVO navVO = new SearchResponseVO.NavVO();
navVO.setNavName("品牌");
// 远程调用耗时,远程查询接口结果加入缓存
R r = productFeignService.getBrandInfos(param.getBrandId());
if (r.getCode() == 0) {
List<BrandResponseVO> brands = r.getData("brands", new TypeReference<List<BrandResponseVO>>() {});
String replace = null;
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < brands.size(); i++) {
BrandResponseVO vo = brands.get(i);
if (i == 0) {
buffer.append(vo.getName());
} else {
buffer.append("、" + vo.getName());
}
replace = replaceQueryString(param.getQueryString(), "brandId", vo.getBrandId()+"");
}
navVO.setNavValue(buffer.toString());
navVO.setLink("http://search.gmall.com/list.html?" + replace);
} else {
log.error("调用远程服务 gmall-product 查询品牌信息失败");
}
navs.add(navVO);
}
// TODO:面包屑-分类
return searchResponseVO;
}
3.2.检索模板页面实现
3.2.1.模板页面数据渲染
- 商品列表渲染
- 商品筛选条件渲染
- 商品列表分页数据渲染
3.2.2.商品检索页面功能
-
商品筛选条件过滤
-
关键字检索
-
商品价格区间过滤
-
商品排序
1、综合排序
2、按销量排序
3、 按价格排序 -
分页跳转处理
-
面包屑导航