发布时间:2023-03-28 11:30
pom.xml----->elasticsearch依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
pom.xml----->该搜索微服务全部依赖
<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>leyouartifactId>
<groupId>com.leyou.parentgroupId>
<version>1.0.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.leyou.searchgroupId>
<artifactId>ly-searchartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>com.leyou.upload.servicegroupId>
<artifactId>ly-item-interfaceartifactId>
<version>1.0.0-SNAPSHOTversion>
dependency>
dependencies>
project>
application.yml----->配置elasticsearch
data:
elasticsearch:
cluster-name: elasticsearch # 集群名称
cluster-nodes: 192.168.79.128:9300 # 集群地址
application.yml----->该搜索微服务全部配置
server:
port: 8084
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch # 集群名称
cluster-nodes: 192.168.79.128:9300 # 集群地址
Jackson:
default-property-inclusion: non_null # 返回的结果是null的就排除
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5 # 每5秒拉一次注册信息
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
LySearchApplication
package com.leyou.search;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-11 14:39
**/
@SpringBootApplication
@EnableDiscoveryClient // eureka
@EnableFeignClients // feign
public class LySearchApplication {
public static void main(String[] args) {
SpringApplication.run(LySearchApplication.class);
}
}
Goods
package com.leyou.search.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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.List;
import java.util.Map;
import java.util.Set;
@NoArgsConstructor
@AllArgsConstructor
@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;// 卖点
// 过滤字段 不加注解, spring会自动映射进去
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是参数名,值是参数值
}
SearchRequest
package com.leyou.search.pojo;
import lombok.Data;
import org.bouncycastle.jcajce.provider.symmetric.IDEA;
import java.util.Map;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-12 17:49
**/
public class SearchRequest {
// 当前页码
private Integer page;
// 搜索字段
private String key;
// 因为是给用户看的页面, 所以每页大小必须定死, 不可修改, 要是给用户输入的话如果是1千万那不搜索服务器直接挂了
private static final int DEFAULT_PAGE = 1;
private static final int DEFAULT_SIZE = 20;
// 过滤字段
private Map<String, String> filter;
public Integer getPage() {
if (page == null) {
return DEFAULT_PAGE;
}
// 比较两个数的大小, page 大于 DEFAULT_PAGE用page, 小于DEFAULT_PAGE用DEFAULT_PAGE
return Math.max(DEFAULT_PAGE, page);
}
public void setPage(Integer page) {
this.page = page;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public int getSize() {
return DEFAULT_SIZE;
}
public Map<String, String> getFilter() {
return filter;
}
public void setFilter(Map<String, String> filter) {
this.filter = filter;
}
}
SearchResult
package com.leyou.search.pojo;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class SearchResult extends PageResult<Goods> {
private List<Category> categories; // 分类待选项
private List<Brand> brands; // 品牌待选项
private List<Map<String, Object>> specs; // 规格参数 key及待选项
public SearchResult() {
}
public SearchResult(Long total, Long totalPage, List<Goods> items, List<Category> categories, List<Brand> brands, List<Map<String, Object>> specs) {
super(total, totalPage, items);
this.categories = categories;
this.brands = brands;
this.specs = specs;
}
}
BrandClient
package com.leyou.search.client;
import com.leyou.item.api.BrandApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @program: leyou
* @description: 告诉feign 请求服务 请求方式 请求路径 请求参数 返回结果
* @author: Mr.Xiao
* @create: 2020-06-11 17:00
**/
@FeignClient(\"item-service\") // 参数是服务名称
public interface BrandClient extends BrandApi {
}
CategoryClient
package com.leyou.search.client;
import com.leyou.item.api.CategoryApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-11 17:00
**/
@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;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-11 16:59
**/
@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;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-11 17:01
**/
@FeignClient(\"item-service\")
public interface SpecificationClient extends SpecificationApi {
}
GoodsRepository
package com.leyou.search.repository;
import com.leyou.search.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* @program: leyou
* @description: ElasticsearchRepository 第一个参数实体类类型, 第二个参数id类型
* @author: Mr.Xiao
* @create: 2020-06-12 12:13
**/
// ElasticsearchRepository 跟 通用mapper一样, 里面包含了各种增删改查
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
SearchController
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.pojo.SearchResult;
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;
/**
* @program: leyou
* @description: 搜索服务 表现层
* @author: Mr.Xiao
* @create: 2020-06-12 17:57
**/
@RestController
public class SearchController {
@Autowired
private SearchService searchService;
@PostMapping(\"/page\")
public ResponseEntity<SearchResult> search(@RequestBody SearchRequest request) {
return ResponseEntity.ok(searchService.search(request));
}
}
SearchService接口
package com.leyou.search.service;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Spu;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-12 09:32
**/
public interface SearchService {
/**
* 封装goods对象
* @param spu
* @return
*/
Goods buildGoods(Spu spu);
/**
* 获取搜索结果
* @param request
* @return
*/
SearchResult search(SearchRequest request);
}
SearchServiceImpl实体类
package com.leyou.search.service.impl;
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.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import com.leyou.search.service.SearchService;
import javafx.beans.binding.ObjectExpression;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.esf.SPUserNotice;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
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.aggregation.AggregatedPage;
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 javax.swing.plaf.ScrollPaneUI;
import java.util.*;
import java.util.stream.Collectors;
/**
* @program: leyou
* @description:
* @author: Mr.Xiao
* @create: 2020-06-12 09:32
**/
@Slf4j
@Service(\"searchService\")
public class SearchServiceImpl implements SearchService {
@Autowired
private BrandClient brandClient;
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
/**
* 封装goods对象
* @param spu
* @return
*/
@Override
public Goods buildGoods(Spu spu) {
// 获取spu id
Long spuId = spu.getId();
// 获取all 所有需要被搜索的信息,包含标题,分类,甚至品牌
String all = \"\";
// 获取分类信息
List<Category> categoryList = categoryClient.queryCategoryListByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
if (CollectionUtils.isEmpty(categoryList)) {
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
}
// 获取分类名称 以 空格 隔开
List<String> cnames = categoryList.stream().map(Category::getName).collect(Collectors.toList());
// 获取品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
if (brand == null) {
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
all = spu.getTitle() + StringUtils.join(cnames, \" \") + brand.getName();
// 获取sku商品信息
List<Sku> skuList = goodsClient.querySkuListBySpuId(spuId);
if (CollectionUtils.isEmpty(skuList)) {
throw new LyException(ExceptionEnum.GOODS_NOT_FOUND);
}
// sku 商品集合
ArrayList<Map<String, Object>> skus = new ArrayList<>();
// sku 价格集合
HashSet<Long> prices = new HashSet<>();
// 优于展示字段只是sku中的几个字段, 所以我们这里不需要全部字段, 需要做抽离
for (Sku sku : skuList) {
HashMap<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(), \",\"));
// 添加sku商品
skus.add(map);
// 添加sku价格
prices.add(sku.getPrice());
}
// 规格参数
Map<String, Object> specs = new HashMap<>();
// 获取存储规格参数键对象
// 可搜索字段, 分类id是3级分类
List<SpecParam> specParams = specificationClient.queryParamByList(null, spu.getCid3(), true);
if (CollectionUtils.isEmpty(specParams)) {
throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
}
// 获取spu详细信息
SpuDetail spuDetail = goodsClient.querySpuDetailBySpuId(spuId);
if (spuDetail == null) {
throw new LyException(ExceptionEnum.SPU_NOT_FOUND);
}
// 获取通用规格参数
Map<Long, Object> genericSpec = JsonUtils.parseMap(spuDetail.getGenericSpec(), Long.class, Object.class);
// 获取特有规格参数
Map<Long, List<Object>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(),
new TypeReference<Map<Long, List<Object>>>() {});
// 遍历处理specs
for (SpecParam specParam : specParams) {
String key = specParam.getName();
Object value = \"\";
// 是通用规格参数
if (specParam.getGeneric()) {
value = genericSpec.get(specParam.getId());
if (specParam.getNumeric()) {
// 是数值类型
if (StringUtils.isNotBlank(specParam.getSegments())) {
// 处理成段
value = chooseSegment(value.toString(), specParam);
}
}
} else {
value = specialSpec.get(specParam.getId());
}
specs.put(key, value);
}
// 封装对象
Goods goods = new Goods();
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setBrandId(spu.getBrandId());
goods.setCreateTime(spu.getCreateTime());
goods.setId(spuId);
goods.setSubTitle(spu.getSubTitle());
goods.setAll(all); // 所有需要被搜索的信息,包含标题,分类,甚至品牌
goods.setSkus(JsonUtils.serialize(skus)); // sku商品
goods.setPrice(prices); // sku 价格集合
goods.setSpecs(specs); // 可搜索的规格参数,key是参数名,值是参数值
return goods;
}
/**
* 获取搜索结果
* @param request
* @return
*/
@Override
public SearchResult search(SearchRequest request) {
// 获取参数
int page = request.getPage() - 1; // 注意: elasticsearch分页是以0开始的
int size = request.getSize();
// 1. 创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 2. 结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{\"id\", \"subTitle\", \"skus\"}, null));
// 3. 分页
queryBuilder.withPageable(PageRequest.of(page, size));
// 4. 创建查询条件
QueryBuilder basicQuery = buildConditions(request);
queryBuilder.withQuery(basicQuery);
// 5. 聚合
// 5.1品牌聚合
String brandAggName = \"brand_agg\";
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field(\"brandId\"));
// 5.2分类集合
String categoryAggName = \"category_agg\";
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field(\"cid3\"));
// 6. 执行这里用了聚合, 所以只能用template
AggregatedPage<Goods> goodsList = template.queryForPage(queryBuilder.build(), Goods.class);
// 7. 结果解析
long total = goodsList.getTotalElements();
int totalPages = goodsList.getTotalPages();
List<Goods> content = goodsList.getContent();
// 7. 获取品牌List
List<Brand> brandList = parseBrand((LongTerms) goodsList.getAggregation(brandAggName));
// 8. 获取分类List
List<Category> categoryList = parseCategory((LongTerms) goodsList.getAggregation(categoryAggName));
// 9. 判断分类不问空, 并且为1才聚合规格参数
List<Map<String, Object>> specs = null;
if (categoryList != null && categoryList.size() == 1) {
// 构建规格参数
specs = buildSpecificationAgg(categoryList.get(0).getId(), basicQuery);
}
return new SearchResult(total, Long.valueOf(totalPages), content, categoryList, brandList, specs);
}
/**
* 构建查询过滤条件
* @param request
* @return
*/
private QueryBuilder buildConditions(SearchRequest request) {
// 1. 构建布尔条件
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 2. 构建查询条件
queryBuilder.must(QueryBuilders.matchQuery(\"all\", request.getKey()));
// 3. 构建过滤条件
Map<String, String> filter = request.getFilter();
if (filter != null) {
for (Map.Entry<String, String> map : filter.entrySet()) {
// 获取, 过滤条件的键
String key = map.getKey();
// 如果不是分类或者品牌id
if (!\"cid3\".equals(key) && !\"brandId\".equals(key)) {
key = \"specs.\" + key + \".keyword\";
}
queryBuilder.filter(QueryBuilders.termQuery(key, map.getValue()));
}
}
return queryBuilder;
}
/**
* 构建规格参数集合
* @param cid
* @param basicQuery
* @return
*/
private List<Map<String, Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) {
// 1. 创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 2. 把搜索条件添加进去
queryBuilder.withQuery(basicQuery);
// 3. 获取规格参数
List<SpecParam> specParams = specificationClient.queryParamByList(null, cid, true);
// 4. 判断是否为空
if (CollectionUtils.isEmpty(specParams)) {
throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
}
// 5. 创建对象
List<Map<String, Object>> specs = new ArrayList<>();
// 6. 遍历规格参数, 聚合每一个规格参数
for (SpecParam specParam : specParams) {
String name = specParam.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(name).field(\"specs.\" + name + \".keyword\"));
}
// 在原有的搜索条件上加上现在聚合的规格参数条件进行查询
AggregatedPage<Goods> goods = template.queryForPage(queryBuilder.build(), Goods.class);
// 7. 解析结果集
// 7.1 遍历规格参数构造, 规格参数待选项
for (SpecParam specParam : specParams) {
String name = specParam.getName();
StringTerms terms = (StringTerms) goods.getAggregation(name);
List<StringTerms.Bucket> buckets = terms.getBuckets();
// 构建map对象
Map<String, Object> map = new HashMap<>();
map.put(\"k\", name);
map.put(\"options\", buckets.stream().map(b -> b.getKeyAsString()).collect(Collectors.toList()));
// 添加进规格参数集合中
specs.add(map);
}
return specs;
}
/**
* 获取聚合后所有的 品牌
* @param terms
* @return
*/
private List<Brand> parseBrand(LongTerms terms) {
try{
List<Long> ids = terms.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());
List<Brand> brands = brandClient.queryBrandByIds(ids);
return brands;
} catch (Exception e) {
log.error(\"[搜索服务]查询品牌异常: \", e);
return null;
}
}
/**
* 获取聚合后所有的分类
* @param terms
* @return
*/
private List<Category> parseCategory(LongTerms terms) {
try{
List<Long> ids = terms.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());
List<Category> categories = categoryClient.queryCategoryListByIds(ids);
return categories;
} catch (Exception e) {
log.error(\"[搜索服务]查询品牌分类异常: \", e);
return null;
}
}
/**
* 拼接规格参数
* @param value
* @param p
* @return
*/
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;
}
}