ElasticSearch的使用
概念
ElasticSearch是一個基于Lucene的搜索搜索引擎,它提供了一個分布式多用戶能力的全文搜索引擎,基于RESTful web介面,Elasticsearch是用Java開發的,并作為Apache許可條款下的開放原始碼發布,是當前流行的企業級搜索引擎,設計用于云計算中,能夠達到實時搜索,穩定,可靠,快速,安裝使用方便,
同類產品 solr
直奔主題,整體思路
使用目的: 作為門戶網站的分布式專案,其中的搜索欄,如果使用模糊搜索,不僅效率低,且對資料庫造成較大的壓力,千萬級的資料無法在短時間內搜索,用戶體驗不好,所以必須使用ElasticSearch來幫助我們開發搜索模塊
思路如下 圖所示:

開發商品模塊介面,目的: 通過資料庫查詢,獲取商品資訊,屆時通過OpenFeign宣告介面,給search模塊微服務呼叫,用來初始化ES中的index索引
1. 商品微服務的介面開發
a. 正常介面開發: 使用的是MybatisPlus去連接Mysql的資料庫,在對應的表,mall_goods , mall_goods_brand , mall_goods_cat 通過三表的連查去查找所有審核通過的商品資訊
b. 使用OpenFeign的介面宣告,去宣告介面,給其他需要的微服務呼叫
goods微服務中的依賴
<!-- mvc 起步依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</depedency>
<!-- nacos的注冊與發現 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</depedency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</depedency>
<!-- 端點監控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</depedency>
<!-- druid起步依賴 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</depedency>
<!-- mysql驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</depedency>
<!-- mybatisplus 起步依賴 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</depedency>
<!-- entity依賴 -->
<dependency>
<groupId>com.fengmi</groupId>
<artifactId>fengmi-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</depedency>
goods微服務中的mapper層
public interface MallGoodsMapper extends BaseMapper<MallGoods> {
// 查詢所有審核通過的商品
public List<MallGoods> findAllGoodsAudited();
}
goods微服務中的mapper層映射檔案
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fengmi.mapper.MallGoodsMapper">
<!-- public List<MallGoods> findAllGoodsAudited(); -->
<resultMap id="findAllGoodsAuditedMap" type="mallgoods">
<id column="spu_id" property="spuId"/>
<result column="goods_name" property="goodsName"/>
<result column="price" property="price"/>
<result column="album_pics" property="albumPics"/>
<association property="mallGoodsBrand" javaType="mallGoodsBrand">
<id column="brandId" property="id"/>
<result column="brandName" property="name"/>
</association>
<association property="cat1" javaType="MallGoodsCat">
<id column="cat1Id" property="id"/>
<result column="cat1Name" property="name"/>
</association>
<association property="cat2" javaType="MallGoodsCat">
<id column="cat2Id" property="id"/>
<result column="cat2Name" property="name"/>
</association>
<association property="cat3" javaType="MallGoodsCat">
<id column="cat3Id" property="id"/>
<result column="cat3Name" property="name"/>
</association>
</resultMap>
<select id="findAllGoodsAudited" resultMap="findAllGoodsAuditedMap">
SELECT
goods.spu_id,
goods.goods_name,
goods.price,
goods.album_pics,
goods.brand_id AS brandId,
brand.name AS brandName,
goods.category1_id AS cat1Id,
cat1.name AS cat1Name,
goods.category2_id AS cat2Id,
cat2.name AS cat2Name,
goods.category3_id AS cat3Id,
cat3.name AS cat3Name
FROM mall_goods goods
LEFT JOIN mall_goods_brand brand ON goods.brand_id = brand.id
LEFT JOIN mall_goods_cat cat1 ON goods.category1_id = cat1.id
LEFT JOIN mall_goods_cat cat2 ON goods.category2_id = cat2.id
LEFT JOIN mall_goods_cat cat3 ON goods.category3_id = cat3.id
WHERE goods.audit_status = 1;
</select>
</mapper>
goods微服務中的service層
public interface IMallGoodsService extends IService<MallGoods> {
// 查詢所有審核通過上線的商品資訊
public List<MallGoods> findAllGoodsInfo();
}
@Service
public class MallGoodsServiceImpl extends ServiceImpl<MallGoodsMapper, MallGoods> implements IMallGoodsService {
@Override
public List<MallGoods> findAllGoodsInfo() {
return this.baseMapper.findAllGoodsAudited();
}
}
goods微服務中的controller層
@RestController
@RequestMapping("/goods")
public class MallGoodsController {
@Autowired
private IMallGoodsService goodsService;
@GetMapping("findAllGoodsInfo")
public List<MallGoods> findAllGoodsInfo() {
return goodsService.findAllGoodsInfo();
}
}
goods微服務進行OpenFeign的介面宣告
這里需要創建一個模塊,專門用于OpenFeign的介面宣告,注意分包!!!
import com.fengmi.goods.MallGoods;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient("fengmi-goods")
@RequestMapping("goods")
public interface GoodsApi {
@GetMapping("findAllGoodsInfo")
public List<MallGoods> findAllGoodsInfo();
}
2. search模塊微服務的介面開發
這一塊微服務主要分兩個步驟:
a. 初始化elasticsearch的索引和域 => 這需要查詢到資料庫中的資料(呼叫以上介面即可),通過ElasticsearchRestTemplate中的 save()方法去初始化
b. 查詢操作 => 需要系結查詢的條件,如bool復合查詢,highlight高亮顯示,aggs分組,filters過濾等,這需要熟練使用ElasticSearchRestTemplate的Api
2.1 開發所需依賴
<!-- es的起步依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- nacos的注冊與發現 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- springMVC 起步依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 端點監控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- entity依賴 -->
<dependency>
<groupId>com.fengmi</groupId>
<artifactId>fengmi-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- OpenFeign 的宣告模塊的依賴 -->
<dependency>
<groupId>com.fengmi</groupId>
<artifactId>fengmi-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.2 引導類中需要掃描
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.fengmi.api.goods"}) // 掃描才能呼叫openfeign的介面哦
public class SearchApp {
public static void main(String[] args) {
SpringApplication.run(SearchApp.class,args);
}
}
2.3 初始化ElasticSearch的介面
service 的介面
public interface ISearchService {
// 初始化elasticsearch 看這里!!!!
public ResultVO initEs();
// 使用es查詢并分頁
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO);
}
service的實作
明確
所謂的elasticsearch初始化主要分兩個步驟:
- 洗掉之前可能存在的索引,對應Api => ElasticSearchRestTemplate.deleteIndex(對應的物體Class),之后再通過設計的物體去創建索引
- 查詢資料庫中的資料,然后遍歷轉換為設定的索引物體類,之后使用save()方法匯入資料
至此,索引,域創建完畢,資料匯入完畢
- 索引物體類
物體類的設計對于其中欄位要明確三個維度:
- 要不要分詞,通過 type 去確認欄位的型別,如果type確認欄位是 TEXT 那么就可以通過 analyzer去確定分詞的型別是 ik_max_word 或者 ik_smart ;如果欄位的型別設定為了Keyword,表示這個欄位是不使用分詞的,就是其本身
- 要不要建立索引,通過 index 屬性來設定,一般的欄位都是需要建立索引的,但如圖片地址等一些 不會用到的搜索條件,我們可以不建立索引
- 要不要儲存,通過 store 屬性來設定,一般來說我們設計的欄位都是需要儲存的
package com.fengmi.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
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;
@Data
@Document(indexName = "es-goods", shards = 1, replicas = 0)
public class ESGoods {
@Id
private Long spuId; // spuId
@Field(type = FieldType.Text, analyzer = "ik_max_word",store=true)
private String goodsName;
@Field(type=FieldType.Long,index=true,store=true)
private Long brandId; // 品牌id
@Field(type = FieldType.Keyword,index=true,store=true)
private String brandName; //品牌名稱
@Field(type=FieldType.Long,index=true,store=true)
private Long cid1id; // 1級分類id
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat1name; // 1級分類名稱
@Field(type=FieldType.Long,index=true,store=true)
private Long cid2id; // 2級分類id
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat2name; // 2級分類名稱
@Field(type=FieldType.Long,index=true,store=true)
private Long cid3id; // 3級分類id
@Field(type = FieldType.Keyword,index=true,store=true)
private String cat3name; // 3級分類名稱
@Field(type=FieldType.Date,index=true,store=true,format = DateFormat.date_time)
private Date createTime; // 創建時間
@Field(type=FieldType.Double,index=true,store=true)
private Double price; // 價格,spu默認的sku的price
@Field(type = FieldType.Keyword,index = false,store=true)
private String smallPic; // 圖片地址
}
- 物體類設計完畢,下面開始做es初始化的作業,代碼如下:
@Service
public class SearchService implements ISearchService {
@Autowired
private GoodsApi goodsApi;
@Autowired
private ElasticsearchRestTemplate restTemplate;
@Override
public ResultVO initEs() {
// 洗掉原有索引
restTemplate.deleteIndex(ESGoods.class);
// 初始化索引
restTemplate.createIndex(ESGoods.class);
// 初始化分詞器
restTemplate.putMapping(ESGoods.class);
// 遠程呼叫goodsApi 查詢商品資訊
List<MallGoods> allGoodsInfo = goodsApi.findAllGoodsInfo();
if (allGoodsInfo == null || allGoodsInfo.size() < 0) {
return new ResultVO(false, "初始化失敗");
}
// 遍歷 將物體類的映射設定到索引庫
List<ESGoods> goodsList = allGoodsInfo.stream().map(spu -> {
ESGoods esGoods = new ESGoods();
// id
esGoods.setSpuId(spu.getSpuId());
// brand 品牌資訊
esGoods.setBrandId(spu.getMallGoodsBrand().getId());
esGoods.setBrandName(spu.getMallGoodsBrand().getName());
// 分類資訊
esGoods.setCid1id(spu.getCat1().getId());
esGoods.setCat1name(spu.getCat1().getName());
esGoods.setCid2id(spu.getCat2().getId());
esGoods.setCat2name(spu.getCat2().getName());
esGoods.setCid3id(spu.getCat3().getId());
esGoods.setCat3name(spu.getCat3().getName());
// 維護創建時間,后面可以做排序處理
esGoods.setCreateTime(new Date());
// 商品名稱
esGoods.setGoodsName(spu.getGoodsName());
esGoods.setPrice(spu.getPrice().doubleValue());
// 圖片是字串形式,需要做處理
String albumPics = spu.getAlbumPics();
// 對字串做處理要先進行非空判斷
if (!StringUtils.isEmpty(albumPics)) {
String[] split = albumPics.split(",");
// 非空判斷
if (split != null && split.length > 0) {
esGoods.setSmallPic(spu.getAlbumPics());
}
}
return esGoods;
}).collect(Collectors.toList());
// 保存到es中
restTemplate.save(goodsList);
return new ResultVO(true, "elasticsearch初始化成功");
}
}
提供初始化的controller介面
@RestController
@RequestMapping("search")
public class SearchController {
@Autowired
private ISearchService searchService;
// 初始化介面方法
@RequestMapping("initES")
public ResultVO initES() {
return searchService.initEs();
}
}
至此,初始化的作業進行完畢
2.4 使用ElasticSearch查詢介面開發
接下來就是重頭戲了,使用elasticsearch來作為查詢的作業
2.4.1 思路
- 先做基本查詢的功能,需要多個域匹配,所以我們采用復合查詢bool中的should來操作
- 基本查詢完成之后進行分頁的操作,可以通過設定分頁來完成
- 對輸入的關鍵字進行設定,采用高亮設定,以實作查詢
- 分組,聚合查詢,完成品牌和種類串列的實作
- 過濾,這里必須使用過濾,對上述查詢的結果進行過濾的操作不會影響分數!
- 排序,對結果集添加排序的操作,這里實作價格的排序,其他方法是一致的
- 先看看原型界面

明確要求:
- 查詢時,首先是要匹配幾個域的,分別是: 商品的名稱goodsName,商品的品牌brandName,商品三級分類的名稱cat3name,而這幾個域只要有一個匹配上就展示,這樣能搜索到的商品就更多
- 域的分詞,goodsName采用 ik_max_word分詞,這樣也能搜索到更多的產品,以提升用戶的購買率,品牌和商品三級分類則可以不需要分詞
- 以上要求組合起來可以得到:需要使用復合查詢,should條件拼接
- 順帶可以將分頁也處理了,只需添加分頁設定即可
2.4.2 基本分頁查詢
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
// 非空判斷
if (searchDTO == null) {
return new PageResultVO<>(false, "引數不合法");
}
// 從es中查詢
// 1. 多重匹配查詢
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 條件匹配,需要分詞,匹配商品名稱,從條件dto中獲取,先做非空判斷,keyword為空是查所有
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 條件匹配,不分詞,匹配品牌名稱
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 條件匹配 , 不分詞, 匹配3級分類名稱
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一個符合就可以,這樣符合電商的性質
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 設定分頁資訊,防止惡意攻擊
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 7. 創建并設定查詢物件
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 設定查詢條件
.withQuery(boolQueryBuilder)
}
- searchDTO類
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchDTO {
// 搜索的關鍵字
private String keyword;
// 當前頁
private Integer page = 1;
// 每頁條數
private Integer size = 5;
// 過濾所需欄位
private String brandNameFilter;
private String cat3NameFilter;
// 排序所需欄位
private String sortField;
}
- 回應類:ResultVO 和PageResultVO
@Data
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
public class ResultVO {
@NonNull
private boolean success; //標記操作的狀態
@NonNull
private String msg; //錯誤資訊
private Object data ; //資料
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
public class PageResultVO<T> {
private Long total; // 總記錄數
private Integer pages; // 總頁數
private List<T> data; // 每頁資料
@NonNull
private boolean success; // 回應標志位
@NonNull
private String msg; // 回應資訊
}
2.4.3 高亮設定
對于查詢的關鍵字keyword可以設定高亮顯示,這樣的用戶體驗會更好
@Override
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
// 非空判斷
if (searchDTO == null) {
return new PageResultVO<>(false, "引數不合法");
}
// 從es中查詢
// 1. 多重匹配查詢
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 條件匹配,需要分詞,匹配商品名稱,從條件dto中獲取,先做非空判斷
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 條件匹配,不分詞,匹配品牌名稱
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 條件匹配 , 不分詞, 匹配3級分類名稱
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一個符合就可以,這樣符合電商的性質
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 設定分頁資訊,防止惡意攻擊
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 7. 創建并設定查詢物件
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 設定查詢條件
.withQuery(boolQueryBuilder)
// 設定分頁條件
.withPageable(pageRequest)
// 設定高亮
.withHighlightBuilder(getHighlightBuilder("goodsName"))
// 8. 查詢
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
// 9. 設定總條數
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
// 10. 獲取命中物件
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
// 11. 遍歷,處理每頁資料,和高亮
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
// 獲取內容
ESGoods esGoods = hit.getContent();
// 設定高亮資訊
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
// v 即是每個資訊中高亮的陣列屬性,做非空判斷
if (v != null && v.size() > 0) {
// 如果k與域名相同,那么才設定高亮
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
// 設定每頁資訊
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
// 設定高亮欄位 自定義方法
private HighlightBuilder getHighlightBuilder(String... fields) {
// 高亮條件
HighlightBuilder highlightBuilder = new HighlightBuilder(); //生成高亮查詢器
for (String field : fields) {
highlightBuilder.field(field);//高亮查詢欄位
}
highlightBuilder.requireFieldMatch(false); //如果要多個欄位高亮,這項要為false
highlightBuilder.preTags("<span style=\"color:red\">"); //高亮設定
highlightBuilder.postTags("</span>");
//下面這兩項,如果你要高亮如文字內容等有很多字的欄位,必須配置,不然會導致高亮不全,文章內容缺失等
highlightBuilder.fragmentSize(800000); //最大高亮分片數
highlightBuilder.numOfFragments(0); //從第一個分片獲取高亮片段
return highlightBuilder;
}
}
2.4.4 聚合統計品牌和分類資訊
回憶一下在kibana中我們是怎么做聚合的,是與query同級,使用aggs來聚合,設定聚合組名,聚合桶只能使用term不分詞,設定field欄位對應的域
那么在代碼中使用聚合也是在與查詢同級
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
// 非空判斷
if (searchDTO == null) {
return new PageResultVO<>(false, "引數不合法");
}
// 從es中查詢
// 1. 多重匹配查詢
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 條件匹配,需要分詞,匹配商品名稱,從條件dto中獲取,先做非空判斷
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 條件匹配,不分詞,匹配品牌名稱
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 條件匹配 , 不分詞, 匹配3級分類名稱
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一個符合就可以,這樣符合電商的性質
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 設定分頁資訊,防止惡意攻擊
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 6.1設定聚合
// terms設定組名,field設定對應域,size設定桶的個數
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
// 7. 創建并設定查詢物件
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 設定查詢條件
.withQuery(boolQueryBuilder)
// 設定分頁條件
.withPageable(pageRequest)
// 設定高亮
.withHighlightBuilder(getHighlightBuilder("goodsName"))
// 設定聚合
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
// 8. 查詢
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
// 9. 設定總條數
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
// 10. 獲取命中物件
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
// 11. 遍歷,處理每頁資料,和高亮
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
// 獲取內容
ESGoods esGoods = hit.getContent();
// 設定高亮資訊
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
// v 即是每個資訊中高亮的陣列屬性,做非空判斷
if (v != null && v.size() > 0) {
// 如果k與域名相同,那么才設定高亮
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
// 12. 獲取聚合資訊
Aggregations aggregations = search.getAggregations();
// 從指定的域中獲取聚合的資訊
Terms brandName = aggregations.get("brandAgg");
Terms cat3name = aggregations.get("cat3Agg");
// 獲取桶
List<? extends Terms.Bucket> brandNameBuckets = brandName.getBuckets();
List<? extends Terms.Bucket> cat3nameBuckets = cat3name.getBuckets();
// 遍歷獲取
List<String> brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List<String> cat3NameList = cat3nameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setCat3NameList(cat3NameList);
// 設定每頁資訊
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
// 設定高亮欄位
private HighlightBuilder getHighlightBuilder(String... fields) {
// 高亮條件
HighlightBuilder highlightBuilder = new HighlightBuilder(); //生成高亮查詢器
for (String field : fields) {
highlightBuilder.field(field);//高亮查詢欄位
}
highlightBuilder.requireFieldMatch(false); //如果要多個欄位高亮,這項要為false
highlightBuilder.preTags("<span style=\"color:red\">"); //高亮設定
highlightBuilder.postTags("</span>");
//下面這兩項,如果你要高亮如文字內容等有很多字的欄位,必須配置,不然會導致高亮不全,文章內容缺失等
highlightBuilder.fragmentSize(800000); //最大高亮分片數
highlightBuilder.numOfFragments(0); //從第一個分片獲取高亮片段
return highlightBuilder;
}
2.4.5 過濾查詢,通過聚合出的品牌名稱和分類名稱過濾
為什么要使用過濾而非在復合條件中添加?
關鍵字: 分數
使用復合條件查詢對結果的分數會有影響,就會影響到排序,頁面圖片的改變就會變大,這里我們希望是不影響結果的分數,那么只能使用過濾的方式來實作
回憶一下在kibana中怎么使用過濾的:
過濾在bool復合查詢中,使用filters與bool同級,使用term或者terms來設定過濾的欄位,該欄位是不分詞的
所以在代碼中使用過濾也是在bool中組合,而非與查詢同級
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
// 非空判斷
if (searchDTO == null) {
return new PageResultVO<>(false, "引數不合法");
}
// 從es中查詢
// 1. 多重匹配查詢
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 條件匹配,需要分詞,匹配商品名稱,從條件dto中獲取,先做非空判斷
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 條件匹配,不分詞,匹配品牌名稱
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 條件匹配 , 不分詞, 匹配3級分類名稱
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一個符合就可以,這樣符合電商的性質
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 設定分頁資訊,防止惡意攻擊
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 6.1設定聚合
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
// 6.2 設定過濾,過濾不會影響分數,過濾在bool中!!!!
if (!StringUtils.isEmpty(searchDTO.getBrandNameFilter())) {
// 不為空才設定
TermQueryBuilder brandNameTermQueryBuilder = QueryBuilders.termQuery("brandName", searchDTO.getBrandNameFilter());
// 系結到復合查詢中
boolQueryBuilder.filter(brandNameTermQueryBuilder);
}
if (!StringUtils.isEmpty(searchDTO.getCat3NameFilter())) {
// 不為空才設定
TermQueryBuilder cat3namebrandNameTermQueryBuilder = QueryBuilders.termQuery("cat3name", searchDTO.getCat3NameFilter());
// 系結到復合查詢中
boolQueryBuilder.filter(cat3namebrandNameTermQueryBuilder);
}
// 7. 創建并設定查詢物件
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 設定查詢條件,這里面已經設定了過濾了
.withQuery(boolQueryBuilder)
// 設定分頁條件
.withPageable(pageRequest)
// 設定高亮
.withHighlightBuilder(getHighlightBuilder("goodsName"))
// 設定聚合
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
// 8. 查詢
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
// 9. 設定總條數
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
// 10. 獲取命中物件
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
// 11. 遍歷,處理每頁資料,和高亮
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
// 獲取內容
ESGoods esGoods = hit.getContent();
// 設定高亮資訊
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
// v 即是每個資訊中高亮的陣列屬性,做非空判斷
if (v != null && v.size() > 0) {
// 如果k與域名相同,那么才設定高亮
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
// 12. 從查詢結果中獲取聚合資訊
Aggregations aggregations = search.getAggregations();
// 從指定的域中獲取聚合的資訊
Terms brandName = aggregations.get("brandAgg");
Terms cat3name = aggregations.get("cat3Agg");
// 獲取桶
List<? extends Terms.Bucket> brandNameBuckets = brandName.getBuckets();
List<? extends Terms.Bucket> cat3nameBuckets = cat3name.getBuckets();
// 遍歷獲取
List<String> brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List<String> cat3NameList = cat3nameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setCat3NameList(cat3NameList);
// 設定每頁資訊
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
// 設定高亮欄位
private HighlightBuilder getHighlightBuilder(String... fields) {
// 高亮條件
HighlightBuilder highlightBuilder = new HighlightBuilder(); //生成高亮查詢器
for (String field : fields) {
highlightBuilder.field(field);//高亮查詢欄位
}
highlightBuilder.requireFieldMatch(false); //如果要多個欄位高亮,這項要為false
highlightBuilder.preTags("<span style=\"color:red\">"); //高亮設定
highlightBuilder.postTags("</span>");
//下面這兩項,如果你要高亮如文字內容等有很多字的欄位,必須配置,不然會導致高亮不全,文章內容缺失等
highlightBuilder.fragmentSize(800000); //最大高亮分片數
highlightBuilder.numOfFragments(0); //從第一個分片獲取高亮片段
return highlightBuilder;
}
2.4.6 設定排序
對于排序,我們可以通過頁面原型發現,這里需要做的是對商品價格的排序
在查詢的DTO中插入排序的欄位String sortFilter,通過對傳入的欄位是asc還是desc來決定排序方式
這里需要注意的是:可能前端未傳入排序方式,那么此時就不需要在查詢中添加排序
回憶一下在kibana中我們是怎么使用排序的:
排序sort,和aggs聚合一樣與query同級,設定排序的欄位,order中設定排序方式,asc為升序,desc為降序
所以在代碼中的查詢中添加排序條件
- 由此給出全部的查詢在kibana中的查詢陳述句,供參考和對比
GET es-goods/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"goodsName": "搶購"
}
},
{
"term": {
"brandName": {
"value": "良品鋪子"
}
}
},
{
"term": {
"cat3name": {
"value": ""
}
}
}
],
"filter": [
{
"term": {
"brandName": "百草味"
}
}
]
}
},
"aggs": {
"brandName_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"cat3Name_agg":{
"terms": {
"field": "cat3name",
"size": 10
}
}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
- 最終代碼實作:
@Override
public PageResultVO<ESGoods> searchAndPageByES(SearchDTO searchDTO) {
// 非空判斷
if (searchDTO == null) {
return new PageResultVO<>(false, "引數不合法");
}
// 從es中查詢
// 1. 多重匹配查詢
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 2. 條件匹配,需要分詞,匹配商品名稱,從條件dto中獲取,先做非空判斷
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
MatchQueryBuilder goodsName = QueryBuilders.matchQuery("goodsName", searchDTO.getKeyword());
// 3. 條件匹配,不分詞,匹配品牌名稱
TermQueryBuilder brandName = QueryBuilders.termQuery("brandName", searchDTO.getKeyword());
// 4. 條件匹配 , 不分詞, 匹配3級分類名稱
TermQueryBuilder cat3name = QueryBuilders.termQuery("cat3name", searchDTO.getKeyword());
// 5. 使用should匹配,只要有一個符合就可以,這樣符合電商的性質
boolQueryBuilder.should(goodsName).should(brandName).should(cat3name);
}
// 6. 設定分頁資訊,防止惡意攻擊
if (searchDTO.getPage() <= 0 || searchDTO.getSize() <= 0) {
searchDTO.setPage(1);
searchDTO.setSize(5);
}
PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getSize());
// 6.1設定聚合
TermsAggregationBuilder brandNameTermsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brandName").size(30);
TermsAggregationBuilder cat3nameTermsAggregationBuilder = AggregationBuilders.terms("cat3Agg").field("cat3name").size(30);
// 6.2 設定過濾,過濾不會影響分數,過濾在bool中!!!!
if (!StringUtils.isEmpty(searchDTO.getBrandNameFilter())) {
// 不為空才設置
TermQueryBuilder brandNameTermQueryBuilder = QueryBuilders.termQuery("brandName", searchDTO.getBrandNameFilter());
boolQueryBuilder.filter(brandNameTermQueryBuilder);
}
if (!StringUtils.isEmpty(searchDTO.getCat3NameFilter())) {
// 不為空才設定
TermQueryBuilder cat3namebrandNameTermQueryBuilder = QueryBuilders.termQuery("cat3name", searchDTO.getCat3NameFilter());
boolQueryBuilder.filter(cat3namebrandNameTermQueryBuilder);
}
// 6.3 設定排序 , 排序與query同級
// 提出變數,按條件賦值
FieldSortBuilder price = null;
if (!StringUtils.isEmpty(searchDTO.getSortField()) && "desc".equals(searchDTO.getSortField())) {
// 不為空,且前端傳過來的是desc ,則按降序排列
price = SortBuilders.fieldSort("price").order(SortOrder.DESC);
}
if (!StringUtils.isEmpty(searchDTO.getSortField()) && "asc".equals(searchDTO.getSortField())) {
// 不為空,且前端傳過來的是desc ,則按降序排列
price = SortBuilders.fieldSort("price").order(SortOrder.ASC);
}
// 7. 創建并設定查詢物件
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder()
// 設定查詢條件
.withQuery(boolQueryBuilder)
// 設定分頁條件
.withPageable(pageRequest)
// 設定高亮
.withHighlightBuilder(getHighlightBuilder("goodsName"))
// 設定聚合
.addAggregation(brandNameTermsAggregationBuilder)
.addAggregation(cat3nameTermsAggregationBuilder);
// 7.1 判斷排序條件是否存在
if (price != null){
// 存在則加上排序
nativeSearchQueryBuilder.withSort(price);
}
// 8. 查詢
SearchHits<ESGoods> search = restTemplate.search(nativeSearchQueryBuilder.build(), ESGoods.class);
// 9. 設定總條數
PageResult pageResultVO = new PageResult();
pageResultVO.setTotal(search.getTotalHits());
// 10. 獲取命中物件
List<SearchHit<ESGoods>> searchHits = search.getSearchHits();
// 11. 遍歷,處理每頁資料,和高亮
List<ESGoods> esGoodsList = searchHits.stream().map(hit -> {
// 獲取內容
ESGoods esGoods = hit.getContent();
// 設定高亮資訊
Map<String, List<String>> highlightFields = hit.getHighlightFields();
highlightFields.forEach((k, v) -> {
// v 即是每個資訊中高亮的陣列屬性,做非空判斷
if (v != null && v.size() > 0) {
// 如果k與域名相同,那么才設定高亮
if ("goodsName".equals(k)) {
esGoods.setGoodsName(v.get(0));
}
}
});
return esGoods;
}).collect(Collectors.toList());
// 12. 從查詢結果中獲取聚合資訊
Aggregations aggregations = search.getAggregations();
// 從指定的域中獲取聚合的資訊
Terms brandName = aggregations.get("brandAgg");
Terms cat3name = aggregations.get("cat3Agg");
// 獲取桶
List<? extends Terms.Bucket> brandNameBuckets = brandName.getBuckets();
List<? extends Terms.Bucket> cat3nameBuckets = cat3name.getBuckets();
// 遍歷獲取
List<String> brandNameList = brandNameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setBrandNameList(brandNameList);
List<String> cat3NameList = cat3nameBuckets.stream().map(item -> (String) item.getKey()).collect(Collectors.toList());
pageResultVO.setCat3NameList(cat3NameList);
// 設定每頁資訊
pageResultVO.setData(esGoodsList);
return pageResultVO;
}
// 設定高亮欄位
private HighlightBuilder getHighlightBuilder(String... fields) {
// 高亮條件
HighlightBuilder highlightBuilder = new HighlightBuilder(); //生成高亮查詢器
for (String field : fields) {
highlightBuilder.field(field);//高亮查詢欄位
}
highlightBuilder.requireFieldMatch(false); //如果要多個欄位高亮,這項要為false
highlightBuilder.preTags("<span style=\"color:red\">"); //高亮設定
highlightBuilder.postTags("</span>");
//下面這兩項,如果你要高亮如文字內容等有很多字的欄位,必須配置,不然會導致高亮不全,文章內容缺失等
highlightBuilder.fragmentSize(800000); //最大高亮分片數
highlightBuilder.numOfFragments(0); //從第一個分片獲取高亮片段
return highlightBuilder;
}
至此,一個ElasticSearch的基本使用完成,你學廢了么?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/317839.html
標籤:其他
上一篇:java Spring Cloud+Spring boot+mybatis企業快速開發架構之云架構系統管理平臺
下一篇:hive資料庫及表操作
