乐优商城学习Day07:
注意:此次代码都是在第六天的基础上
第六天的链接如下:
https://blog.csdn.net/zcylxzyh/article/details/99943700
此次笔记内容主要为:
1.elasticsearch复杂查询,聚合
2.搭建搜索微服务
3.搜索的页面渲染
下面开始第七天的学习:
1.elasticsearch复杂查询,聚合
复杂玩法:
Elasticsearch的原生查询:
在昨天的es-demo中的EsTest中加入:
@Test
public void testQuery(){
//创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id","title","price"},null));
//添加查询条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title","小米手机"));
//排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
//分页
queryBuilder.withPageable(PageRequest.of(0,2));
Page<Item> result = repository.search(queryBuilder.build());
long total = result.getTotalElements();
System.out.println("total = " + total);
int totalPages = result.getTotalPages();
System.out.println("totalPages = " + totalPages);
List<Item> list = result.getContent();
for (Item item : list) {
System.out.println("item = " + item);
}
}
结果:
聚合:
继续加入:
//聚合
@Test
public void testAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
String aggName = "popularBrand";
//聚合
queryBuilder.addAggregation(AggregationBuilders.terms(aggName).field("brand"));
//查询并返回待聚合结果
AggregatedPage<Item> result = template.queryForPage(queryBuilder.build(), Item.class);
//解析聚合
Aggregations aggs = result.getAggregations();
//获取指定名称的聚合
StringTerms terms = aggs.get(aggName);
//获取桶
List<StringTerms.Bucket> buckets = terms.getBuckets();
for (StringTerms.Bucket bucket : buckets) {
System.out.println("bucket.getKeyAsString() = " + bucket.getKeyAsString());
System.out.println("bucket.getDocCount() = " + bucket.getDocCount());
}
}
结果:
第一部分结束。
2.搭建搜索微服务
2.1 搜索微服务
首先创建出搜索微服务:
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-search</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.service</groupId>
<artifactId>ly-item-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
配置文件:
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.133.128:9300
jackson:
default-property-inclusion: non_null
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
启动类:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchApplication {
public static void main(String[] args) {
SpringApplication.run(LySearchApplication.class, args);
}
}
2.2 索引库
接下来,我们需要商品数据导入索引库,便于用户搜索。
那么问题来了,我们有SPU和SKU,到底如何保存到索引库?
再来看看页面中有什么数据:
最终的数据结构:
我们创建一个类,封装要保存到索引库的数据,并设置映射属性
Goods:
package com.leyou.search.pojo;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
import java.util.Map;
import java.util.Set;
@Data
@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
@Id
private Long id; // spuId
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌
@Field(type = FieldType.Keyword, index = false)
private String subTitle;// 卖点
private Long brandId;// 品牌id
private Long cid1;// 1级分类id
private Long cid2;// 2级分类id
private Long cid3;// 3级分类id
private Date createTime;// 创建时间
private Set<Long> price;// 价格
@Field(type = FieldType.Keyword, index = false)
private String skus;// sku信息的json结构
private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
}
索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。
先思考我们需要的数据:
- SPU信息
- SKU信息
- SPU的详情
- 商品分类名称(拼接all字段)
再思考我们需要哪些服务:
- 第一:分批查询spu的服务,已经写过。
- 第二:根据spuId查询sku的服务,已经写过
- 第三:根据spuId查询SpuDetail的服务,已经写过
- 第四:根据商品分类id,查询商品分类名称,没写过
- 第五:根据商品品牌id,查询商品的品牌,没写过
因此我们需要额外提供一个查询商品分类名称的接口。
在CategoryController中添加:
//根据id查询商品分类
@GetMapping("list/ids")
public ResponseEntity<List<Category>> queryCategoryByIds(@RequestParam("ids")List<Long> ids){
return ResponseEntity.ok(categoryService.queryByIds(ids));
}
对应的service:
public List<Category> queryByIds(List<Long> ids){
List<Category> list = categoryMapper.selectByIdList(ids);
if (CollectionUtils.isEmpty(list)) {
//返回404
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
}
return list;
}
BrandController中添加:
//根据id查询品牌
@GetMapping("{id}")
public ResponseEntity<Brand> queryBrandById(@PathVariable("id") Long id){
return ResponseEntity.ok(brandService.queryById(id));
}
对应的service
public Brand queryById(Long id){
Brand brand = brandMapper.selectByPrimaryKey(id);
if(brand ==null){
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
return brand;
}
然后:操作leyou-search工程
现在,我们要在搜索微服务调用商品微服务的接口。
方法:
- 我们的服务提供方不仅提供实体类,还要提供api接口声明
- 调用方不用字自己编写接口方法声明,直接继承提供方给的Api接口即可,
第一步:服务的提供方在leyou-item-interface中提供API接口,并编写接口声明:需要引入springMVC及leyou-common的依赖:
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ly-item</artifactId>
<groupId>com.leyou.service</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-item-interface</artifactId>
<dependencies>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-core</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
BrandApi:
package com.leyou.item.api;
import com.leyou.item.pojo.Brand;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
public interface BrandApi {
@GetMapping("brand/{id}")
Brand queryBrandById(@PathVariable("id") Long id);
}
CategoryApi:
package com.leyou.item.api;
import com.leyou.item.pojo.Category;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
public interface CategoryApi {
@GetMapping("category/list/ids")
List<Category> queryCategoryByIds(@RequestParam("ids")List<Long> ids);
}
GoodsApi :
package com.leyou.item.api;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
public interface GoodsApi {
//根据spu查询下面的所有sku
@GetMapping("sku/list")
List<Sku> querySkuBySpuId(@RequestParam("id") Long spuId);
@GetMapping("/spu/page")
PageResult<Spu> querySpuByPage(
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "saleable",required = false) Boolean saleable,
@RequestParam(value = "key",required = false) String key
);
//根据spu的id查询详情detail
@GetMapping("/spu/detail/{id}")
SpuDetail queryDetailById(@PathVariable("id") Long spuId);
}
SpecificationApi:
package com.leyou.item.api;
import com.leyou.item.pojo.SpecParam;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
public interface SpecificationApi {
@GetMapping("spec/params")
List<SpecParam> queryParamList(
@RequestParam(value = "gid", required = false) Long gid,
@RequestParam(value = "cid", required = false) Long cid,
@RequestParam(value = "searching", required = false) Boolean searching
);
}
第二步:在调用方leyou-search中编写FeignClient,但不要写方法声明了,直接继承leyou-item-interface提供的api接口:
BrandClient
package com.leyou.search.client;
import com.leyou.item.api.BrandApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}
CategoryClient
package com.leyou.search.client;
import com.leyou.item.api.CategoryApi;
import com.leyou.item.pojo.Category;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface CategoryClient extends CategoryApi {
}
GoodsClient
package com.leyou.search.client;
import com.leyou.item.api.GoodsApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}
SpecificationClient
package com.leyou.search.client;
import com.leyou.item.api.SpecificationApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}
导入数据:
导入数据只做一次,以后的更新删除等操作通过消息队列来操作索引库
创建GoodsRepository
GoodsRepository
package com.leyou.search.repository;
import com.leyou.search.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}
创建索引:
我们新建一个测试类,在里面进行数据的操作:
创建索引库:
GoodsRepositoryTest:
package com.leyou.search.repository;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Spu;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.service.SearchService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SearchService searchService;
@Test
public void testCreateIndex(){
template.createIndex(Goods.class);
template.putMapping(Goods.class);
}
@Test
public void loadData(){
int page = 1;
int rows = 100;
int size = 0;
do {
//查询spu信息
PageResult<Spu> result = goodsClient.querySpuByPage(page, rows, true, null);
List<Spu> spuList = result.getItems();
if(CollectionUtils.isEmpty(spuList)){
break;
}
//构建成goods
List<Goods> goodsList = spuList.stream().map(searchService::buildGoods).collect(Collectors.toList());
//存入索引库
goodsRepository.saveAll(goodsList);
//翻页
page++;
size = spuList.size();
}while (size == 100);
}
}
2.3 导入数据
导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此我们先编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods
SearchService:
package com.leyou.search.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.JsonUtils;
import com.leyou.common.utils.NumberUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.QueryBuilders;
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.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specClient;
@Autowired
private GoodsRepository repository;
public Goods buildGoods(Spu spu) {
Long spuId = spu.getId();
//查询分类
List<Category> categories = categoryClient.queryCategoryByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
if (CollectionUtils.isEmpty(categories)) {
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
}
//从List集合获取到名字
List<String> names = categories.stream().map(Category::getName).collect(Collectors.toList());
//查询品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
if (brand == null) {
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
// 搜索字段
String all = spu.getTitle() + StringUtils.join(names, " ") + brand.getName();
//查询sku
List<Sku> skuList = goodsClient.querySkuBySpuId(spuId);
if (CollectionUtils.isEmpty(skuList)) {
throw new LyException(ExceptionEnum.GOODS_SKU_NOT_FOUND);
}
//对sku进行处理
List<Map<String, Object>> skus = new ArrayList<>();
//价格集合
Set<Long> priceList = new HashSet<>();
for (Sku sku : skuList) {
Map<String, Object> map = new HashMap<>();
map.put("id", sku.getId());
map.put("title", sku.getTitle());
map.put("price", sku.getPrice());
map.put("image", StringUtils.substringBefore(sku.getImages(), ","));
skus.add(map);
//处理价格
priceList.add(sku.getPrice());
}
//查询规格参数
List<SpecParam> params = specClient.queryParamList(null, spu.getCid3(), true);
if (CollectionUtils.isEmpty(params)) {
throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
}
//查询商品详情
SpuDetail spuDetail = goodsClient.queryDetailById(spuId);
//获取通用规格参数
Map<Long, String> generSpec = JsonUtils.toMap(spuDetail.getGenericSpec(), Long.class, String.class);
//获取特有规格参数
Map<Long, List<String>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
});
//规格参数
Map<String, Object> specs = new HashMap<>();
for (SpecParam param : params) {
//规格名称
String key = param.getName();
Object value = "";
//判断是否是通用规格
if(param.getGeneric()){
value = generSpec.get(param.getId());
//判断是否是数值类型
if(param.getNumeric()){
//处理成段
value = chooseSegment(value.toString(),param);
}
}else {
value = specialSpec.get(param.getId());
}
//存入map
specs.put(key,value);
}
//构建goods对象
Goods goods = new Goods();
goods.setBrandId(spu.getBrandId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(spu.getCreateTime());
goods.setId(spuId);
goods.setAll(all);// 搜索字段,包含标题,分类,品牌,规格等
goods.setPrice(priceList);//所有sku的价格集合
goods.setSkus(JsonUtils.toString(skus));//所有sku的集合的json格式
goods.setSpecs(specs);//所有的可搜索的规格参数
goods.setSubTitle(spu.getSubTitle());
return goods;
}
private String chooseSegment(String value, SpecParam p) {
double val = NumberUtils.toDouble(value);
String result = "其它";
// 保存数值段
for (String segment : p.getSegments().split(",")) {
String[] segs = segment.split("-");
// 获取数值范围
double begin = NumberUtils.toDouble(segs[0]);
double end = Double.MAX_VALUE;
if(segs.length == 2){
end = NumberUtils.toDouble(segs[1]);
}
// 判断是否在范围内
if(val >= begin && val < end){
if(segs.length == 1){
result = segs[0] + p.getUnit() + "以上";
}else if(begin == 0){
result = segs[1] + p.getUnit() + "以下";
}else{
result = segment + p.getUnit();
}
break;
}
}
return result;
}
public PageResult<Goods> search(SearchRequest request) {
int page = request.getPage() - 1;
int size = request.getSize();
//创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//0 结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id","subTitle","skus"},null));
//1 分页
queryBuilder.withPageable(PageRequest.of(page,size));
//2 过滤
queryBuilder.withQuery(QueryBuilders.matchQuery("all",request.getKey()));
//3 查询
Page<Goods> result = repository.search(queryBuilder.build());
//4 解析结果
long total = result.getTotalElements();
int totalPage = result.getTotalPages();
List<Goods> goodsList = result.getContent();
return new PageResult<>(total,totalPage,goodsList);
}
}
然后编写一个测试类,循环查询Spu,然后调用IndexService中的方法,把SPU变为Goods,然后写入索引库:
代码在上面Test中:
2.3 实现基本搜索
首先,在leyou-gateway中
添加网关映射:
server:
port: 10010
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
prefix: /api # 添加路由前缀
routes:
item-service: /item/**
search-service: /search/**
upload-service:
path: /upload/**
serviceId: upload-service
strip-prefix: false
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 5000 # 熔断超时时长:5000ms
ribbon:
ConnectTimeout: 1000 # 连接超时时间(ms)
ReadTimeout: 3500 # 通信超时时间(ms)
MaxAutoRetriesNextServer: 0 # 同一服务不同实例的重试次数
MaxAutoRetries: 0 # 同一实例的重试次数
添加允许信任域名:
然后实现后台接口:
controller
首先分析几个问题:
- 请求方式:Post
- 请求路径:/search/page,不过前面的/search应该是网关的映射路径,因此真实映射路径page,代表分页查询
- 请求参数:json格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此我们可以用一个对象来接收请求的json数据:
package com.leyou.search.pojo;
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private static final int DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
private static final int 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;
}
}
返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息,我们可以使用之前定义的PageResult类
package com.leyou.search.web;
import com.leyou.common.vo.PageResult;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.service.SearchService;
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 SearchService searchService;
//搜索功能
@PostMapping("page")
public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest request) {
return ResponseEntity.ok(searchService.search(request));
}
}
service:
public PageResult<Goods> search(SearchRequest request) {
int page = request.getPage() - 1;
int size = request.getSize();
//创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//0 结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id","subTitle","skus"},null));
//1 分页
queryBuilder.withPageable(PageRequest.of(page,size));
//2 过滤
queryBuilder.withQuery(QueryBuilders.matchQuery("all",request.getKey()));
//3 查询
Page<Goods> result = repository.search(queryBuilder.build());
//4 解析结果
long total = result.getTotalElements();
int totalPage = result.getTotalPages();
List<Goods> goodsList = result.getContent();
return new PageResult<>(total,totalPage,goodsList);
}
注意点:我们要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。
解决办法很简单,在leyou-search的application.yml中添加一行配置,json处理时忽略空值:
spring:
jackson:
default-property-inclusion: non_null # 配置json处理时忽略空值
至此,第二部分结束。
3.搜索的页面渲染
页面渲染部分:
<div class="goods-list">
<ul class="yui3-g">
<li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
<div class="list-wrap">
<div class="p-img">
<a href="item.html" target="_blank"><img :src="goods.selectedSku.image" height="200"/></a>
<ul class="skus">
<li :class="{selected: goods.selectedSku.id === sku.id}" @mouseenter="goods.selectedSku=sku"
v-for="sku in goods.skus" :key="sku.id">
<img :src="sku.image">
</li>
</ul>
</div>
<div class="clearfix"></div>
<div class="price">
<strong>
<em>¥</em>
<i v-text="ly.formatPrice(goods.selectedSku.price)"></i>
</strong>
</div>
<div class="attr">
<em v-text="goods.selectedSku.title.substring(0, 21) + '..'"></em>
</div>
<div class="cu">
<em><span>促</span>{
{goods.subTitle.substring(0,15) + '..'}}</em>
</div>
<div class="commit">
<i class="command">已有2000人评价</i>
</div>
<div class="operate">
<a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a>
</div>
</div>
</li>
</ul>
</div>
提供和后台交互的代码部分:
var vm = new Vue({
el: "#searchApp",
data: {
ly,
search:{},
goodsList:[],
totalPage: 0,
total: 0,
selectedSku:{}
},
created(){
//获取请求参数
const search = ly.parse(location.search.substring(1));
this.search = search;
//发送后台
this.loadData();
},
methods:{
loadData(){
//发送到后台
ly.http.post("/search/page",this.search).then(resp =>{
//保存分页结果
this.total = resp.data.total;
this.totalPage = resp.data.totalPage;
//保存当前页商品
resp.data.items.forEach(goods =>{
//把json处理成JS对象
goods.skus = JSON.parse(goods.skus);
//初始化被选中的sku
goods.selectedSku = goods.skus[0];
})
this.goodsList = resp.data.items;
}).catch(error =>{
})
}
},
components:{
lyTop: () => import("./js/pages/top.js")
}
});
整个search的代码:
<!DOCTYPE html>
<html xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>
<title>乐优商城--商品搜索结果页</title>
<link rel="icon" href="assets/img/favicon.ico">
<link href='./css/material.css' rel="stylesheet">
<link href="./css/vuetify.min.css" rel="stylesheet">
<script src="./js/vue/vue.js"></script>
<script src="./js/vue/vuetify.js"></script>
<script src="./js/axios.min.js"></script>
<script src="./js/common.js"></script>
<link rel="stylesheet" type="text/css" href="css/webbase.css"/>
<link rel="stylesheet" type="text/css" href="css/pages-list.css"/>
<link rel="stylesheet" type="text/css" href="css/widget-cartPanelView.css"/>
<style type="text/css">
* {
box-sizing: unset;
}
.btn-arrow, .btn-arrow:visited, .btn-arrow:link, .btn-arrow:active {
width: 46px;
height: 23px;
border: 1px solid #DDD;
background: #FFF;
line-height: 23px;
font-family: "\5b8b\4f53";
text-align: center;
font-size: 16px;
color: #AAA;
text-decoration: none;
out-line: none
}
.btn-arrow:hover {
background-color: #1299ec;
color: whitesmoke;
}
.top-pagination {
display: block;
padding: 3px 15px;
font-size: 11px;
font-weight: 700;
line-height: 18px;
color: #999;
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
text-transform: uppercase;
float: right;
margin-top: 6px
}
.top-pagination span {
margin-right: 10px;
}
/* .logo-list li{
padding:8px;
}
.logo-list li:hover{
background-color: #f3f3f3;
}*/
.type-list a:hover{
color: #1299ec;
}
.skus {
list-style: none;
}
.skus li{
list-style: none;
display: inline-block;
float: left;
margin-left: 2px;
border: 2px solid #f3f3f3;
}
.skus li.selected{
border: 2px solid #dd1144;
}
.skus img{
width: 25px;
height: 25px;
}
</style>
<script type="text/javascript" src="plugins/jquery/jquery.min.js"></script>
</head>
<body >
<div id="searchApp">
<div id="nav-bottom">
<ly-top/>
</div>
<!--list-content-->
<div class="main" >
<div class="py-container">
<div class="bread">
<!--面包屑-->
<ul class="fl sui-breadcrumb">
<li><span>全部结果:</span></li>
<li ><a href="#">手机</a></li>
<li ><span>手机通讯</span></li>
</ul>
<!--已选择过滤项-->
<ul class="tags-choose">
<li class="tag">
品牌:<span style="color: red">apple</span>
<i class="sui-icon icon-tb-close"></i>
</li>
<li class="tag">
内存:<span style="color: red">4G</span>
<i class="sui-icon icon-tb-close"></i>
</li>
</ul>
<div class="clearfix"></div>
</div>
<!--selector-->
<div class="clearfix selector">
<div class="type-wrap">
<div class="fl key">分类</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>手机</a>
</li>
<li>
<a>儿童手表</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap logo">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<li><img src="img/_/phone01.png" /></li>
<li><img src="img/_/phone02.png" /></li>
<li><img src="img/_/phone03.png" /></li>
<li><img src="img/_/phone04.png" /></li>
<li><img src="img/_/phone05.png" /></li>
<li><img src="img/_/phone06.png" /></li>
<li><img src="img/_/phone07.png" /></li>
<li><img src="img/_/phone08.png" /></li>
<li><img src="img/_/phone09.png" /></li>
<li><img src="img/_/phone10.png" /></li>
<li><img src="img/_/phone11.png" /></li>
<li><img src="img/_/phone12.png" /></li>
<li><img src="img/_/phone13.png" /></li>
<li><img src="img/_/phone14.png" /></li>
<li><img src="img/_/phone05.png" /></li>
<li><img src="img/_/phone06.png" /></li>
<li><img src="img/_/phone07.png" /></li>
<li style="text-align: center"><a style="line-height: 30px; font-size: 12px" href="#">黑马</a></li>
</ul>
</div>
<div class="fl ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
</div>
</div>
<div class="type-wrap">
<div class="fl key">网络制式</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>GSM(移动/联通2G)</a>
</li>
<li>
<a>电信2G</a>
</li>
<li>
<a>电信3G</a>
</li>
<li>
<a>移动3G</a>
</li>
<li>
<a>联通3G</a>
</li>
<li>
<a>联通4G</a>
</li>
<li>
<a>电信3G</a>
</li>
<li>
<a>移动3G</a>
</li>
<li>
<a>联通3G</a>
</li>
<li>
<a>联通4G</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap">
<div class="fl key">显示屏尺寸</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>4.0-4.9英寸</a>
</li>
<li>
<a>4.0-4.9英寸</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap">
<div class="fl key">摄像头像素</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>1200万以上</a>
</li>
<li>
<a>800-1199万</a>
</li>
<li>
<a>1200-1599万</a>
</li>
<li>
<a>1600万以上</a>
</li>
<li>
<a>无摄像头</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap">
<div class="fl key">价格</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>0-500元</a>
</li>
<li>
<a>500-1000元</a>
</li>
<li>
<a>1000-1500元</a>
</li>
<li>
<a>1500-2000元</a>
</li>
<li>
<a>2000-3000元 </a>
</li>
<li>
<a>3000元以上</a>
</li>
</ul>
</div>
<div class="fl ext">
</div>
</div>
<div class="type-wrap" style="text-align: center">
<v-btn small flat>
更多<v-icon>arrow_drop_down</v-icon>
</v-btn>
<v-btn small="" flat>
收起<v-icon>arrow_drop_up</v-icon>
</v-btn>
</div>
</div>
<!--details-->
<div class="details">
<div class="sui-navbar">
<div class="navbar-inner filter">
<ul class="sui-nav">
<li class="active">
<a href="#">综合</a>
</li>
<li>
<a href="#">销量</a>
</li>
<li>
<a href="#">新品</a>
</li>
<li>
<a href="#">评价</a>
</li>
<li>
<a href="#">价格</a>
</li>
</ul>
<div class="top-pagination">
<span>共 <i style="color: #222;">7600+</i> 商品</span>
<span><i style="color: red;">2</i>/123</span>
<a class="btn-arrow" href="#" style="display: inline-block"><</a>
<a class="btn-arrow" href="#" style="display: inline-block">></a>
</div>
</div>
</div>
<div class="goods-list">
<ul class="yui3-g">
<li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
<div class="list-wrap">
<div class="p-img">
<a href="item.html" target="_blank"><img :src="goods.selectedSku.image" height="200"/></a>
<ul class="skus">
<li :class="{selected: goods.selectedSku.id === sku.id}" @mouseenter="goods.selectedSku=sku"
v-for="sku in goods.skus" :key="sku.id">
<img :src="sku.image">
</li>
</ul>
</div>
<div class="clearfix"></div>
<div class="price">
<strong>
<em>¥</em>
<i v-text="ly.formatPrice(goods.selectedSku.price)"></i>
</strong>
</div>
<div class="attr">
<em v-text="goods.selectedSku.title.substring(0, 21) + '..'"></em>
</div>
<div class="cu">
<em><span>促</span>{
{goods.subTitle.substring(0,15) + '..'}}</em>
</div>
<div class="commit">
<i class="command">已有2000人评价</i>
</div>
<div class="operate">
<a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a>
</div>
</div>
</li>
</ul>
</div>
<div class="fr">
<div class="sui-pagination pagination-large">
<ul>
<li class="prev disabled">
<a href="#">«上一页</a>
</li>
<li class="active">
<a href="#">1</a>
</li>
<li>
<a href="#">2</a>
</li>
<li>
<a href="#">3</a>
</li>
<li>
<a href="#">4</a>
</li>
<li>
<a href="#">5</a>
</li>
<li class="dotted"><span>...</span></li>
<li class="next">
<a href="#">下一页»</a>
</li>
</ul>
<div><span>共10页 </span><span>
到第
<input type="text" class="page-num">
页 <button class="page-confirm" onclick="alert(1)">确定</button></span></div>
</div>
</div>
</div>
<!--hotsale-->
<div class="clearfix hot-sale">
<h4 class="title">热卖商品</h4>
<div class="hot-list">
<ul class="yui3-g">
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_01.png"/>
</div>
<div class="attr">
<em>Apple苹果iPhone 6s (A1699)</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4088.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有700人评价</i>
</div>
</div>
</li>
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_03.png"/>
</div>
<div class="attr">
<em>金属A面,360°翻转,APP下单省300!</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4088.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有700人评价</i>
</div>
</div>
</li>
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_04.png"/>
</div>
<div class="attr">
<em>256SSD商务大咖,完爆职场,APP下单立减200</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4068.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有20人评价</i>
</div>
</div>
</li>
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_02.png"/>
</div>
<div class="attr">
<em>Apple苹果iPhone 6s (A1699)</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4088.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有700人评价</i>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var vm = new Vue({
el: "#searchApp",
data: {
ly,
search:{
},
goodsList:[],
totalPage: 0,
total: 0,
selectedSku:{
}
},
created(){
//获取请求参数
const search = ly.parse(location.search.substring(1));
this.search = search;
//发送后台
this.loadData();
},
methods:{
loadData(){
//发送到后台
ly.http.post("/search/page",this.search).then(resp =>{
//保存分页结果
this.total = resp.data.total;
this.totalPage = resp.data.totalPage;
//保存当前页商品
resp.data.items.forEach(goods =>{
//把json处理成JS对象
goods.skus = JSON.parse(goods.skus);
//初始化被选中的sku
goods.selectedSku = goods.skus[0];
})
this.goodsList = resp.data.items;
}).catch(error =>{
})
}
},
components:{
lyTop: () => import("./js/pages/top.js")
}
});
</script>
<!-- 底部栏位 -->
<!--页面底部,由js动态加载-->
<div class="clearfix footer"></div>
<script type="text/javascript">$(".footer").load("foot.html");</script>
<!--页面底部END-->
</body >
<!--购物车单元格 模板-->
<script type="text/template" id="tbar-cart-item-template">
<div class="tbar-cart-item">
<div class="jtc-item-promo">
<em class="promo-tag promo-mz">满赠<i class="arrow"></i></em>
<div class="promo-text">已购满600元,您可领赠品</div>
</div>
<div class="jtc-item-goods">
<span class="p-img"><a href="#" target="_blank"><img src="{2}" alt="{1}" height="50" width="50"/></a></span>
<div class="p-name">
<a href="#">{
1}</a>
</div>
<div class="p-price"><strong>¥{
3}</strong>×{
4}</div>
<a href="#none" class="p-del J-del">删除</a>
</div>
</div>
</script>
<!--侧栏面板结束-->
<script type="text/javascript" src="js/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript">
$(function () {
$("#service").hover(function () {
$(".service").show();
}, function () {
$(".service").hide();
});
$("#shopcar").hover(function () {
$("#shopcarlist").show();
}, function () {
$("#shopcarlist").hide();
});
})
</script>
<script type="text/javascript" src="js/model/cartModel.js"></script>
<script type="text/javascript" src="js/czFunction.js"></script>
<script type="text/javascript" src="js/plugins/jquery.easing/jquery.easing.min.js"></script>
<script type="text/javascript" src="js/plugins/sui/sui.min.js"></script>
<script type="text/javascript" src="js/widget/cartPanelView.js"></script>
</html>
结果:
至此,今天的学习结束。