- 实现搜索页渲染与展示
ly-portal项目中search.html
02、搜索页渲染:微服务搜索功能基本代码
今天我们主要工作就是从Elasticsearch中检索数据,最终展示到搜索页面上。效果如下:
1) 定义接收请求参数的dto对象在ly-pojo-search模块中提供一个DTO接口,用于接收页面传递的搜索参数和分页参数
package com.leyou.search.pojo.dto;
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
private static final Integer DEFAULT_PAGE = 1;// 默认页
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Integer getPage() {
if(page == null){
return DEFAULT_PAGE;
}
// 获取页码时做一些校验,不能小于1
return Math.max(DEFAULT_PAGE, page);
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return DEFAULT_SIZE;
}
}
2) 定义一个返回商品数据的dto对象
在ly-pojo-search模块中定义一个用于返回商品数据的DTO对象
package com.leyou.search.pojo.dto;
import lombok.Data;
@Data
public class GoodsDTO {
private Long id;
private String spuName;
private String subTitle;
private String skus;
}
3)定义一个返回所有搜索结果的对象
package com.leyou.search.pojo.vo; import com.leyou.commom.exception.pojo.PageResult; import lombok.Data; import java.util.Map; @Data public class SearchResult4)编写处理器extends PageResult { private Map filterConditions; }
package com.leyou.search.repository.search.controller;
import com.leyou.search.pojo.dto.GoodsDTO;
import com.leyou.search.pojo.dto.SearchRequest;
import com.leyou.search.pojo.vo.SearchResult;
import com.leyou.search.repository.search.service.impl.SearchServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SearchController {
@Autowired
private SearchServiceImpl searchService;
@PostMapping("/page")
public ResponseEntity> goodsSearchPage(@RequestBody SearchRequest searchRequest){
SearchResult searchResult = searchService.goodsSearchPage(searchRequest);
return ResponseEntity.ok(searchResult);
}
}
5)编写业务层
SearchService类添加方法:
package com.leyou.search.repository.search.service.impl;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.commom.exception.pojo.PageResult;
import com.leyou.commom.utils.BeanHelper;
import com.leyou.commom.utils.HighlightUtils;
import com.leyou.commom.utils.JsonUtils;
import com.leyou.item.ItemClient;
import com.leyou.item.dto.SpuDto;
import com.leyou.item.pojo.*;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.dto.GoodsDTO;
import com.leyou.search.pojo.dto.SearchRequest;
import com.leyou.search.pojo.vo.SearchResult;
import com.leyou.search.repository.SearchRepository;
import org.apache.lucene.queryparser.xml.builders.BooleanQueryBuilder;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SearchServiceImpl {
@Autowired
private SearchRepository searchRepository; //完成基本CRUD操作
@Autowired
private ElasticsearchTemplate esTemplate;//用于高级搜索
@Autowired
private ItemClient itemClient;
public void importData(){
int page = 1;//当前页码
int rows = 100;//每页条数
int totalPage = 1;//总页数
do {
//1.分页查询Spu商品数据,直到全部数据查询完毕
PageResult pageResult = itemClient.spuPageQuery(page, rows, null, true);//注意:导入到ES的数据必须是上架的数据
//2.取出所有商品数据
List spuDTOList = pageResult.getItems();
//3.判空处理
if(CollectionUtils.isNotEmpty(spuDTOList)){
//获取List集合
List goodsList = spuDTOList.stream().map( spuDTO -> buildGoods(spuDTO) ).collect(Collectors.toList());
//保存List数据到ES中
searchRepository.saveAll(goodsList);
}
//获取总页数
totalPage = pageResult.getTotalPage().intValue();
page++;
}while (page<=totalPage);
}
public Goods buildGoods(SpuDto spuDto){
Goods goods = new Goods();
//塞入商品基本信息
goods.setId(spuDto.getId());
goods.setSpuName(spuDto.getName());
goods.setSubTitle(spuDto.getSubTitle());
goods.setCreateTime(System.currentTimeMillis());
goods.setCategoryId(spuDto.getCid3());
goods.setBrandId(spuDto.getBrandId());
//根据spuId查询所有Sku
List skuList = itemClient.findSkusBySpuId(spuDto.getId());
//只取出Sku部分属性
List
03、搜索页渲染:渲染商品列表-后端代码
1)原生查询语句(了解)
在Kibana编写根据条件查询商品列表
# 搜索"手机"关键词
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"all": "手机"
}
}
]
}
},
"_source": {
"includes": ["id","spuName","subTitle","skus"]
},
"from": 0,
"size": 20
}
2)SearchService的itemPageQuery方法
public PageResult itemQueryPage(SearchRequest searchRequest) {
//1.创建本地查询构造器对象
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//2.封装地查询构造器(*)
//1)创建布尔查询构造器
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//2)在布尔查询构造器中追加must条件
boolQueryBuilder.must( QueryBuilders.matchQuery("all",searchRequest.getKey()) );
//3)把布尔查询构造器存入本地查询构造器
queryBuilder.withQuery(boolQueryBuilder);
//4) 设置结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","spuName","subTitle","skus"},null));
//5)设置分页参数
queryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1,searchRequest.getSize()));
//3.执行查询,获取结果
Page pageBean = esTemplate.queryForPage(queryBuilder.build(),Goods.class);
//4.处理结果,并返回
//1)取出Goods集合
List goodsList = pageBean.getContent();
//2)把Goods集合拷贝到GoodsDTO集合
List goodsDTOList = BeanHelper.copyWithCollection(goodsList,GoodsDTO.class);
//3)封装PageResult
PageResult pageResult = new PageResult(
pageBean.getTotalElements(),
Long.valueOf(pageBean.getTotalPages()),
goodsDTOList
);
return pageResult;
}
3)页面查看返回数据
确定前端可以接收到后端返回结果
04、搜索页渲染:渲染商品列表-前端实现 1)前端接收返回数据在之前的loadSearchPage方法中,接收后端返回的结果,注意:
- skus属性为字符串类型,需要转换为Sku的JSON对象
- 需要添加selectedSku属性标记为当前选中的Sku对象
methods:{
//商品搜索方法
loadSearchPage(){
//ajax请求后端
ly.http.post('/search/page',this.searchParams).then(resp=>{
//接收后端返回的数据
//处理resp.data.items
//把items中每个item的skus属性转换为js对象
resp.data.items.forEach(item=>{
item.skus = JSON.parse(item.skus);
//在item中添加一个selectedSku,用于存储当前选中的Sku数据(默认把skus的第一个作为选中的)
item.selectedSku = item.skus[0];
});
this.items = resp.data.items;
this.total = resp.data.total;
this.totalPage = resp.data.totalPage;
}).catch(e=>{
console.log('查询商品失败');
});
}
},
2)渲染数据到页面
删除多余li元素
找到class为goods-list的div,在该div里面把多余的li元素删除,剩下一个li即可
渲染数据注意一个细节:
效果:
05、搜索页渲染:高亮显示 1)后端代码 ly-common导入依赖ly-common添加高亮工具类org.springframework.data spring-data-elasticsearch
在ly-common模块下添加高亮工具类
2)底层REST指令# 搜索"手机"关键词
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "手机",
"fields": ["all","spuName"]
}
}
]
}
},
"_source": {
"includes": ["id","spuName","subTitle","skus"]
},
"from": 0,
"size": 20,
"highlight": {
"pre_tags": "",
"post_tags": "",
"fields": {
"spuName": {}
}
}
}
修改itemPageQuery方法
修改item-search模块的SearchService类的itemPageQuery方法
public PageResult itemQueryPage(SearchRequest searchRequest) {
//1.创建本地查询构造器对象
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//2.封装地查询构造器(*)
//1)创建布尔查询构造器
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//2)在布尔查询构造器中追加must条件
//boolQueryBuilder.must( QueryBuilders.matchQuery("all",searchRequest.getKey()) );
boolQueryBuilder.must( QueryBuilders.multiMatchQuery(searchRequest.getKey(),"all","spuName") );
//3)把布尔查询构造器存入本地查询构造器
queryBuilder.withQuery(boolQueryBuilder);
//4) 设置结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","spuName","subTitle","skus"},null));
//5)设置分页参数
queryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1,searchRequest.getSize()));
//6)设置高亮字段与格式
HighlightUtils.highlightField(queryBuilder,"spuName");
//3.执行查询,获取结果
Page pageBean = esTemplate.queryForPage(queryBuilder.build(),Goods.class,HighlightUtils.highlightBody(Goods.class,"spuName"));
//4.处理结果,并返回
//1)取出Goods集合
List goodsList = pageBean.getContent();
//2)把Goods集合拷贝到GoodsDTO集合
List goodsDTOList = BeanHelper.copyWithCollection(goodsList,GoodsDTO.class);
//3)封装PageResult
PageResult pageResult = new PageResult(
pageBean.getTotalElements(),
Long.valueOf(pageBean.getTotalPages()),
goodsDTOList
);
return pageResult;
}
主要修改的地方如下:
2)前端代码效果:
06、搜索页渲染:渲染固定过滤条件-后端代码 1)原生查询语句(了解)需求:渲染搜索页过滤条件中的分类和品牌这两个固定过滤条件
# 聚合查询
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "手机",
"fields": ["all","spuName"]
}
}
]
}
},
"_source": {
"includes": [""]
},
"aggs": {
"categoryAgg": {
"terms": {
"field": "categoryId"
}
},
"brandAgg": {
"terms": {
"field": "brandId"
}
}
}
}
2)filterConditionsQuery方法
SearchService类:
public Map filterConditionsQuery(SearchRequest searchRequest) {
//1.创建Map集合 使用有序的Map集合:LinkedHashMap
Map filterConditionsMap = new LinkedHashMap<>();
//2.封装Map集合
//2.1 封装固定过滤条件(分类和品牌)
//1)获取搜索条件
NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);
//2)设置结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""},null));
//3)添加聚合条件
String categoryAgg = "categoryAgg";
String brandAgg = "brandAgg";
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));
//4)执行聚合查询
AggregatedPage aggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);
//5)获取所有聚合结果
Aggregations aggregations = aggregatedPage.getAggregations();
//6)取出分类的聚合结果
Terms categoryTerms = aggregations.get(categoryAgg);
//6.1 取出所有结果的分类ID
List categoryIds = categoryTerms.getBuckets()
.stream()
.map(Terms.Bucket::getKeyAsNumber)//以Number类型获取key
.map(Number::longValue) // 使用Number的longValue方法把Number类型转换为Long类型
.collect(Collectors.toList());
//6.2 把分类的ID集合转换为分类对象集合
List categoryList = itemClient.findCategoriesByIds(categoryIds);
//6.3.把品牌对象集合存入Map集合
filterConditionsMap.put("分类",categoryList);
//7)取出品牌的聚合结果
Terms brandTerms = aggregations.get(brandAgg);
//7.1 取出所有结果的品牌ID
List brandIds = brandTerms.getBuckets()
.stream()
.map(Terms.Bucket::getKeyAsNumber)//以Number类型获取key
.map(Number::longValue) // 使用Number的longValue方法把Number类型转换为Long类型
.collect(Collectors.toList());
//7.2 把品牌的ID集合转换为品牌对象集合
List brandList = itemClient.findBrandsByIds(brandIds);
//7.3.把品牌对象集合存入Map集合
filterConditionsMap.put("品牌",brandList);
//2.2 封装动态过滤条件
//3.返回Map集合
return filterConditionsMap;
}
public PageResult itemQueryPage(SearchRequest searchRequest) {
//1.创建本地查询构造器对象
NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);
//4) 设置结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","spuName","subTitle","skus"},null));
//5)设置分页参数
queryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1,searchRequest.getSize()));
//6)设置高亮字段与格式
HighlightUtils.highlightField(queryBuilder,"spuName");
//3.执行查询,获取结果
Page pageBean = esTemplate.queryForPage(queryBuilder.build(),Goods.class,HighlightUtils.highlightBody(Goods.class,"spuName"));
//4.处理结果,并返回
//1)取出Goods集合
List goodsList = pageBean.getContent();
//2)把Goods集合拷贝到GoodsDTO集合
List goodsDTOList = BeanHelper.copyWithCollection(goodsList,GoodsDTO.class);
//3)封装PageResult
PageResult pageResult = new PageResult(
pageBean.getTotalElements(),
Long.valueOf(pageBean.getTotalPages()),
goodsDTOList
);
return pageResult;
}
public NativeSearchQueryBuilder createNativeQueryBuilder(SearchRequest searchRequest) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//2.封装地查询构造器(*)
//1)创建布尔查询构造器
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//2)在布尔查询构造器中追加must条件
//boolQueryBuilder.must( QueryBuilders.matchQuery("all",searchRequest.getKey()) );
boolQueryBuilder.must( QueryBuilders.multiMatchQuery(searchRequest.getKey(),"all","spuName") );
//3)把布尔查询构造器存入本地查询构造器
queryBuilder.withQuery(boolQueryBuilder);
return queryBuilder;
}
3)在ly-item项目提供方法
BrandController:
@GetMapping("/brand/list")
public ResponseEntity> findBrandsByIds(@RequestParam("ids") List ids){
List brands = brandService.findBrandsByIds(ids);
return ResponseEntity.ok(brands);
}
BrandService:
public List4)在Feign接口提供方法findBrandsByIds(List ids) { List brands = brandMapper.selectBatchIds(ids); if(CollectionUtils.isEmpty(brands)){ throw new LyException(ExceptionEnum.BRAND_NOT_FOUND); } return brands; }
ItemClient接口:
@GetMapping("/category/list")
public List findCategoriesByIds(@RequestParam("ids") List ids);
@GetMapping("/brand/list")
public List findBrandsByIds(@RequestParam("ids") List ids);
3)页面查看返回数据
07、搜索页渲染:渲染固定过滤条件-前端代码
1)接收后端返回数据
2)渲染过滤条件页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B7aDlXYT-1657867880638)(assets/1599037979400.png)]
为了页面样式好看,将事先准备好的样式放开。
最终页面效果如下:
# 聚合查询
# 注意:在聚合查询,参与聚合的字段必须是不分词字段
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "手机",
"fields": ["all","spuName"]
}
}
]
}
},
"_source": {
"includes": [""]
},
"aggs": {
"CPU核数": {
"terms": {
"field": "specs.CPU核数.keyword"
}
},
"CPU品牌": {
"terms": {
"field": "specs.CPU品牌.keyword"
}
}
}
}
2)filterConditionsQuery方法
public Map filterConditionsQuery(SearchRequest searchRequest) {
//1.创建Map集合 使用有序的Map集合:LinkedHashMap
Map filterConditionsMap = new LinkedHashMap<>();
//2.封装Map集合
//2.1 封装固定过滤条件(分类和品牌)
//1)获取搜索条件
NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);
//2)设置结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""},null));
//3)添加聚合条件
String categoryAgg = "categoryAgg";
String brandAgg = "brandAgg";
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));
//4)执行聚合查询
AggregatedPage aggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);
//5)获取所有聚合结果
Aggregations aggregations = aggregatedPage.getAggregations();
//6)取出分类的聚合结果
Terms categoryTerms = aggregations.get(categoryAgg);
//6.1 取出所有结果的分类ID
List categoryIds = categoryTerms.getBuckets()
.stream()
.map(Terms.Bucket::getKeyAsNumber)//以Number类型获取key
.map(Number::longValue) // 使用Number的longValue方法把Number类型转换为Long类型
.collect(Collectors.toList());
//6.2 把分类的ID集合转换为分类对象集合
List categoryList = itemClient.findCategoriesByIds(categoryIds);
//6.3.把品牌对象集合存入Map集合
filterConditionsMap.put("分类",categoryList);
//7)取出品牌的聚合结果
Terms brandTerms = aggregations.get(brandAgg);
//7.1 取出所有结果的品牌ID
List brandIds = brandTerms.getBuckets()
.stream()
.map(Terms.Bucket::getKeyAsNumber)//以Number类型获取key
.map(Number::longValue) // 使用Number的longValue方法把Number类型转换为Long类型
.collect(Collectors.toList());
//7.2 把品牌的ID集合转换为品牌对象集合
List brandList = itemClient.findBrandsByIds(brandIds);
//7.3.把品牌对象集合存入Map集合
filterConditionsMap.put("品牌",brandList);
//2.2 封装动态过滤条件(规格参数聚合)
if(categoryIds!=null){
categoryIds.forEach(categoryId -> {
//1)根据分类ID查询参与搜索过滤条件的规格参数
List specParams = itemClient.findSpecParams(null, categoryId, true);
//2)遍历规格参数,逐个添加到聚合条件中
specParams.forEach(specParam -> {
queryBuilder.addAggregation( AggregationBuilders.terms(specParam.getName()).field("specs."+specParam.getName()+".keyword") );
});
//3)执行聚合查询
AggregatedPage specParamAggPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);
Aggregations specParamAggs = specParamAggPage.getAggregations();
//4)遍历规格参数, 获取所有规格参数的聚合结果,把每个规格参数的聚合结果存入Map集合
specParams.forEach(specParam -> {
Terms specParamTerms = specParamAggs.get(specParam.getName());
List
3)页面查看返回数据
09、搜索页渲染:渲染动态过滤条件-前端代码
10、搜索页渲染:过滤条件收起和更多功能 1) 在data中定义控制收起和更多的布尔值{{key}}
data: {
ly,
//前端->后端
searchParams:{
key:"", //搜索关键字
page:1, //页码值
},
//后端->前端
items:[],//接收商品列表
total:1,//接收总记录数
totalPage:1,//接收总页数
filterConditions:{},//接收搜索过滤条件
//控制收起和更多的布尔值
showMore: false
},
2) 给收起和更多按钮绑定点击事件
3) 指定过滤条件的显示条件更多 arrow_drop_down 收起 arrow_drop_up



