看下圖的淘寶頁面,可以看到搜尋有多個條件及搜尋產品,並且支援多種排序方式,例如按價格;其實這塊有個特點,就是不管你搜尋哪個商品他都是有分類的,以及他對應的品牌,這兩個是固定的,但其它引數不一定所有商品都具有;這一塊設計就涉及到動態變化資料的載入,設計是比較複雜的,這個可以在後面慢慢說,其實這次想分析的主要是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); } }
原始碼:https://gitee.com/TongHuaShuShuoWoDeJieJu/spring-cloud-alibaba1.git