本文要实现的一个功能,根据品牌、分类、规格、价格过滤查询商品的功能,并对查询结果的关键字
进行高亮显示。只做后端功能。
代码驱动
,如果看不太懂,可以先复制代码,再慢慢看,注释很详细。主要就是fastjson
和spring-boot-starter-data-elasticsearch
(SpringBoot项目),fastJson的作用是转换对象使用,当然也可以进行时间格式化(本文未作处理)。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.28version>
dependency>
①创建Goods类,测试类里注入相关对象
@Document(indexName = "goods_sku",type = "goods")
@Data
public class Goods {
@Id
@Field(type = FieldType.Long,store = true)
private Long id; // 主键Id
@Field(type = FieldType.Text,analyzer = "ik_smart",store = true)
private String name; // 商品名称
@Field(type = FieldType.Integer,store = true)
private Integer price; // 商品价格
@Field(type = FieldType.Text,store = true,index = false)
private String image; // 商品图片src
@Field(type = FieldType.Date,store = true,index = false)
private Date createTime; // 商品创建时间
@Field(type = FieldType.Long,store = true,index = false)
private Long spuId; // Spu的Id
@Field(type = FieldType.Keyword,store = true)
private String categoryName;// 分类名称
@Field(type = FieldType.Keyword,store = true)
private String brandName; // 品牌名称
@Field(type = FieldType.Object,store = true)
private Map spec; // 规格Map Map,如<"颜色","黑色">
@Field(type = FieldType.Integer,store = true,index = false)
private Integer saleNum; // 销量
public Goods(){
}
public Goods(Long id, String name, Integer price, String image, Date createTime, Long spuId, String categoryName, String brandName, Map spec, Integer saleNum) {
this.id = id;
this.name = name;
this.price = price;
this.image = image;
this.createTime = createTime;
this.spuId = spuId;
this.categoryName = categoryName;
this.brandName = brandName;
this.spec = spec;
this.saleNum = saleNum;
}
}
@Autowired
private ElasticsearchTemplate template;
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private EsResultMapper esResultMapper;
②数据准备 - 尽量多准备一些数据,方便测试查询
@Test
public void createIndex(){
template.createIndex(Goods.class);
}
@Test
public void createDoc(){
Map map1 = new HashMap();
map1.put("颜色","紫色");
map1.put("套餐","标准套餐");
Goods goods1 = new Goods(7L,"小米 Mini9秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
goodsRepository.save(goods1);
// 使用saveAll批量存储
}
该方法通过传过来的条件Map
,根据条件进行过滤查询,比如分类、品牌、规格、价格区间等(具体取决于需求)。
/**
* 构建基本查询 - 搜索关键字、分类、品牌、规格、价格
* @param searchMap
* @return
*/
private BoolQueryBuilder buildBasicQuery(Map searchMap) {
// 构建布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键字查询
boolQueryBuilder.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")));
// 分类、品牌、规格 都是需要精准查询的,无需分词
// 商品分类过滤
if (searchMap.get("category") != null){
boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("categoryName",searchMap.get("category")));
}
// 商品品牌过滤
if(searchMap.get("brand") != null){
boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("brandName",searchMap.get("brand")));
}
// 规格过滤
if(searchMap.get("spec") != null){
Map<String,String> map = (Map) searchMap.get("spec");
for(Map.Entry<String,String> entry : map.entrySet()){
// 规格查询[spec.xxx],因为规格是不确定的,所以需要精确查找,加上.keyword,如spec.颜色.keyword
boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("spec." + entry.getKey() + ".keyword",entry.getValue()));
}
}
// 价格过滤
if(searchMap.get("price") != null){
// 价格: 0-500 0-*
String[] prices = ((String)searchMap.get("price")).split("-");
if(!prices[0].equals("0")){ // 加两个0是,因为价格转换成分
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(prices[0] + "00"));
}
if(!prices[1].equals("*")){ // 价格有上限
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(prices[1] + "00"));
}
}
return boolQueryBuilder;
}
主要是根据搜索关键字
查询查询出来的结果,将其分类,然后把分类查询出来,显示到前端。
/**
* 查询分类列表
* @param searchMap
* @return
*/
private List<String> searchCategoryList(Map searchMap) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
// 构建查询
BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
// 分类聚合名
String groupName = "sku_category";
// 构建聚合查询
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field("categoryName");
nativeSearchQueryBuilder.addAggregation(termsAggregationBuilder);
// 获取聚合分页结果
AggregatedPage<Goods> goodsList = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build());
// 在查询结果中找到聚合 - 根据聚合名称
StringTerms stringTerms = (StringTerms) goodsList.getAggregation(groupName);
// 获取桶
List<StringTerms.Bucket> buckets = stringTerms.getBuckets();
// 使用流Stream 将分类名存入集合
List<String> categoryList = buckets.stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
// 打印分类名称
categoryList.forEach(System.out::println);
return categoryList;
}
既然有了分类,那么肯定还有对应的品牌、规格。其实品牌和规格与分类是有一个联系的。ElasticSearch
查询出分类,每个分类对应一个id
,也就是说所有分类和分类的id应该存到Redis
中去,这样前端就可以根据返回的分类集合去查询对应的品牌和规格,这里只是提供一个实现思路。
String categoryName = ""; // 分类名
if(searchMap.get("category") == null){ // 如果查询条件没有分类
// 默认取分类列表的第一个
if(categoryList.size() > 0){
categoryName = categoryList.get(0);
}
}else{ // 如果查询条件有分类
// 则取查询条件中的分类
categoryName = searchMap.get("category");
}
// 根据分类名查询品牌 - 实际应该从Redis中查询
if(searchMap.get("brand")==null) {
List<Map> brandList = brandDao.findListByCategoryName(categoryName);
resultMap.put("brandList", brandList);
}
// 根据分类查询规格 - 实际应该从Redis中查询
List<Map> specList = specDao.findListByCategoryName(categoryName);
for(Map spec:specList){
// 规格选项列表 - 选项与选项之间是以,(逗号)分隔的
String[] options = ((String) spec.get("options")).split(",");
// 讲过规格选项放入到规格对象中
spec.put("options",options);
}
// 将规格对象放入到结果集
resultMap.put("specList",specList);
SearchResultMapper
- 高亮前奏因为默认的SearchResultMapper
是没有高亮的,我们需要重新实现,重写AggregatedPage
方法。
@Component
public class EsResultMapper implements SearchResultMapper {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
// 记录总条数
long totalHits = response.getHits().getTotalHits();
// 记录列表(泛型) - 构建Aggregate使用
List<T> list = Lists.newArrayList();
// 获取搜索结果(真正的的记录)
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
if(hits.getHits().length <= 0){
return null;
}
// 将原本的JSON对象转换成Map对象
Map<String, Object> map = hit.getSourceAsMap();
// 获取高亮的字段Map
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
// 获取高亮的Key
String key = highlightField.getKey();
// 获取高亮的Value
HighlightField value = highlightField.getValue();
// 实际fragments[0]就是高亮的结果,无需遍历拼接
Text[] fragments = value.getFragments();
StringBuilder sb = new StringBuilder();
for (Text text : fragments) {
sb.append(text);
}
// 因为高亮的字段必然存在于Map中,就是key值
// 可能有一种情况,就是高亮的字段是嵌套Map,也就是说在Map里面还有Map的这种情况,这里没有考虑
map.put(key, sb.toString());
}
// 把Map转换成对象
T item = JSON.parseObject(JSONObject.toJSONString(map),aClass);
list.add(item);
}
// 返回的是带分页的结果
return new AggregatedPageImpl<>(list, pageable, totalHits);
}
}
/**
* 查询Sku集合 - 商品列表
* @param searchMap 查询条件
* @return
*/
private Map searchSkuList(Map searchMap) {
Map resultMap = new HashMap();
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
// 查询
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
// 排序
String sortField = (String)searchMap.get("sortField"); // 排序字段
String sortRule = (String)searchMap.get("sortRule"); // 排序规则 - 顺序(ASC)/倒序(DESC)
if(sortField!= null && !"".equals(sortField)){
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
}
// 构建分页
nativeSearchQueryBuilder.withPageable(PageRequest.of(0,15));
// 构建高亮查询
HighlightBuilder.Field field = new HighlightBuilder.Field("name").preTags("").postTags("");
nativeSearchQueryBuilder.withHighlightFields(field); // 名字高亮
NativeSearchQuery build = nativeSearchQueryBuilder.build();
// 获取查询结果
AggregatedPage<Goods> goodsPage = template.queryForPage(build, Goods.class, esResultMapper);
long total = goodsPage.getTotalElements(); // 总数据量
long totalPage = goodsPage.getTotalPages(); // 总页数
// ...你还要将是否有上页下页等内容传过去
List<Goods> goodsList = goodsPage.getContent();
goodsList.forEach(System.out::println);
resultMap.put("rows",goodsList);
resultMap.put("total",total);
resultMap.put("totalPage",totalPage);
return resultMap;
}
/**
* 搜索方法 - searchMap应该由前端传过来
* searchMap里封装了一些条件,根据条件进行过滤
*/
@Test
public void search(){
// 搜索条件Map
Map searchMap = new HashMap();
searchMap.put("keywords","小米");
// searchMap.put("category","手机");
// searchMap.put("brand","小米");
Map map = new HashMap();
map.put("颜色","紫色");
// map.put("",""); // 其他规格类型
searchMap.put("spec",map);
// searchMap.put("price","0-3000");
// 返回结果Map
Map resultMap = new HashMap();
// 查询商品列表
resultMap.putAll(searchSkuList(searchMap));
// 查询分类列表
List<String> categoryList = searchCategoryList(searchMap);
resultMap.put("categoryList",categoryList);
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsTest {
@Autowired
private ElasticsearchTemplate template;
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private EsResultMapper esResultMapper;
@Test
public void createIndex(){
template.createIndex(Goods.class);
}
@Test
public void createDoc(){
// Map map1 = new HashMap();
// map1.put("颜色","蓝色");
// map1.put("套餐","标准套餐");
// Goods goods1 = new Goods(2L,"Redmi Note7秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
//
// Map map2 = new HashMap();
// map2.put("颜色","蓝色");
// map2.put("套餐","标准套餐");
// Goods goods2 = new Goods(3L,"Redmi Note7秘境黑优惠套餐16G+64G",500,"xxxx",new Date(),3L,"手机","小米",map2,100);
//
// Map map3 = new HashMap();
// map3.put("颜色","黑色");
// map3.put("尺寸","64寸");
// Goods goods3 = new Goods(4L,"小米电视 黑色 64寸 优惠套餐",1000,"xxxx",new Date(),4L,"电视","小米",map3,100);
//
// Map map4 = new HashMap();
// map4.put("颜色","金色");
// map4.put("尺寸","46寸");
// Goods goods4 = new Goods(5L,"华为电视 金色 46寸 优惠套餐",1500,"xxxx",new Date(),5L,"电视","华为",map4,100);
//
// Map map5 = new HashMap();
// map5.put("颜色","白金色");
// map5.put("网络制式","全网通5G");
// Goods goods5 = new Goods(6L,"华为P30 金色 全网通5G 优惠套餐",2000,"xxxx",new Date(),6L,"手机","华为",map5,100);
// List list = new ArrayList<>();
// list.add(goods1);
// list.add(goods2);
// list.add(goods3);
// list.add(goods4);
// list.add(goods5);
// goodsRepository.saveAll(list);
Map map1 = new HashMap();
map1.put("颜色","紫色");
map1.put("套餐","标准套餐");
Goods goods1 = new Goods(7L,"小米 Mini9秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
goodsRepository.save(goods1);
// Map map1 = new HashMap();
// map1.put("颜色","蓝色");
// map1.put("套餐","标准套餐");
// Goods goods1 = new Goods(2L,"Redmi Note7秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
// goodsRepository.save(goods1);
}
/**
* 搜索方法 - searchMap应该由前端传过来
* searchMap里封装了一些条件,根据条件进行过滤
*/
@Test
public void search(){
// 搜索条件Map
Map searchMap = new HashMap();
searchMap.put("keywords","小米");
// searchMap.put("category","手机");
// searchMap.put("brand","小米");
Map map = new HashMap();
map.put("颜色","紫色");
// map.put("",""); // 其他规格类型
searchMap.put("spec",map);
// searchMap.put("price","0-3000");
// 返回结果Map
Map resultMap = new HashMap();
// 查询商品列表
resultMap.putAll(searchSkuList(searchMap));
// 查询分类列表
List<String> categoryList = searchCategoryList(searchMap);
resultMap.put("categoryList",categoryList);
}
/**
* 查询Sku集合 - 商品列表
* @param searchMap 查询条件
* @return
*/
private Map searchSkuList(Map searchMap) {
Map resultMap = new HashMap();
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
// 查询
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
// 排序
String sortField = (String)searchMap.get("sortField"); // 排序字段
String sortRule = (String)searchMap.get("sortRule"); // 排序规则 - 顺序(ASC)/倒序(DESC)
if(sortField!= null && !"".equals(sortField)){
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
}
// 构建分页
nativeSearchQueryBuilder.withPageable(PageRequest.of(0,15));
// 构建高亮查询
HighlightBuilder.Field field = new HighlightBuilder.Field("name").preTags("").postTags("");
nativeSearchQueryBuilder.withHighlightFields(field); // 名字高亮
NativeSearchQuery build = nativeSearchQueryBuilder.build();
// 获取查询结果
AggregatedPage<Goods> goodsPage = template.queryForPage(build, Goods.class, esResultMapper);
long total = goodsPage.getTotalElements(); // 总数据量
long totalPage = goodsPage.getTotalPages(); // 总页数
// ...你还要将是否有上页下页等内容传过去
List<Goods> goodsList = goodsPage.getContent();
goodsList.forEach(System.out::println);
resultMap.put("rows",goodsList);
resultMap.put("total",total);
resultMap.put("totalPage",totalPage);
return resultMap;
}
/**
* 查询分类列表
* @param searchMap
* @return
*/
private List<String> searchCategoryList(Map searchMap) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
// 构建查询
BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
// 分类聚合名
String groupName = "sku_category";
// 构建聚合查询
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field("categoryName");
nativeSearchQueryBuilder.addAggregation(termsAggregationBuilder);
// 获取聚合分页结果
AggregatedPage<Goods> goodsList = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build());
// 在查询结果中找到聚合 - 根据聚合名称
StringTerms stringTerms = (StringTerms) goodsList.getAggregation(groupName);
// 获取桶
List<StringTerms.Bucket> buckets = stringTerms.getBuckets();
// 使用流Stream 将分类名存入集合
List<String> categoryList = buckets.stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
// 打印分类名称
categoryList.forEach(System.out::println);
return categoryList;
}
/**
* 构建基本查询 - 搜索关键字、分类、品牌、规格、价格
* @param searchMap
* @return
*/
private BoolQueryBuilder buildBasicQuery(Map searchMap) {
// 构建布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键字查询
boolQueryBuilder.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")));
// 分类、品牌、规格 都是需要精准查询的,无需分词
// 商品分类过滤
if (searchMap.get("category") != null){
boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("categoryName",searchMap.get("category")));
}
// 商品品牌过滤
if(searchMap.get("brand") != null){
boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("brandName",searchMap.get("brand")));
}
// 规格过滤
if(searchMap.get("spec") != null){
Map<String,String> map = (Map) searchMap.get("spec");
for(Map.Entry<String,String> entry : map.entrySet()){
// 规格查询[spec.xxx],因为规格是不确定的,所以需要精确查找,加上.keyword,如spec.颜色.keyword
boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("spec." + entry.getKey() + ".keyword",entry.getValue()));
}
}
// 价格过滤
if(searchMap.get("price") != null){
// 价格: 0-500 0-*
String[] prices = ((String)searchMap.get("price")).split("-");
if(!prices[0].equals("0")){ // 加两个0是,因为价格转换成分
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(prices[0] + "00"));
}
if(!prices[1].equals("*")){ // 价格有上限
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(prices[1] + "00"));
}
}
return boolQueryBuilder;
}
}