ES服務的搭建(八)
看下圖的淘寶頁面,可以看到搜索有多個條件及搜索產品,並且支援多種排序方式,例如按價格;其實這塊有個特點,就是不管你搜索哪個商品他都是有分類的,以及他對應的品牌,這兩個是固定的,但其它參數不一定所有商品都具有;這一塊設計就涉及到動態變化數據的載入,設計是比較複雜的,這個可以在後面慢慢說,其實這次想分析的主要是es的搜索服務使用
一、es的搜索服務使用
- 完成關鍵字的搜索功能
- 完成商品分類過濾功能
- 完成品牌、規格過濾功能
- 完成價格區間過濾功能
二、ES服務的搭建
在搭建服務前先理下流程,其實流程也很簡單,前台服務對資料庫進行了操作後,canal會同步變化的數據,將數據發到ES搜索引擎上去,用戶就可以在前台使用不同條件進行搜索,關鍵詞、分類、價格區間、動態屬性;因為搜索功能在很多模組會被調用,所以先在api模組下建一個子服務spring-cloud-search-api,然後導入包
<dependencies> <!--ElasticSearch--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> </dependencies>
在接下來寫前看下上圖片,現在需要將資料庫數據查詢出來,再存入ES中,但中間需要有一個和ES索引庫對應的JavaBean,為了不影響原來程式對象,所以會創建一個新的 JavaBean 對象
/** * indexName是索引庫中對應的索引名稱 * type是當前實體類中對應的一個類型,可以它理解一個表的名字 */ @Data @Document(indexName = "shopsearch",type = "skues") public class SkuEs { @Id private String id; //這裡是因為要對商品進行模糊查詢,要對它進行分詞查找,所以要選擇分詞器,這裡選擇的是IK分詞器 @Field(type = FieldType.Text,analyzer = "ik_smart",searchAnalyzer = "ik_smart") private String name; private Integer price; private Integer num; private String image; private String images; private Date createTime; private Date updateTime; private String spuId; private Integer categoryId; //Keyword:不分詞,這是里分類名稱什麼的是不用分詞拆分的所以選擇不分詞 @Field(type= FieldType.Keyword) private String categoryName; private Integer brandId; @Field(type=FieldType.Keyword) private String brandName; @Field(type=FieldType.Keyword) private String skuAttribute; private Integer status; //屬性映射(動態創建域資訊) private Map<String,String> attrMap; }
這一步搞定後就是要搭建搜索工程了,接下來在spring-cloud-service下面搭建子服務spring-cloud-search-service
<dependency>
<groupId>com.ghy</groupId>
<artifactId>spring-cloud-search-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
server: port: 8084 spring: application: name: spring-cloud-search-service cloud: nacos: config: file-extension: yaml server-addr: 192.168.32.135:8848 discovery: #Nacos的註冊地址 server-addr: 192.168.32.135:8848 #Elasticsearch服務配置 6.8.12 elasticsearch: rest: uris: http://192.168.32.135:9200 #日誌配置 logging: pattern: console: "%msg%n"
上面配置工作做完後下面要的事就是寫業務程式碼了,在業務場景中當資料庫sku數據變更的時候,需要做的操作就是通過Canal微服務調用當前搜索微服務實現數據實時更新,原因在上面也畫圖說明了。接下來先先Mapper程式碼
public interface SkuSearchMapper extends ElasticsearchRepository<SkuEs,String> { }
public interface SkuSearchService { //增加索引 void add(SkuEs skuEs); //刪除索引 void del(String id); }
@Service public class SkuSearchServiceImpl implements SkuSearchService { @Autowired private SkuSearchMapper skuSearchMapper; /*** * 增加索引 * @param skuEs */ @Override public void add(SkuEs skuEs) { //獲取屬性 String attrMap = skuEs.getSkuAttribute(); if(!StringUtils.isEmpty(attrMap)){ //將屬性添加到attrMap中 skuEs.setAttrMap(JSON.parseObject(attrMap, Map.class)); } skuSearchMapper.save(skuEs); } /*** * 根據主鍵刪除索引 * @param id */ @Override public void del(String id) { skuSearchMapper.deleteById(id); } }
@RestController @RequestMapping(value = "/search") public class SkuSearchController { @Autowired private SkuSearchService skuSearchService; /***** * 增加索引 */ @PostMapping(value = "/add") public RespResult add(@RequestBody SkuEs skuEs){ skuSearchService.add(skuEs); return RespResult.ok(); } /*** * 刪除索引 */ @DeleteMapping(value = "/del/{id}") public RespResult del(@PathVariable(value = "id")String id){ skuSearchService.del(id); return RespResult.ok(); } }
和上一篇一樣,這個搜索功能在很多模組會被調用,所以要在對應的API中寫上feign介面
@FeignClient(value = "spring-cloud-search-service") public interface SkuSearchFeign { /***** * 增加索引 */ @PostMapping(value = "/search/add") RespResult add(@RequestBody SkuEs skuEs); /*** * 刪除索引 */ @DeleteMapping(value = "/search/del/{id}") RespResult del(@PathVariable(value = "id")String id); }
索引服務的刪除和添加功能做好了,但是這樣還沒完,前面說過ES的更新是由Canal推過來的,所以需要在Canal服務調用剛剛上面寫的兩個介面,在spring-cloud-canal-service引入search的api
<dependency> <groupId>com.ghy</groupId> <artifactId>spring-cloud-search-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
然後和上一篇一樣在canal服務中寫一個監聽事件
@CanalTable(value = "sku") @Component public class Search implements EntryHandler<Sku> { @Resource private SkuSearchFeign skuSearchFeign; /*** * 增加數據監聽 * @param sku */ @Override public void insert(Sku sku) { if(sku.getStatus().intValue()==1){ //將Sku轉成JSON,再將JSON轉成SkuEs skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(sku), SkuEs.class)); } } /**** * 修改數據監聽 * @param before * @param after */ @Override public void update(Sku before, Sku after) { if(after.getStatus().intValue()==2){ //刪除索引 skuSearchFeign.del(after.getId()); }else{ //更新 skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(after), SkuEs.class)); } } /*** * 刪除數據監聽 * @param sku */ @Override public void delete(Sku sku) { skuSearchFeign.del(sku.getId()); } }
現在看似功能做好了,數據也能監聽推送到es了,但是還有一個問題,啟動程式測試一下就可以發現,由於實體類與資料庫映射關係問題導致,所以需要在api中導入以下包
<!--JPA-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
然後在對應的實體類上加上@Column註解就解決了
然後打開es控制面板,在資料庫隨便操作一條數據會發現控制面板有更新,做到這一步就說明實時更新已經完成
添加和刪除搞定後,接下來就來搞下查詢功能,也就是關鍵詞搜索功能,實現也很簡單,就是用戶輸入關鍵詞後,將關鍵詞一起傳入後台,需要根據商品名字進行搜索。以後也有可能根據別的條件查詢,所以傳入後台的數據可以用Map接收,響應頁面的數據包含列表、分頁等資訊,可以用Map封裝。
public interface SkuSearchService { /**** * 搜索數據 */ Map<String,Object> search(Map<String,Object> searchMap); //增加索引 void add(SkuEs skuEs); //刪除索引 void del(String id); }
/**** * 關鍵詞搜索 * @param searchMap * 關鍵詞:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->構建搜索條件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //skuSearchMapper進行搜索 Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); //獲取結果集:集合列表、總記錄數 Map<String,Object> resultMap = new HashMap<String,Object>(); List<SkuEs> list = page.getContent(); resultMap.put("list",list); resultMap.put("totalElements",page.getTotalElements()); return resultMap; } /**** * 搜索條件構建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判斷關鍵詞是否為空,不為空,則設置條件 if(searchMap!=null && searchMap.size()>0){ //關鍵詞條件,關鍵詞前後台要統一 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); } return builder; }
@RestController @RequestMapping(value = "/search") public class SkuSearchController { @Autowired private SkuSearchService skuSearchService; /*** * 商品搜索 */ @GetMapping public RespResult<Map<String,Object>> search(@RequestParam(required = false)Map<String,Object> searchMap){ Map<String, Object> resultMap = skuSearchService.search(searchMap); return RespResult.ok(resultMap); } /***** * 增加索引 */ @PostMapping(value = "/add") public RespResult add(@RequestBody SkuEs skuEs){ skuSearchService.add(skuEs); return RespResult.ok(); } /*** * 刪除索引 */ @DeleteMapping(value = "/del/{id}") public RespResult del(@PathVariable(value = "id")String id){ skuSearchService.del(id); return RespResult.ok(); } }
條件回顯問題:

看上圖可知,當每次執行搜索的時候,頁面會顯示不同搜索條件,例如:品牌,這些搜索條件都不是固定的,其實他們是沒執行搜索的時候,符合搜索條件的商品所有品牌和所有分類,以及所有屬性,把他們查詢出來,然後頁面顯示。但是這些條件都沒有重複的,也就是說要去重,去重一般採用分組查詢即可,所以我們要想動態獲取這樣的搜索條件,需要在後台進行分組查詢。 這個也很簡單,只用修改上面寫的search方法的業務層程式碼就好。
/**** * 關鍵詞搜索 * @param searchMap * 關鍵詞:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->構建搜索條件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分組搜索調用 group(queryBuilder,searchMap); //skuSearchMapper進行搜索 //Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); //獲取結果集:集合列表、總記錄數 Map<String,Object> resultMap = new HashMap<String,Object>(); //分組數據解析 parseGroup(page.getAggregations(),resultMap); List<SkuEs> list = page.getContent(); resultMap.put("list",list); resultMap.put("totalElements",page.getTotalElements()); return resultMap; } /*** * 分組結果解析 */ public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){ if(aggregations!=null){ for (Aggregation aggregation : aggregations) { //強轉ParsedStringTerms ParsedStringTerms terms = (ParsedStringTerms) aggregation; //循環結果集對象 List<String> values = new ArrayList<String>(); for (Terms.Bucket bucket : terms.getBuckets()) { values.add(bucket.getKeyAsString()); } //名字 String key = aggregation.getName(); resultMap.put(key,values); } } } /*** * 分組查詢 */ public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){ //用戶如果沒有輸入分類條件,則需要將分類搜索出來,作為條件提供給用戶 if(StringUtils.isEmpty(searchMap.get("category"))){ queryBuilder.addAggregation( AggregationBuilders .terms("categoryList")//別名,類似Map的key .field("categoryName")//根據categoryName域進行分組 .size(100) //分組結果顯示100個 ); } //用戶如果沒有輸入品牌條件,則需要將品牌搜索出來,作為條件提供給用戶 if(StringUtils.isEmpty(searchMap.get("brand"))){ queryBuilder.addAggregation( AggregationBuilders .terms("brandList")//別名,類似Map的key .field("brandName")//根據brandName域進行分組 .size(100) //分組結果顯示100個 ); } //屬性分組查詢 queryBuilder.addAggregation( AggregationBuilders .terms("attrmaps")//別名,類似Map的key .field("skuAttribute")//根據skuAttribute域進行分組 .size(100000) //分組結果顯示100000個 ); } /**** * 搜索條件構建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判斷關鍵詞是否為空,不為空,則設置條件 if(searchMap!=null && searchMap.size()>0){ //關鍵詞條件,關鍵詞前後台要統一 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); } return builder; }
經過上面的步驟就完成了搜索功能中的分類和品牌的操作,這兩塊相對來說還是比較簡單的,因為他們是固定的,但接下來的什麼價格呀、款式呀什麼的不是固定的,是動態的。下面就說下這塊屬性回顯的做法;屬性條件其實就是當前搜索的所有商品屬性資訊,所以我們可以把所有屬性資訊全部查詢出來,然後把屬性名作為key,屬性值用集合存起來,就是我們頁面要的屬性條件了。
/**** * 關鍵詞搜索 * @param searchMap * 關鍵詞:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->構建搜索條件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分組搜索調用 group(queryBuilder,searchMap); //skuSearchMapper進行搜索 //Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); //獲取結果集:集合列表、總記錄數 Map<String,Object> resultMap = new HashMap<String,Object>(); //分組數據解析 parseGroup(page.getAggregations(),resultMap); //動態屬性解析 attrParse(resultMap); List<SkuEs> list = page.getContent(); resultMap.put("list",list); resultMap.put("totalElements",page.getTotalElements()); return resultMap; } /**** * 將屬性資訊合併成Map對象 */ public void attrParse(Map<String,Object> searchMap){ //先獲取attrmaps Object attrmaps = searchMap.get("attrmaps"); if(attrmaps!=null){ //集合數據 List<String> groupList= (List<String>) attrmaps; //定義一個集合Map<String,Set<String>>,存儲所有匯總數據 Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>(); //循環集合 for (String attr : groupList) { Map<String,String> attrMap = JSON.parseObject(attr,Map.class); for (Map.Entry<String, String> entry : attrMap.entrySet()) { //獲取每條記錄,將記錄轉成Map 就業薪資 學習費用 String key = entry.getKey(); Set<String> values = allMaps.get(key); //空表示沒有這個對象 if(values==null){ values = new HashSet<String>(); } values.add(entry.getValue()); //覆蓋之前的數據 allMaps.put(key,values); } } //覆蓋之前的attrmaps searchMap.put("attrmaps",allMaps); } } /*** * 分組結果解析 */ public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){ if(aggregations!=null){ for (Aggregation aggregation : aggregations) { //強轉ParsedStringTerms ParsedStringTerms terms = (ParsedStringTerms) aggregation; //循環結果集對象 List<String> values = new ArrayList<String>(); for (Terms.Bucket bucket : terms.getBuckets()) { values.add(bucket.getKeyAsString()); } //名字 String key = aggregation.getName(); resultMap.put(key,values); } } } /*** * 分組查詢 */ public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){ //用戶如果沒有輸入分類條件,則需要將分類搜索出來,作為條件提供給用戶 if(StringUtils.isEmpty(searchMap.get("category"))){ queryBuilder.addAggregation( AggregationBuilders .terms("categoryList")//別名,類似Map的key .field("categoryName")//根據categoryName域進行分組 .size(100) //分組結果顯示100個 ); } //用戶如果沒有輸入品牌條件,則需要將品牌搜索出來,作為條件提供給用戶 if(StringUtils.isEmpty(searchMap.get("brand"))){ queryBuilder.addAggregation( AggregationBuilders .terms("brandList")//別名,類似Map的key .field("brandName")//根據brandName域進行分組 .size(100) //分組結果顯示100個 ); } //屬性分組查詢 queryBuilder.addAggregation( AggregationBuilders .terms("attrmaps")//別名,類似Map的key .field("skuAttribute")//根據skuAttribute域進行分組 .size(100000) //分組結果顯示100000個 ); } /**** * 搜索條件構建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判斷關鍵詞是否為空,不為空,則設置條件 if(searchMap!=null && searchMap.size()>0){ //關鍵詞條件,關鍵詞前後台要統一 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); } return builder; }
前面的做法還停留在單條件,但用戶在前端執行條件搜索的時候,有可能會選擇分類、品牌、價格、屬性,每次選擇條件傳入後台,後台按照指定參數進行條件查詢,這裡制定一個傳參數的規則:
1、分類參數:category 2、品牌參數:brand 3、價格參數:price 4、屬性參數:attr_屬性名:屬性值 5、分頁參數:page
現在來做的是獲取category,brand,price的值,並根據這三個只分別實現分類過濾、品牌過濾、價格過濾,其中價格過濾傳入的數據以-分割,修改的實現程式碼如下:
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //組合查詢對象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判斷關鍵詞是否為空,不為空,則設置條件 if(searchMap!=null && searchMap.size()>0){ //關鍵詞條件 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ //builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString())); } //分類查詢 Object category = searchMap.get("category"); if(!StringUtils.isEmpty(category)){ boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString())); } //品牌查詢 Object brand = searchMap.get("brand"); if(!StringUtils.isEmpty(brand)){ boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString())); } //價格區間查詢 price=0-500元 500-1000元 1000元以上 Object price = searchMap.get("price"); if(!StringUtils.isEmpty(price)){ //價格區間 String[] prices = price.toString().replace("元","").replace("以上","").split("-"); //price>x boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0]))); //price<=y if(prices.length==2){ boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1]))); } } //動態屬性查詢 for (Map.Entry<String, Object> entry : searchMap.entrySet()) { //以attr_開始,動態屬性 attr_網路:移動5G if(entry.getKey().startsWith("attr_")){ String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword"; boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString())); } } } return builder; }
上面查詢搞完了準備收尾工作了,加上前面說的排序問題和分頁程式碼
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //組合查詢對象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判斷關鍵詞是否為空,不為空,則設置條件 if(searchMap!=null && searchMap.size()>0){ //關鍵詞條件 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ //builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString())); } //分類查詢 Object category = searchMap.get("category"); if(!StringUtils.isEmpty(category)){ boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString())); } //品牌查詢 Object brand = searchMap.get("brand"); if(!StringUtils.isEmpty(brand)){ boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString())); } //價格區間查詢 price=0-500元 500-1000元 1000元以上 Object price = searchMap.get("price"); if(!StringUtils.isEmpty(price)){ //價格區間 String[] prices = price.toString().replace("元","").replace("以上","").split("-"); //price>x boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0]))); //price<=y if(prices.length==2){ boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1]))); } } //動態屬性查詢 for (Map.Entry<String, Object> entry : searchMap.entrySet()) { //以attr_開始,動態屬性 attr_網路:移動5G if(entry.getKey().startsWith("attr_")){ String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword"; boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString())); } } //排序 Object sfield = searchMap.get("sfield"); Object sm = searchMap.get("sm"); if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){ builder.withSort( SortBuilders.fieldSort(sfield.toString()) //指定排序域 .order(SortOrder.valueOf(sm.toString())) //排序方式 ); } } //分頁查詢 builder.withPageable(PageRequest.of(currentPage(searchMap),5)); return builder.withQuery(boolQueryBuilder); }
- 配置高亮域以及對應的樣式
- 從結果集中取出高亮數據,並將非高亮數據換成高亮數據
接下來按這個思路來玩下,在search方法中加入下面一段程式碼就好了
//1.設置高亮資訊 關鍵詞前(後)面的標籤、設置高亮域 HighlightBuilder.Field field = new HighlightBuilder .Field("name") //根據指定的域進行高亮查詢 .preTags("<span style=\"color:red;\">") //關鍵詞高亮前綴 .postTags("</span>") //高亮關鍵詞後綴 .fragmentSize(100); //碎片長度 queryBuilder.withHighlightFields(field);
public class HighlightResultMapper extends DefaultResultMapper { /*** * 映射轉換,將非高亮數據替換成高亮數據 * @param response * @param clazz * @param pageable * @param <T> * @return */ @Override public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) { //1、獲取所有非高亮數據 SearchHits hits = response.getHits(); //2、循環非高亮數據集合 for (SearchHit hit : hits) { //非高亮數據 Map<String, Object> sourceAsMap = hit.getSourceAsMap(); //3、獲取高亮數據 for (Map.Entry<String, HighlightField> entry : hit.getHighlightFields().entrySet()) { //4、將非高亮數據替換成高亮數據 String key = entry.getKey(); //如果當前非高亮對象中有該高亮數據對應的非高亮對象,則進行替換 if(sourceAsMap.containsKey(key)){ //高亮碎片 String hlresult = transTxtToArrayToString(entry.getValue().getFragments()); if(!StringUtils.isEmpty(hlresult)){ //替換高亮 sourceAsMap.put(key,hlresult); } } } //更新hit的數據 hit.sourceRef(new ByteBufferReference(ByteBuffer.wrap(JSONObject.toJSONString(sourceAsMap).getBytes()))); } return super.mapResults(response, clazz, pageable); } /*** * Text轉成字元串 * @param fragments * @return */ public String transTxtToArrayToString(Text[] fragments){ if(fragments!=null){ StringBuffer buffer = new StringBuffer(); for (Text fragment : fragments) { buffer.append(fragment.toString()); } return buffer.toString(); } return null; } }
@Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate;
AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,new HighlightResultMapper());
完整類程式碼
@Service public class SkuSearchServiceImpl implements SkuSearchService { @Autowired private SkuSearchMapper skuSearchMapper; @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; /**** * 關鍵詞搜索 * @param searchMap * 關鍵詞:keywords->name * @return */ @Override public Map<String, Object> search(Map<String, Object> searchMap) { //QueryBuilder->構建搜索條件 NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分組搜索調用 group(queryBuilder,searchMap); //1.設置高亮資訊 關鍵詞前(後)面的標籤、設置高亮域 HighlightBuilder.Field field = new HighlightBuilder .Field("name") //根據指定的域進行高亮查詢 .preTags("<span style=\"color:red;\">") //關鍵詞高亮前綴 .postTags("</span>") //高亮關鍵詞後綴 .fragmentSize(100); //碎片長度 queryBuilder.withHighlightFields(field); //2.將非高亮數據替換成高亮數據 //skuSearchMapper進行搜索 //Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); //AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,new HighlightResultMapper()); //獲取結果集:集合列表、總記錄數 Map<String,Object> resultMap = new HashMap<String,Object>(); //分組數據解析 parseGroup(page.getAggregations(),resultMap); //動態屬性解析 attrParse(resultMap); List<SkuEs> list = page.getContent(); resultMap.put("list",list); resultMap.put("totalElements",page.getTotalElements()); return resultMap; } /**** * 將屬性資訊合併成Map對象 */ public void attrParse(Map<String,Object> searchMap){ //先獲取attrmaps Object attrmaps = searchMap.get("attrmaps"); if(attrmaps!=null){ //集合數據 List<String> groupList= (List<String>) attrmaps; //定義一個集合Map<String,Set<String>>,存儲所有匯總數據 Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>(); //循環集合 for (String attr : groupList) { Map<String,String> attrMap = JSON.parseObject(attr,Map.class); for (Map.Entry<String, String> entry : attrMap.entrySet()) { //獲取每條記錄,將記錄轉成Map 就業薪資 學習費用 String key = entry.getKey(); Set<String> values = allMaps.get(key); //空表示沒有這個對象 if(values==null){ values = new HashSet<String>(); } values.add(entry.getValue()); //覆蓋之前的數據 allMaps.put(key,values); } } //覆蓋之前的attrmaps searchMap.put("attrmaps",allMaps); } } /*** * 分組結果解析 */ public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){ if(aggregations!=null){ for (Aggregation aggregation : aggregations) { //強轉ParsedStringTerms ParsedStringTerms terms = (ParsedStringTerms) aggregation; //循環結果集對象 List<String> values = new ArrayList<String>(); for (Terms.Bucket bucket : terms.getBuckets()) { values.add(bucket.getKeyAsString()); } //名字 String key = aggregation.getName(); resultMap.put(key,values); } } } /*** * 分組查詢 */ public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){ //用戶如果沒有輸入分類條件,則需要將分類搜索出來,作為條件提供給用戶 if(StringUtils.isEmpty(searchMap.get("category"))){ queryBuilder.addAggregation( AggregationBuilders .terms("categoryList")//別名,類似Map的key .field("categoryName")//根據categoryName域進行分組 .size(100) //分組結果顯示100個 ); } //用戶如果沒有輸入品牌條件,則需要將品牌搜索出來,作為條件提供給用戶 if(StringUtils.isEmpty(searchMap.get("brand"))){ queryBuilder.addAggregation( AggregationBuilders .terms("brandList")//別名,類似Map的key .field("brandName")//根據brandName域進行分組 .size(100) //分組結果顯示100個 ); } //屬性分組查詢 queryBuilder.addAggregation( AggregationBuilders .terms("attrmaps")//別名,類似Map的key .field("skuAttribute")//根據skuAttribute域進行分組 .size(100000) //分組結果顯示100000個 ); } /**** * 搜索條件構建 * @param searchMap * @return */ /**** * 搜索條件構建 * @param searchMap * @return */ public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){ NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //組合查詢對象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判斷關鍵詞是否為空,不為空,則設置條件 if(searchMap!=null && searchMap.size()>0){ //關鍵詞條件 Object keywords = searchMap.get("keywords"); if(!StringUtils.isEmpty(keywords)){ //builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString())); } //分類查詢 Object category = searchMap.get("category"); if(!StringUtils.isEmpty(category)){ boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString())); } //品牌查詢 Object brand = searchMap.get("brand"); if(!StringUtils.isEmpty(brand)){ boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString())); } //價格區間查詢 price=0-500元 500-1000元 1000元以上 Object price = searchMap.get("price"); if(!StringUtils.isEmpty(price)){ //價格區間 String[] prices = price.toString().replace("元","").replace("以上","").split("-"); //price>x boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0]))); //price<=y if(prices.length==2){ boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1]))); } } //動態屬性查詢 for (Map.Entry<String, Object> entry : searchMap.entrySet()) { //以attr_開始,動態屬性 attr_網路:移動5G if(entry.getKey().startsWith("attr_")){ String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword"; boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString())); } } //排序 Object sfield = searchMap.get("sfield"); Object sm = searchMap.get("sm"); if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){ builder.withSort( SortBuilders.fieldSort(sfield.toString()) //指定排序域 .order(SortOrder.valueOf(sm.toString())) //排序方式 ); } } //分頁查詢 builder.withPageable(PageRequest.of(currentPage(searchMap),5)); return builder.withQuery(boolQueryBuilder); } /*** * 分頁參數 */ public int currentPage(Map<String,Object> searchMap){ try { Object page = searchMap.get("page"); return Integer.valueOf(page.toString())-1; } catch (Exception e) { return 0; } } /*** * 增加索引 * @param skuEs */ @Override public void add(SkuEs skuEs) { //獲取屬性 String attrMap = skuEs.getSkuAttribute(); if(!StringUtils.isEmpty(attrMap)){ //將屬性添加到attrMap中 skuEs.setAttrMap(JSON.parseObject(attrMap, Map.class)); } skuSearchMapper.save(skuEs); } /*** * 根據主鍵刪除索引 * @param id */ @Override public void del(String id) { skuSearchMapper.deleteById(id); } }
源碼://gitee.com/TongHuaShuShuoWoDeJieJu/spring-cloud-alibaba1.git