You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

496 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 商城检索服务
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/8ecc8f927e474f39aac038df1b2071a4.png)
## 1.检索页面的搭建
  商品检索页面我们放在search服务中处理首页我们需要在mall-search服务中支持Thymeleaf。添加对应的依赖
```xml
<!-- 添加Thymeleaf的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```
然后我们拷贝模板文件到template目录下然后不要忘记添加Thymeleaf的名称空间
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/377ec04c591946f8b45465e03d2a31a3.png)
需要把相关的静态资源文件拷贝到Nginx服务中。目录结构是/mydata/nginx/html/static/search/
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/c421808d6bc2482c895ecf9126114fa8.png)
我们需要修改index.html页面中的资源的路径
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/1e711cb680094d8ab71394eda1b9fd7e.png)
然后我们要通过 `msb.search.com` 来访问我们的检索服务那么就需要设置对应的host文件
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/3c2372b09ef144ccbb631836cfbb240c.png)
然后我们就需要修改Nginx的配置
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/542a3ec0152948ed892f76da0297fd9f.png)
这时我需要在修改网关的服务,根据我们的域名访问,那么需要网关路由到我们的检索服务中
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/a42fbe4df3b64802833fefb85d3c26f5.png)
然后我们就可以重启相关的服务 ,来测试了
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/1462/1648456266000/95ad0cb04db54184a2013fe7ee363311.png)
## 2.检索服务
### 2.1 创建对应VO
&emsp;&emsp;我们需要检索数据库中的相关的商品信息那么我们就需要提交相关的检索条件为了统一的管理提交的数据我们需要创建一个VO来封装信息。
```java
/**
* 封装页面所有可能提交的查询条件
*/
@Data
public class SearchParam {
private String keyword; // 页面传递的查询全文匹配的关键字
private Long catalog3Id;// 需要根据分类查询的编号
/**
* sort=salaCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
private String sort; // 排序条件
// 查询的筛选条件 hasStock=0/1;
private Integer hasStock ; // 是否只显示有货
// brandId=1&brandId=2
private List<Long> brandId; // 按照品牌来查询,可以多选
// skuPrice=200_300
// skuPrice=_300
// skuPrice=200_
private String skuPrice; // 价格区间查询
// 不同的属性 attrs:1_苹果:6.5寸
private List<String> attrs; // 按照属性信息进行筛选
private Integer pageNum; // 页码
}
```
&emsp;&emsp;然后就是检索后的数据我们需要封装的VO对象定义如下
```java
package com.msb.mall.mallsearch.vo;
import com.msb.common.dto.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<AttrVo> attrs;
// 当前查询的所有商品涉及到的所有的类别信息
private List<CatalogVO> catalogs;
@Data
public static class CatalogVO{
private Long catalogId;
private String catalogName;
}
/**
* 品牌的相关信息
*/
@Data
public static class BrandVO{
private Long brandId; // 品牌的编号
private String brandName; // 品牌的名称
private String brandImg; // 品牌的图片
}
@Data
public static class AttrVo{
private Long attrId; // 属性的编号
private String attrName; // 属性的名称
private List<String> attrValue; // 属性的值
}
}
```
### 2.2 构建查询DSL语句
&emsp;&emsp;我们需要根据基本的检索条件来封装对应的DSL语句
* 查询关键字 模糊匹配
* 过滤(分类,品牌,属性,价格区间,库存...)
* 排序
* 分页
* 高亮
```json
GET /product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"subTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"13",
"16",
"14"
]
}
},
{
"range": {
"skuPrice": {
"gte": 10,
"lte": 12000
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "9"
}
}
},
{
"terms": {
"attrs.attrValue": [
"12",
"08",
"11"
]
}
}
]
}
}
}
}
]
}
},"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],"from": 0
,"size": 20
,"highlight": {
"fields": {"subTitle": {}}
,"pre_tags": "<b style='color:red'>"
,"post_tags": "<b>"
}
}
```
### 2.3 构建SearchRequest对象
&emsp;&emsp;根据客户端提交的检索的信息我们需要封装为对应的SearchRequest对象然后通过ES的API来检索数据。
```
/**
* 构建检索的请求
* 模糊匹配,关键字匹配
* 过滤(类别,品牌,属性,价格区间,库存)
* 排序
* 分页
* 高亮
* 聚合分析
* @param param
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param) {
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices(ESConstant.PRODUCT_INDEX);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 构建具体的检索的条件
// 1.构建bool查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 1.1 关键字的条件
if(!StringUtils.isEmpty(param.getKeyword())){
boolQuery.must(QueryBuilders.matchQuery("subTitle",param.getKeyword()));
}
// 1.2 类别的检索条件
if(param.getCatalog3Id() != null){
boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
}
// 1.3 品牌的检索条件
if(param.getBrandId() != null && param.getBrandId().size() > 0){
boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
}
// 1.4 是否有库存
if(param.getHasStock() != null){
boolQuery.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1));
}
// 1.5 根据价格区间来检索
if(!StringUtils.isEmpty(param.getSkuPrice())){
String[] msg = param.getSkuPrice().split("_");
RangeQueryBuilder skuPrice = QueryBuilders.rangeQuery("skuPrice");
if(msg.length == 2){
// 说明是 200_300
skuPrice.gte(msg[0]);
skuPrice.lte(msg[1]);
}else if(msg.length == 1){
// 说明是 _300 200_
if(param.getSkuPrice().endsWith("_")){
// 说明是 200_
skuPrice.gte(msg[0]);
}
if(param.getSkuPrice().startsWith("_")){
// 说明是 _300
skuPrice.lte(msg[0]);
}
}
boolQuery.filter(skuPrice);
}
// 1.6 属性的检索条件 attrs=20_8英寸:10英寸&attrs=19_64GB:32GB
if(param.getAttrs() != null && param.getAttrs().size() > 0){
for (String attrStr : param.getAttrs()) {
BoolQueryBuilder boolNestedQuery = QueryBuilders.boolQuery();
// attrs=19_64GB:32GB 我们首先需要根据 _ 做分割
String[] attrStrArray = attrStr.split("_");
// 属性的编号
String attrId = attrStrArray[0];
// 64GB:32GB 获取属性的值
String[] values = attrStrArray[1].split(":");
// 拼接组合条件
boolNestedQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
boolNestedQuery.must(QueryBuilders.termsQuery("attrs.attrValue",values));
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolNestedQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
sourceBuilder.query(boolQuery);
// 2.排序
if(!StringUtils.isEmpty(param.getSort())){
// sort=salaCount_asc/desc
String[] s = param.getSort().split("_");
SortOrder order = s[1].equalsIgnoreCase("asc")?SortOrder.ASC:SortOrder.DESC;
sourceBuilder.sort(s[0], order);
}
// 3.处理分页
// Integer pageNum; // 页码
if(param.getPageNum() != null){
// 需要做分页处理 pageSize = 5
// pageNum:1 from:0 [0,1,2,3,4]
// pageNum:2 from:5 [5,6,7,8,9]
// from = ( pageNum - 1 ) * pageSize
sourceBuilder.from( (param.getPageNum() - 1 ) * ESConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(ESConstant.PRODUCT_PAGESIZE);
}
// 4. 设置高亮
if(!StringUtils.isEmpty(param.getKeyword())){
// 如果有根据关键字查询那么我们才需要高亮设置
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("subTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
sourceBuilder.highlighter(highlightBuilder);
}
// 5.聚合运算
// 5.1 品牌的聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId");
brand_agg.size(50);
// 品牌的子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(10));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(10));
sourceBuilder.aggregation(brand_agg);
// 5.2 类别的聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
catalog_agg.field("catalogId");
catalog_agg.size(10);
// 类别的子聚合
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(10));
sourceBuilder.aggregation(catalog_agg);
// 5.3 属性的聚合
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
// 属性id聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg");
attr_id_agg.field("attrs.attrId");
attr_id_agg.size(10);
// 属性id下的子聚合 属性名称和属性值
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(10));
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(10));
attr_agg.subAggregation(attr_id_agg);
sourceBuilder.aggregation(attr_agg);
System.out.println(sourceBuilder.toString());
searchRequest.source(sourceBuilder);
return searchRequest;
}
```
### 2.4 构建SearchResult对象
&emsp;&emsp;当我们通过封装的SearchRequest对象从ES中检索出了相关的信息后我们需要将返回的SearchResponse对象封装为前端接收的SearchResult对象。
* 所有的满足条件的商品
* 分页相关的信息
* 当前商品涉及的品牌信息
* 当前商品涉及的类别信息
* 当前商品涉及的属性信息
```
/**
* 根据检索的结果解析封装为SearchResult对象
* @param response
* @return
*/
private SearchResult buildSearchResult(SearchResponse response,SearchParam param){
SearchResult result = new SearchResult();
SearchHits hits = response.getHits();
// 1.检索的所有商品信息
SearchHit[] products = hits.getHits();
List<SkuESModel> esModels = new ArrayList<>();
if(products != null && products.length > 0){
for (SearchHit product : products) {
String sourceAsString = product.getSourceAsString();
// 把json格式的字符串通过fastjson转换为SkuESModel对象
SkuESModel model = JSON.parseObject(sourceAsString, SkuESModel.class);
if(!StringUtils.isEmpty(param.getKeyword())){
// 我们需要设置高亮
HighlightField subTitle = product.getHighlightFields().get("subTitle");
String subTitleHighlight = subTitle.getFragments()[0].string();
model.setSubTitle(subTitleHighlight); // 设置高亮
}
esModels.add(model);
}
}
result.setProducts(esModels);
Aggregations aggregations = response.getAggregations();
// 2.当前商品所涉及到的所有的品牌
ParsedLongTerms brand_agg = aggregations.get("brand_agg");
List<? extends Terms.Bucket> buckets = brand_agg.getBuckets();
// 存储所有品牌的容器
List<SearchResult.BrandVO> brandVOS = new ArrayList<>();
if(buckets!=null && buckets.size() > 0){
for (Terms.Bucket bucket : buckets) {
SearchResult.BrandVO brandVO = new SearchResult.BrandVO();
// 获取品牌的key
String keyAsString = bucket.getKeyAsString();
brandVO.setBrandId(Long.parseLong(keyAsString)); // 设置品牌的编号
// 然后我们需要获取品牌的名称和图片的地址
ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
List<? extends Terms.Bucket> bucketsImg = brand_img_agg.getBuckets();
if(bucketsImg != null && bucketsImg.size() > 0){
String img = bucketsImg.get(0).getKeyAsString();
brandVO.setBrandImg(img);
}
// 获取品牌名称的信息
ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
String breadName = brand_name_agg.getBuckets().get(0).getKeyAsString();
brandVO.setBrandName(breadName);
brandVOS.add(brandVO);
}
}
result.setBrands(brandVOS);
// 3.当前商品涉及到的所有的类别信息
ParsedLongTerms catalog_agg = aggregations.get("catalog_agg");
List<? extends Terms.Bucket> bucketsCatalogs = catalog_agg.getBuckets();
// 创建一个保存所有类别的容器
List<SearchResult.CatalogVO> catalogVOS = new ArrayList<>();
if(bucketsCatalogs != null && bucketsCatalogs.size() > 0){
for (Terms.Bucket bucket : bucketsCatalogs) {
SearchResult.CatalogVO catalogVO = new SearchResult.CatalogVO();
String keyAsString = bucket.getKeyAsString(); // 获取类别的编号
catalogVO.setCatalogId(Long.parseLong(keyAsString));
// 获取类别的名称
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);
}
}
result.setCatalogs(catalogVOS);
// 4.当前商品涉及到的所有的属性信息
ParsedNested attr_agg = aggregations.get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
List<? extends Terms.Bucket> bucketsAttr = attr_id_agg.getBuckets();
List<SearchResult.AttrVo > attrVos = new ArrayList<>();
if(bucketsAttr != null && bucketsAttr.size() > 0){
for (Terms.Bucket bucket : bucketsAttr) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
// 获取属性的编号
String keyAsString = bucket.getKeyAsString();
attrVo.setAttrId(Long.parseLong(keyAsString));
// 又得分别获取 属性的名称 和 属性的值
ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString(); // 属性的名称
attrVo.setAttrName(attrName);
ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
if(attr_value_agg.getBuckets() != null && attr_value_agg.getBuckets().size() > 0 ){
List<String> values = attr_value_agg.getBuckets().stream().map(item -> {
String keyAsString1 = item.getKeyAsString();
return keyAsString1;
}).collect(Collectors.toList());
attrVo.setAttrValue(values);
}
attrVos.add(attrVo);
}
}
result.setAttrs(attrVos);
// 5. 分页信息 当前页 总的记录数 总页数
long total = hits.getTotalHits().value;
result.setTotal(total);// 设置总记录数 6 /5 1+1
result.setPageNum(param.getPageNum()); // 设置当前页
long totalPage = total % ESConstant.PRODUCT_PAGESIZE == 0 ? total / ESConstant.PRODUCT_PAGESIZE : (total / ESConstant.PRODUCT_PAGESIZE + 1);
result.setTotalPages((int)totalPage); // 设置总的页数
return result;
}
```