谷粒商城--搜索服务--高级篇笔记六

谷粒商城–搜索服务–高级篇笔记六

1 搜索页面搭建

1.1 添加依赖

		<!--devtools-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <!--Spring Cache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

1.1 上传静态文件至/mydata/nginx/html/static/search目录下

image-20211115141435233

1.2 将搜索页主页放在gulimall-search的templates目录下

image-20211115141750715

1.3 修改index.html中的文件引入路径

image-20211115142840430

image-20211115142908125

1.4 SwitchHosts配置

# gulimall
192.168.157.128 gulimall.com
#search
192.168.157.128 search.gulimall.com

image-20211115161555122

1.5 测试一

访问http://localhost:12000/

image-20211115161347925

访问http://search.gulimall.com/

image-20211115161645568

1.6 nginx配置

image-20211117140359495

server_name : gulimall.com->*.gulimall.com

server {
    
    
    listen       80;
    server_name  *.gulimall.com;


    location /static/ {
    
    
        root  /usr/share/nginx/html;
    }

    location / {
    
    
        proxy_set_header Host $host;
        proxy_pass http://gulimall;
    }


}

1.7 配置网关

        #将主机地址为search.gulimall.com转发至gulimall-search
        - id: gulimall_serach_host
          uri: lb://gulimall-search
          predicates:
            - Host=search.gulimall.com


        #将主机地址为**.gulimall.com转发至gulimall-product
        - id: gulimall_host
          uri: lb://gulimall-product
          predicates:
            - Host=**.gulimall.com

1.8 重启服务并测试

http://search.gulimall.com/

image-20211115172700346

访问gulimall.com

image-20211115175140858

解决:

server_name   gulimall.com *.gulimall.com;

重启nginx服务

访问gulimall.com

image-20211115175925780

1.9 修改index.html文件名

修改index.html为list.html

原因:在首页点击搜索跳转的链接为list.html

image-20211117140634878

gulimall-search/src/main/java/site/zhourui/gilimall/search/controller 新增listpage方法

package site.zhourui.gilimall.search.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author zr
 * @date 2021/11/17 14:35
 */
@Controller
public class SearchController {
    
    
    @GetMapping("/list.html")
    public String listPage(){
    
    

        return "list";
    }
}

重启服务测试

http://search.gulimall.com/list.html

image-20211117145316501

2 检索

2.1 检索条件分析

  • 全文检索: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&at trs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏

2.2 查询参数封装

gulimall-search/src/main/java/site/zhourui/gilimall/search/vo/SearchParam.java

package site.zhourui.gilimall.search.vo;

import lombok.Data;

import java.util.List;

/**
 * @author zr
 * @date 2021/11/17 15:02
 */
@Data
public class SearchParam {
    
    
    private String keyword;//页面传递过来的全文匹配关键字 v
    private Long catalog3Id;//三级分类 id v
    /**
     * 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/_500/500_
     *  brandId=1
     *  attrs=2_5 存:6 寸
     */

    private Integer hasStock;//是否只显示有货  0(无库存) 1(有库存)
    private String skuPrice;//价格区间查询
    private List<Long> brandId;//按照品牌进行查询, 可以多选
    private List<String> attrs;//按照属性进行筛选
    private Integer pageNum = 1;//页码
    private String _queryString;//原生的所有查询条件
}

2.3 相应结果封装

gulimall-search/src/main/java/site/zhourui/gilimall/search/vo/SearchResult.java

package site.zhourui.gilimall.search.vo;

import lombok.Data;
import site.zhourui.common.to.es.SkuEsModel;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zr
 * @date 2021/11/17 15:06
 */
@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<>();
    private List<Long> attrIds = 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;
    }
    
    @Data
    public static class AttrVo{
    
    
        private Long attrId;
        private String attrName;
        private List<String> attrValue;
    }

}

2.4 检索DSL

2.4.1 聚合时出现的问题 Use doc values instead

2.4.1.1 问题原因

index: 默认 true, 如果为 false, 表示该字段不会被索引, 但是检索结果里面有, 但字段本身不能 当做检索条件。

doc_values:默认 true, 设置为 false, 表示不可以做排序、 聚合以及脚本操作, 这样更节省磁盘空间。 还可以通过设定 doc_values 为true, index 为 false 来让字段不能被搜索但可以用于排序、 聚 合以及脚本操作:

image-20211117163829837

2.4.1.2 解决办法

需要修改索引映射中的doc

解决办法:删除映射中的属性

“index” : false,

index: 默认 true, 如果为 false, 表示该字段不会被索引, 但是检索结果里面有, 但字段本身不能 当做检索条件。

“doc_values” : false

默认 true, 设置为 false, 表示不可以做排序、 聚合以及脚本操作, 这样更节省磁盘空间。 还可以通过设定 doc_values 为true, index 为 false 来让字段不能被搜索但可以用于排序、 聚 合以及脚本操作:

image-20211117164029754

2.4.1.2.1 新建索引 gulimall_product
PUT gulimall_product
{
    
    
  "mappings": {
    
    
    "properties": {
    
    
      "skuId": {
    
    
        "type": "long"
      },
      "spuId": {
    
    
        "type": "keyword"
      },
      "skuTitle": {
    
    
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
    
    
        "type": "keyword"
      },
      "skuImg": {
    
    
        "type": "keyword"
      },
      "saleCount": {
    
    
        "type": "long"
      },
      "hasStock": {
    
    
        "type": "boolean"
      },
      "hotScore": {
    
    
        "type": "long"
      },
      "brandId": {
    
    
        "type": "long"
      },
      "catalogId": {
    
    
        "type": "long"
      },
      "brandName": {
    
    
        "type": "keyword"
      },
      "brandImg": {
    
    
        "type": "keyword"
      },
      "catalogName": {
    
    
        "type": "keyword"
      },
      "attrs": {
    
    
        "type": "nested",
        "properties": {
    
    
          "attrId": {
    
    
            "type": "long"
          },
          "attrName": {
    
    
            "type": "keyword"
          },
          "attrValue": {
    
    
            "type": "keyword"
          }
        }
      }
    }
  }
}
2.4.1.2.2 将product索引的数据迁移到gulimall_product
POST _reindex
{
  "source": {
    "index": "product"
  },
  "dest": {
    "index": "gulimall_product"
  }
}

迁移成功

image-20211117164729407

2.4.1.2.3 修改索引常量名

gulimall-common/src/main/java/site/zhourui/common/constant/EsConstant.java

image-20211117164937416

2.4.2 DSL查询部分

2.4.2.1 注:嵌入式属性type=nested,查询、过滤的时候也要使用嵌入式

官方文档

2.4.2.2 查询部分DSL

注意点:

  1. nested查询
  2. 价格区间 gte,lte
#查询部分
GET gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "手机"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "3",
              "5"
            ]
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "6"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "海思(Hisilicon)",
                        "以官网信息为准"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": {
              "value": "true"
            }
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 0,
              "lte": 7000
            }
          }
        }
      ]
    }
  }
}

2.4.2.3 查询部分+排序+分页+高亮DSL

模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮 ,聚合分析【分析所有可选的规格、分类、品牌】

#查询部分+排序+分页+高亮
GET gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "手机"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "1",
              "2",
              "9"
            ]
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "6"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "海思(Hisilicon)",
                        "以官网信息为准"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": {
              "value": "true"
            }
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 0,
              "lte": 7000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 2,
  "highlight": {
    "fields": {
      "skuTitle": {}
    },
    "pre_tags": "<b style='color:red'>",
    "post_tags": "</b>"
  }
  
}

2.4.2.4 聚合部分DSL

1、可以在聚合内聚合,就可以根据上一次聚合的结果 作为条件再聚合查【上一次聚合的结果只是没显示,可以作为内聚合的条件继续聚合,例如根据brandId聚合,再使用内部聚合可以聚合brandName】
2、并且嵌入式属性,聚合的时候也要使用嵌入式

#聚合部分
GET gulimall_product/_search
{
  "query": {
    "match_all": {}
  },
  "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
              }
            }
          }
        }
      }
    }
  }
}

image-20211117175054225

2.4.2.5 查询部分+排序+分页+高亮+聚合DSL

GET gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "手机"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "3",
              "5"
            ]
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "6"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "海思(Hisilicon)",
                        "以官网信息为准"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": {
              "value": "true"
            }
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 0,
              "lte": 7000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 2,
  "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
              }
            }
          }
        }
      }
    }
  }
}

2.4.3 使用java构建检索语句(将2.4.2检索语句翻译为java)

gulimall-search/src/main/java/site/zhourui/gilimall/search/controller/SearchController.java

package site.zhourui.gilimall.search.controller;

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 site.zhourui.gilimall.search.vo.SearchParam;
import site.zhourui.gilimall.search.vo.SearchResult;

import javax.servlet.http.HttpServletRequest;

/**
 * @author zr
 * @date 2021/11/17 14:35
 */
@Controller
public class SearchController {
    
    
    @Autowired
    MallSearchService mallSearchService;

    @GetMapping("/list.html")
    public String listPage(SearchParam param, Model model, HttpServletRequest request) {
    
    

        param.set_queryString(request.getQueryString());
        SearchResult result = mallSearchService.search(param);
        model.addAttribute("result", result);
        return "list";
    }
}

gulimall-search/src/main/java/site/zhourui/gilimall/search/service/MallSearchService.java

package site.zhourui.gilimall.search.service;

import site.zhourui.gilimall.search.vo.SearchParam;
import site.zhourui.gilimall.search.vo.SearchResult;

/**
 * @author zr
 * @date 2021/11/17 18:06
 */
public interface MallSearchService {
    
    
    SearchResult search(SearchParam param);
}

gulimall-search/src/main/java/site/zhourui/gilimall/search/service/impl/MallSearchServiceImpl.java

下面这部分代码是核心,请仔细阅读

image-20211117181413878

package site.zhourui.gilimall.search.service.impl;

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.ArrayUtils;
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 org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import site.zhourui.common.constant.EsConstant;
import site.zhourui.common.to.es.SkuEsModel;
import site.zhourui.gilimall.search.config.GulimallElasticSearchConfig;
import site.zhourui.gilimall.search.service.MallSearchService;
import site.zhourui.gilimall.search.vo.SearchParam;
import site.zhourui.gilimall.search.vo.SearchResult;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author zr
 * @date 2021/11/17 18:06
 */
@Service
public class MallSearchServiceImpl implements MallSearchService {
    
    

    @Autowired
    RestHighLevelClient client;

    /**
     * 在es检索
     *
     * @param param 检索的所有参数
     * @return 返回的结果
     */
    public SearchResult search(SearchParam param) {
    
    
        SearchResult result = null;
        // 1、动态构建检索需要的DSL语句

        // 1、准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);
        try {
    
    
            // 2、执行检索请求
            SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

            // 3、分析响应数据,封装成需要的格式
            result = buildSearchResponse(param, searchResponse);

        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 准备检所请求
     * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮 ,聚合分析【分析所有可选的规格、分类、品牌】
     *
     * 1、创建查询请求
     *      SearchRequest searchRequest = new SearchRequest("newbank")
     * 2、创建构建DSL检索条件对象
     *      SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
     * 3、创建各种条件
     *      QueryBuilder boolQuery = QueryBuilders.boolQuery()
     *      QueryBuilder matchQuery = QueryBuilders.matchAllQuery()
     *
     * 4、组合检索条件
     *         sourceBuilder.sort();
     *         sourceBuilder.from();
     *         sourceBuilder.size();
     *         sourceBuilder.aggregation();
     *         sourceBuilder.query(QueryBuilder);
     *         sourceBuilder.query(boolQuery);
     * 5、请求绑定条件
     *      searchRequest.source(sourceBuilder);
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
    
    
        // 构建DSL语句对象
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        /**
         * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
         */
        // 1、构建bool - query
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 1.1、must【得分】
        // 分词匹配skuTitle
        if (!StringUtils.isEmpty(param.getKeyword())) {
    
    
            boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
        }

        // 1.2、filter【无得分】
        // 三级分类
        if (param.getCatalog3Id() != null) {
    
    
            boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
        }

        // 1.3、品牌
        if (!CollectionUtils.isEmpty(param.getBrandId())) {
    
    
            boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
        }

        // 1.4、属性
        if (!CollectionUtils.isEmpty(param.getAttrs())) {
    
    
            for (String attr : param.getAttrs()) {
    
    
                BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
                String[] s = attr.split("_");
                String attrId = s[0];
                String[] attrValues = s[1].split(":");
                // must中的必须是同时满足的,如果boolQuery不是内层的
                // 那么boolQuery会拼接多个must(attrs.attrId),例如id = 6时,同时必须id = 7,没有此attr
                boolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
                // 每一个属性都要生成一个嵌入式查询
                // 商品必须包含每一个 传入的属性
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolQuery, ScoreMode.None);
                boolQueryBuilder.filter(nestedQuery);
            }
        }

        // 1.5、库存
        if (param.getHasStock() != null) {
    
    
            boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
        }

        // 1.6、价格区间 0_500  _500  500_
        if (!StringUtils.isEmpty(param.getSkuPrice())) {
    
    
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
            String[] s = param.getSkuPrice().split("_");
            if (s.length == 2) {
    
    
                rangeQuery.gte(s[0]).lte(s[1]);
            }else if (s.length == 1) {
    
    
                if (param.getSkuPrice().startsWith("_")) {
    
    
                    rangeQuery.lte(s[0]);
                }
                if (param.getSkuPrice().endsWith("_")) {
    
    
                    rangeQuery.gte(s[0]);
                }
            }
            boolQueryBuilder.filter(rangeQuery);
        }

        // 1、END 封装查询条件
        sourceBuilder.query(boolQueryBuilder);

        /**
         * 排序,分页,高亮
         */
        // 2.1、排序
        if (!StringUtils.isEmpty(param.getSort())) {
    
    
            String[] s = param.getSort().split("_");
            SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            sourceBuilder.sort(s[0], order);
        }

        // 2.2、分页
        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);
        }

        /**
         * 聚合分析【品牌、分类、分析所有可选的规格】
         */
        // 3.1、品牌聚合
        TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(10);
        // 子聚合,获得品牌name和图片
        brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
        brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
        // 构建DSL
        // TODO 聚合品牌
        sourceBuilder.aggregation(brand_agg);

        // 3.2、分类聚合
        TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
        // 子聚合,获得分类名
        catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
        // 构建DSL
        // TODO 聚合分类
        sourceBuilder.aggregation(catalog_agg);

        // 3.3、规格聚合【嵌入式】
        NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
        // 根据id分组
        TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId").size(10);
        // 子聚合=》在id分组内部,继续按照 attr_name分组
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
        // 子聚合=》在id分组内部,继续按照 attr_value分组
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
        // 嵌入式聚合
        attr_agg.subAggregation(attr_id_agg);
        // 构建DSL
        // TODO 聚合属性规格
        sourceBuilder.aggregation(attr_agg);

        SearchRequest searchRequest = new SearchRequest(new String[]{
    
    EsConstant.PRODUCT_INDEX}, sourceBuilder);

        // 打印DSL
        System.out.println(sourceBuilder.toString());
        return searchRequest;
    }

    /**
     * 封装检索结果
     * 1、返回所有查询到的商品
     * 2、当前所有商品涉及到的所有属性信息
     * 3、当前所有商品涉及到的所有品牌信息
     * 4、当前所有商品涉及到的所有分类信息
     * 5、分页信息   pageNum:当前页码  total:总记录数    totalPages: 总页码
     */
    private SearchResult buildSearchResponse(SearchParam param, SearchResponse searchResponse) {
    
    
        SearchResult result = new SearchResult();
        SearchHits hits = searchResponse.getHits();
        // 1、返回所有查询到的商品
        List<SkuEsModel> products = new ArrayList<>();
        if (!ArrayUtils.isEmpty(hits.getHits())) {
    
    
            for (SearchHit hit : hits.getHits()) {
    
    
                String jsonStr = hit.getSourceAsString();
                SkuEsModel model = JSON.parseObject(jsonStr, SkuEsModel.class);
                // 设置高亮信息
                if (!StringUtils.isEmpty(param.getKeyword())) {
    
    
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    model.setSkuTitle(skuTitle.getFragments()[0].string());
                }
                products.add(model);
            }
        }
        result.setProducts(products);

        // 2、当前所有商品涉及到的所有属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        ParsedNested attr_agg = searchResponse.getAggregations().get("attr_agg");
        ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
        List<? extends Terms.Bucket> attrBuckets = attr_id_agg.getBuckets();
        for (Terms.Bucket bucket : attrBuckets) {
    
    
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            // 提取属性ID
            attrVo.setAttrId(bucket.getKeyAsNumber().longValue());
            // 提取属性名字
            ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
            attrVo.setAttrName(attr_name_agg.getBuckets().get(0).getKeyAsString());
            // 提取品牌图片
            ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = attr_value_agg.getBuckets().stream().map(item -> {
    
    
                return ((Terms.Bucket) item).getKeyAsString();
            }).collect(Collectors.toList());
            attrVo.setAttrValue(attrValues);
            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);


        // 3、当前所有商品涉及到的所有品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        ParsedLongTerms brand_agg = searchResponse.getAggregations().get("brand_agg");
        List<? extends Terms.Bucket> brandBuckets = brand_agg.getBuckets();
        for (Terms.Bucket bucket : brandBuckets) {
    
    
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            // 提取品牌ID
            brandVo.setBrandId(bucket.getKeyAsNumber().longValue());
            // 提取品牌名字
            ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
            brandVo.setBrandName(brand_name_agg.getBuckets().get(0).getKeyAsString());
            // 提取品牌图片
            ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
            brandVo.setBrandImg(brand_img_agg.getBuckets().get(0).getKeyAsString());
            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);

        // 4、当前所有商品涉及到的所有分类信息
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        ParsedLongTerms catalog_agg = searchResponse.getAggregations().get("catalog_agg");
        List<? extends Terms.Bucket> catalogBuckets = catalog_agg.getBuckets();
        for (Terms.Bucket bucket : catalogBuckets) {
    
    
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            // 提取分类Id
            catalogVo.setCatalogId(Long.parseLong(bucket.getKeyAsString()));
            // 提取分类名字
            ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
            catalogVo.setCatalogName(catalog_name_agg.getBuckets().get(0).getKeyAsString());
            catalogVos.add(catalogVo);
        }
        result.setCatalogs(catalogVos);
        // 5、分页信息   pageNum:当前页码 、total:总记录数 、totalPages: 总页码
        long total = hits.getTotalHits().value;
        int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int)total/EsConstant.PRODUCT_PAGESIZE : ((int)total/EsConstant.PRODUCT_PAGESIZE + 1);
        result.setPageNum(param.getPageNum());
        result.setTotal(total);
        result.setTotalPages(totalPages);

        return result;
    }
}

测试

image-20211117213326039

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jFzeZ5zO-1638070778109)(http://zr.zhourui.site/img/image-20211117215359879.png)]

2.4.4 面包屑导航

gulimall-search/src/main/java/site/zhourui/gilimall/search/service/impl/MallSearchServiceImpl.java

2.4.4.1 构建面包屑导航功能

// 7、构建面包屑导航功能
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
    
    
            List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
    
    
                //1、分析每一个attrs传过来的参数值
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                // attrs=2_5寸:6寸
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);
                R r = productFeignService.attrInfo(Long.parseLong(s[0]));
                // 根据请求构造面包屑 规格属性Id集合,这个集合包含的属性规格不显示【前端会遍历每个参数显示】
                result.getAttrIds().add(Long.parseLong(s[0]));
                if (r.getCode() == 0) {
    
    
                    AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
    
    
                    });
                    // 设置属性名
                    navVo.setNavName(data.getAttrName());
                } else {
    
    
                    navVo.setNavName(s[0]);
                }

                //2、取消了这个面包屑以后,我们要跳转到哪个地方,将请求的地址url里面的当前置空
                //拿到所有的查询条件,去掉当前
                String replace = replaceQueryString(param, attr, "attrs");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);

                return navVo;
            }).collect(Collectors.toList());

            result.setNavs(collect);
        }

        // 品牌、分类
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
    
    
            List<SearchResult.NavVo> navs = result.getNavs();
            SearchResult.NavVo navVo = new SearchResult.NavVo();
            navVo.setNavName("品牌");
            // TODO 远程查询所有品牌
            R r = productFeignService.brandsInfo(param.getBrandId());
            if (r.getCode() == 0) {
    
    
                List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
    
    
                });
                StringBuffer sb = new StringBuffer();
                String replace = "";
                for (BrandVo brandVo : brand) {
    
    
                    sb.append(brandVo.getName()+";");
                    replace = replaceQueryString(param, brandVo.getBrandId()+"", "brandId");
                }
                navVo.setNavValue(sb.toString());
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);
            }
            navs.add(navVo);
        }


2.4.4.2 解决浏览器对空格的编码和Java不一样,差异化处理

浏览器对空格的编码和Java不一样,差异化处理

gulimall-search/src/main/java/site/zhourui/gilimall/search/service/impl/MallSearchServiceImpl.java

	//浏览器对空格的编码和Java不一样,差异化处理
private String replaceQueryString(SearchParam param, String value, String key) {
    
    
        String encode = null;
        try {
    
    
            encode = URLEncoder.encode(value, "UTF-8");
            encode = encode.replace("+", "%20");  //浏览器对空格的编码和Java不一样,差异化处理
        } catch (UnsupportedEncodingException e) {
    
    
            e.printStackTrace();
        }
        // 就是点了X之后,应该跳转的地址
        // 这里要判断一下,attrs是不是第一个参数,因为第一个参数 没有&符号
        // TODO BUG,第一个参数不带&
        return param.get_queryString().replace("&"+ key + "=" + encode, "");
    }

2.4.4.3 编写需要远程调用的接口

2.4.4.3.1 导入依赖

gulimall-search/pom.xml

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
2.4.4.3.2 开启feign@EnableFeignClients
2.4.4.3.3 编写远程调用接口

gulimall-search/src/main/java/site/zhourui/gilimall/search/feign/ProductFeignService.java

package site.zhourui.gilimall.search.feign;

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.RequestParam;
import site.zhourui.common.utils.R;

import java.util.List;

/**
 * @author zr
 * @date 2021/11/22 21:53
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {
    
    
    // 可以以这个为例,放入了缓存中
    @GetMapping("/product/attr/info/{attrId}")
    public R attrInfo(@PathVariable("attrId") Long attrId);

    /**
     * 远程调用的数据可以放入缓存中
     */
    @GetMapping("/product/brand/infos")
    public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);
}

3.4.4.3.4 新增实体类

gulimall-search/src/main/java/site/zhourui/gilimall/search/vo/AttrResponseVo.java

package site.zhourui.gilimall.search.vo;

import lombok.Data;

/**
 * @author zr
 * @date 2021/11/22 22:02
 */
@Data
public class AttrResponseVo {
    
    
    /**
     * 属性id
     */
    private Long attrId;
    /**
     * 属性名
     */
    private String attrName;
    /**
     * 是否需要检索[0-不需要,1-需要]
     */
    private Integer searchType;
    /**
     * 单选 多选[0 1]
     */
    private Integer valueType;
    /**
     * 属性图标
     */
    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;

    /**
     * 属性分类Id,vo独有字段
     */
    private Long attrGroupId;

    private String catelogName;
    private String groupName;

    private Long[] catelogPath;
}

gulimall-search/src/main/java/site/zhourui/gilimall/search/vo/BrandVo.java

package site.zhourui.gilimall.search.vo;

import lombok.Data;

/**
 * @author zr
 * @date 2021/11/22 22:06
 */
@Data
public class BrandVo {
    
    
    private Long brandId;
    private String name;
}
3.4.4.3.5 远程新增接口
3.4.4.3.5.1 attrInfo接口

gulimall-product/src/main/java/site/zhourui/gulimall/product/app/AttrController.java

/**
     * 信息
     */
    @RequestMapping("/info/{attrId}")
    //@RequiresPermissions("product:attr:info")
    public R info(@PathVariable("attrId") Long attrId){
    
    
        //AttrEntity attr = attrService.getById(attrId);
        AttrRespVo respVo = attrService.getAttrInfo(attrId);

        return R.ok().put("attr", respVo);
    }

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/AttrService.java

AttrRespVo getAttrInfo(Long attrId);

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/AttrServiceImpl.java

	/**
     * 获取属性信息{属性分组信息+商品分类信息}
     *
     * @param attrId
     * @return
     */
    @Cacheable(value = "attr", key = "'attrInfo:'+#root.args[0]")
    @Override
    public AttrRespVo getAttrInfo(Long attrId) {
    
    
        AttrEntity attrEntity = this.getById(attrId);
        AttrRespVo respVo = new AttrRespVo();
        BeanUtils.copyProperties(attrEntity, respVo);

        if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
    
    
            // 设置分组信息
            AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
            if (relationEntity != null) {
    
    
                respVo.setAttrGroupId(relationEntity.getAttrGroupId());
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
                // 可能没有设置分组,relationEntity.getAttrGroupId()是null,导致attrGroupEntity也是null
                if (attrGroupEntity != null) {
    
    
                    respVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }
        }
        // 设置分类信息
        Long[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId());
        respVo.setCatelogPath(catelogPath);
        CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
        if (categoryEntity != null) {
    
    
            respVo.setCatelogName(categoryEntity.getName());
        }
        return respVo;
    }
3.4.4.3.5.1 brandsInfo接口

gulimall-product/src/main/java/site/zhourui/gulimall/product/app/BrandController.java

	@GetMapping("/infos")
    public R info(@RequestParam("brandIds") List<Long> brandIds) {
    
    
        List<BrandEntity> brand = brandService.getBrandsByIds(brandIds);
        return R.ok().put("brand", brand);
    }

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/BrandService.java

  List<BrandEntity> getBrandsByIds(List<Long> brandIds);

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/BrandServiceImpl.java

	@Override
    public List<BrandEntity> getBrandsByIds(List<Long> brandIds) {
    
    
        return baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id", brandIds));
    }

需要调用商品服务根据数据id查询属性名

gulimall-product/src/main/java/site/zhourui/gulimall/product/app/AttrController.java

 /**
     * 信息
     */
    @RequestMapping("/info/{attrId}")
    //@RequiresPermissions("product:attr:info")
    public R info(@PathVariable("attrId") Long attrId){
    
    
        //AttrEntity attr = attrService.getById(attrId);
        AttrRespVo respVo = attrService.getAttrInfo(attrId);

        return R.ok().put("attr", respVo);
    }

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/AttrService.java

    AttrRespVo getAttrInfo(Long attrId);

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/AttrServiceImpl.java

再加上缓存

/**
     * 获取属性信息{属性分组信息+商品分类信息}
     *
     * @param attrId
     * @return
     */
    @Cacheable(value = "attr", key = "'attrInfo:'+#root.args[0]")
    @Override
    public AttrRespVo getAttrInfo(Long attrId) {
    
    
        AttrEntity attrEntity = this.getById(attrId);
        AttrRespVo respVo = new AttrRespVo();
        BeanUtils.copyProperties(attrEntity, respVo);

        if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
    
    
            // 设置分组信息
            AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
            if (relationEntity != null) {
    
    
                respVo.setAttrGroupId(relationEntity.getAttrGroupId());
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
                // 可能没有设置分组,relationEntity.getAttrGroupId()是null,导致attrGroupEntity也是null
                if (attrGroupEntity != null) {
    
    
                    respVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }
        }
        // 设置分类信息
        Long[] catelogPath = categoryService.findCatelogPath(attrEntity.getCatelogId());
        respVo.setCatelogPath(catelogPath);
        CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
        if (categoryEntity != null) {
    
    
            respVo.setCatelogName(categoryEntity.getName());
        }
        return respVo;
    }

2.4.5 前端代码

此处前端就不做笔记了

附上代码链接

测试

image-20211122223959750

猜你喜欢

转载自blog.csdn.net/qq_31745863/article/details/121588982