4.搜索页面渲染
4.1搜索分析
搜索页面要显示的内容主要分为3块。
1)搜索的数据结果
2)筛选出的数据搜索条件
3)用户已经勾选的数据条件
4.2搜索实现
搜索的业务流程如上图,用户每次搜索的时候,先经过搜索业务工程,搜索业务工程调用搜索微服务工程。
4.2.1搜索工程搭建
(1)引入依赖
在changgou-service_search工程中的pom.xml中引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
(2)静态资源导入(给我留言可以获取)
将资源中的页面资源/所有内容 拷贝到工程的 resources 目录下如下图:
(3) 更改配置文件,在spring下添加内容
4.2.2基础数据渲染
(1)更新SearchController,定义跳转搜索结果页面方法代码如下 :
package com.changgou.search.controller;
import com.changgou.entity.Page;
import com.changgou.search.pojo.SkuInfo;
import com.changgou.search.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Set;
@Controller
@RequestMapping("/search")
public class SearchController {
@Autowired
private SearchService searchService;
@GetMapping("/list")
public String list(@RequestParam Map<String,String> searchMap,Model model){
//特殊符号处理
this.handleSearchMap(searchMap);
//获取查询结果
Map resultMap = searchService.search(searchMap);
model.addAttribute("result",resultMap);
model.addAttribute("searchMap",searchMap);
//封装分页数据并返回
//1.总记录数
//2.当前页
//3.每页显示多少条
Page<SkuInfo> page = new Page<SkuInfo>(
Long.parseLong(String.valueOf( resultMap.get("total"))),
Integer.parseInt(String.valueOf(resultMap.get("pageNum"))),
Page.pageSize
);
model.addAttribute("page",page);
//拼装url
StringBuilder url = new StringBuilder("/search/list");
if (searchMap != null && searchMap.size()>0){
//是由查询条件
url.append("?");
for (String paramKey : searchMap.keySet()) {
if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){
url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");
}
}
//http://localhost:9009/search/list?keywords=手机&spec_网络制式=4G&
String urlString = url.toString();
//去除路径上的最后一个&
urlString=urlString.substring(0,urlString.length()-1);
model.addAttribute("url",urlString);
}else{
model.addAttribute("url",url);
}
return "search";
}
@GetMapping
@ResponseBody
public Map search(@RequestParam Map<String,String> searchMap){
//特殊符号处理
this.handleSearchMap(searchMap);
Map searchResult = searchService.search(searchMap);
return searchResult;
}
private void handleSearchMap(Map<String, String> searchMap) {
Set<Map.Entry<String, String>> entries = searchMap.entrySet();
for (Map.Entry<String, String> entry : entries) {
if (entry.getKey().startsWith("spec_")){
searchMap.put(entry.getKey(),entry.getValue().replace("+","%2B"));
}
}
}
}
(2) 业务层代码
package com.changgou.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.changgou.search.pojo.SkuInfo;
import com.changgou.search.service.SearchService;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Override
public Map search(Map<String, String> searchMap) {
Map<String,Object> resultMap = new HashMap<>();
//构建查询
if (searchMap != null){
//构建查询条件封装对象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//按照关键字查询
if (StringUtils.isNotEmpty(searchMap.get("keywords"))){
boolQuery.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")).operator(Operator.AND));
}
//按照品牌进行过滤查询
if (StringUtils.isNotEmpty(searchMap.get("brand"))){
boolQuery.filter(QueryBuilders.termQuery("brandName",searchMap.get("brand")));
}
//按照规格进行过滤查询
for (String key : searchMap.keySet()) {
if (key.startsWith("spec_")){
String value = searchMap.get(key).replace("%2B","+");
//spec_网络制式
boolQuery.filter(QueryBuilders.termQuery(("specMap."+key.substring(5)+".keyword"),value));
}
}
//按照价格进行区间过滤查询
if (StringUtils.isNotEmpty(searchMap.get("price"))){
String[] prices = searchMap.get("price").split("-");
// 0-500 500-1000
if (prices.length == 2){
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(prices[1]));
}
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(prices[0]));
}
nativeSearchQueryBuilder.withQuery(boolQuery);
//按照品牌进行分组(聚合)查询
String skuBrand="skuBrand";
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));
//按照规格进行聚合查询
String skuSpec="skuSpec";
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));
//开启分页查询
String pageNum = searchMap.get("pageNum"); //当前页
String pageSize = searchMap.get("pageSize"); //每页显示多少条
if (StringUtils.isEmpty(pageNum)){
pageNum ="1";
}
if (StringUtils.isEmpty(pageSize)){
pageSize="30";
}
//设置分页
//第一个参数:当前页 是从0开始
//第二个参数:每页显示多少条
nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum)-1,Integer.parseInt(pageSize)));
//按照相关字段进行排序查询
// 1.当前域 2.当前的排序操作(升序ASC,降序DESC)
if (StringUtils.isNotEmpty(searchMap.get("sortField")) && StringUtils.isNotEmpty(searchMap.get("sortRule"))){
if ("ASC".equals(searchMap.get("sortRule"))){
//升序
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).order(SortOrder.ASC));
}else{
//降序
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).order(SortOrder.DESC));
}
}
//设置高亮域以及高亮的样式
HighlightBuilder.Field field = new HighlightBuilder.Field("name")//高亮域
.preTags("<span style='color:red'>")//高亮样式的前缀
.postTags("</span>");//高亮样式的后缀
nativeSearchQueryBuilder.withHighlightFields(field);
//开启查询
/**
* 第一个参数: 条件构建对象
* 第二个参数: 查询操作实体类
* 第三个参数: 查询结果操作对象
*/
//封装查询结果
AggregatedPage<SkuInfo> resultInfo = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
//查询结果操作
List<T> list = new ArrayList<>();
//获取查询命中结果数据
SearchHits hits = searchResponse.getHits();
if (hits != null){
//有查询结果
for (SearchHit hit : hits) {
//SearchHit转换为skuinfo
SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null && highlightFields.size()>0){
//替换数据
skuInfo.setName(highlightFields.get("name").getFragments()[0].toString());
}
list.add((T) skuInfo);
}
}
return new AggregatedPageImpl<T>(list,pageable,hits.getTotalHits(),searchResponse.getAggregations());
}
});
//封装最终的返回结果
//总记录数
resultMap.put("total",resultInfo.getTotalElements());
//总页数
resultMap.put("totalPages",resultInfo.getTotalPages());
//数据集合
resultMap.put("rows",resultInfo.getContent());
//封装品牌的分组结果
StringTerms brandTerms = (StringTerms) resultInfo.getAggregation(skuBrand);
List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
resultMap.put("brandList",brandList);
//封装规格分组结果
StringTerms specTerms= (StringTerms) resultInfo.getAggregation(skuSpec);
List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
resultMap.put("specList",this.formartSpec(specList));
//当前页
resultMap.put("pageNum",pageNum);
return resultMap;
}
return null;
}
/**
* 原有数据
* [
* "{'颜色': '黑色', '尺码': '平光防蓝光-无度数电脑手机护目镜'}",
* "{'颜色': '红色', '尺码': '150度'}",
* "{'颜色': '黑色', '尺码': '150度'}",
* "{'颜色': '黑色'}",
* "{'颜色': '红色', '尺码': '100度'}",
* "{'颜色': '红色', '尺码': '250度'}",
* "{'颜色': '红色', '尺码': '350度'}",
* "{'颜色': '黑色', '尺码': '200度'}",
* "{'颜色': '黑色', '尺码': '250度'}"
* ]
*
* 需要的数据格式
* {
* 颜色:[黑色,红色],
* 尺码:[100度,150度]
* }
*/
public Map<String,Set<String>> formartSpec(List<String> specList){
Map<String,Set<String>> resultMap = new HashMap<>();
if (specList!=null && specList.size()>0){
for (String specJsonString : specList) {
//将json数据转换为map
Map<String,String> specMap = JSON.parseObject(specJsonString, Map.class);
for (String specKey : specMap.keySet()) {
Set<String> specSet = resultMap.get(specKey);
if (specSet == null){
specSet = new HashSet<String>();
}
//将规格的值放入set中
specSet.add(specMap.get(specKey));
//将set放入map中
resultMap.put(specKey,specSet);
}
}
}
return resultMap;
}
}
(3)页面渲染
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<title>产品列表页</title>
<link rel="icon" href="/img/favicon.ico">
<link rel="stylesheet" type="text/css" href="/css/all.css" />
<link rel="stylesheet" type="text/css" href="/css/pages-list.css" />
</head>
<body>
<!-- 头部栏位 -->
<!--页面顶部-->
<div id="nav-bottom">
<!--顶部-->
<div class="nav-top">
<div class="top">
<div class="py-container">
<div class="shortcut">
<ul class="fl">
<li class="f-item">畅购欢迎您!</li>
<li class="f-item">请<a href="login.html" target="_blank">登录</a> <span><a href="register.html" target="_blank">免费注册</a></span></li>
</ul>
<div class="fr typelist">
<ul class="types">
<li class="f-item"><span>我的订单</span></li>
<li class="f-item"><span><a href="cart.html" target="_blank">我的购物车</a></span></li>
<li class="f-item"><span><a href="home.html" target="_blank">我的畅购</a></span></li>
<li class="f-item"><span>畅购会员</span></li>
<li class="f-item"><span>企业采购</span></li>
<li class="f-item"><span>关注畅购</span></li>
<li class="f-item"><span><a href="cooperation.html" target="_blank">合作招商</a></span></li>
<li class="f-item"><span><a href="shoplogin.html" target="_blank">商家后台</a></span></li>
<li class="f-item"><span>网站导航</li>
</ul>
</div>
</div>
</div>
</div>
<!--头部-->
<div class="header">
<div class="py-container">
<div class="yui3-g Logo">
<div class="yui3-u Left logoArea">
<a class="logo-bd" title="畅购" href="index.html" target="_blank"></a>
</div>
<div class="yui3-u Rit searchArea">
<div class="search">
<form th:action="@{/search/list}" class="sui-form form-inline">
<div class="input-append">
<input th:type="text" id="autocomplete" name="keywords" th:value="${searchMap.keywords}" class="input-error input-xxlarge" />
<button class="sui-btn btn-xlarge btn-danger" th:type="submit">搜索</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 商品分类导航 -->
<div class="typeNav">
<div class="py-container">
<div class="yui3-g NavList">
<div class="all-sorts-list">
<div class="yui3-u Left all-sort">
<h4>全部商品分类</h4>
</div>
<div class="sort">
<div class="all-sort-list2">
<div class="item bo">
<h3><a href="">图书、音像、数字商品</a></h3>
<div class="item-list clearfix">
<div class="subitem">
<dl class="fore1">
<dt><a href="">电子书</a></dt>
<dd><a href="">免费</a><a href="">小说</a></em><a href="">励志与成功</a><em><a href="">婚恋/两性</a></em><em><a href="">文学</a></em><em><a href="">经管</a></em><em><a href="">畅读VIP</a></em></dd>
</dl>
</div>
</div>
</div>
<div class="item">
<h3><a href="">家用电器</a></h3>
<div class="item-list clearfix">
<div class="subitem">
<dl class="fore1">
<dt><a href="">电子书1</a></dt>
<dd><em><a href="">免费</a></em><em><a href="">小说</a></em><em><a href="">励志与成功</a></em><em><a href="">婚恋/两性</a></em><em><a href="">文学</a></em><em><a href="">经管</a></em><em><a href="">畅读VIP</a></em></dd>
</dl>
<dl class="fore2">
<dt><a href="">数字音乐</a></dt>
<dd><em><a href="">通俗流行</a></em><em><a href="">古典音乐</a></em><em><a href="">摇滚说唱</a></em><em><a href="">爵士蓝调</a></em><em><a href="">乡村民谣</a></em><em><a href="">有声读物</a></em></dd>
</dl>
<dl class="fore3">
<dt><a href="">音像</a></dt>
<dd><em><a href="">音乐</a></em><em><a href="">影视</a></em><em><a href="">教育音像</a></em><em><a href="">游戏</a></em></dd>
</dl>
<dl class="fore4">
<dt>文艺</dt>
<dd><em><a href="">小说</a></em><em><a href="">文学</a></em><em><a href="">青春文学</a></em><em><a href="">传记</a></em><em><a href="">艺术</a></em></dd>
</dl>
<dl class="fore5">
<dt>人文社科</dt>
<dd><em><a href="">历史</a></em><em><a href="">心理学</a></em><em><a href="">政治/军事</a></em><em><a href="">国学/古籍</a></em><em><a href="">哲学/宗教</a></em><em><a href="">社会科学</a></em></dd>
</dl>
<dl class="fore6">
<dt>经管励志</dt>
<dd><em><a href="">经济</a></em><em><a href="">金融与投资</a></em><em><a href="">管理</a></em><em><a href="">励志与成功</a></em></dd>
</dl>
<dl class="fore7">
<dt>生活</dt>
<dd><em><a href="">家庭与育儿</a></em><em><a href="">旅游/地图</a></em><em><a href="">烹饪/美食</a></em><em><a href="">时尚/美妆</a></em