SpringDataElasticSearch - NativeSearchQueryBuilder過濾聚合高亮查詢

本文要實現的一個功能,根據品牌、分類、規格、價格過濾查詢商品的功能,並對查詢結果的關鍵字進行高亮顯示。只做後端功能。
高亮顯示實現功能

本文是以代碼驅動,如果看不太懂,可以先複製代碼,再慢慢看,註釋很詳細。
1、引入相關依賴

主要就是fastjsonspring-boot-starter-data-elasticsearch(SpringBoot項目),fastJson的作用是轉換對象使用,當然也可以進行時間格式化(本文未作處理)。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.28</version>
</dependency>
2、ElasticSearch數據測試準備

①創建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<String,String>,如<"顏色","黑色">
    @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批量存儲
}

ES數據

3、構建基本查詢方法

該方法通過傳過來的條件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;
}
4、查詢分類列表

主要是根據搜索關鍵字查詢查詢出來的結果,將其分類,然後把分類查詢出來,顯示到前端。

/**
 * 查詢分類列表
 * @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);
5、重新實現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);
    }

}
6、查詢商品(sku)列表
/**
 * 查詢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("<font style='color:red'>").postTags("</font>");
	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;
}
7、查詢
/**
 * 搜索方法 - 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<Goods> 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("<font style='color:red'>").postTags("</font>");
        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;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章