1 Easy-Es
使用過Spring Data操作ES的小伙伴應(yīng)該有所了解,它只能實(shí)現(xiàn)一些非?;镜臄?shù)據(jù)管理工作,一旦遇到稍微復(fù)雜點(diǎn)的查詢,基本都要依賴ES官方提供的RestHighLevelClient,Spring Data只是在其基礎(chǔ)上進(jìn)行了簡單的封裝。最近發(fā)現(xiàn)一款更優(yōu)雅的ES ORM框架Easy-Es,使用它能像MyBatis-Plus一樣操作ES
點(diǎn)擊了解 SpringBoot 整合ElasticSearch
1.1 簡介
Easy-Es(簡稱EE)是一款基于Elasticsearch(簡稱ES)官方提供的RestHighLevelClient打造的ORM開發(fā)框架,在RestHighLevelClient的基礎(chǔ)上,只做增強(qiáng)不做改變,為簡化開發(fā)、提高效率而生。EE和Mybatis-Plus(簡稱MP)的用法非常相似,如果你之前使用過MP的話,應(yīng)該能很快上手EE。EE的理念是:把簡單、易用、方便留給用戶,把復(fù)雜留給框架。
官網(wǎng)地址:https://www.easy-es.cn/
EE的主要特性如下:
- 全自動索引托管:開發(fā)者無需關(guān)心索引的創(chuàng)建、更新及數(shù)據(jù)遷移等繁瑣步驟,框架能自動完成。
- 屏蔽語言差異:開發(fā)者只需要會MySQL的語法即可使用ES。
- 代碼量極少:與直接使用官方提供的
RestHighLevelClient相比,相同的查詢平均可以節(jié)省3-5倍的代碼量。 - 零魔法值:字段名稱直接從實(shí)體中獲取,無需手寫。
- 零額外學(xué)習(xí)成本: 開發(fā)者只要會國內(nèi)最受歡迎的Mybatis-Plus用法,即可無縫遷移至EE。
1.2 MySQL與Easy-Es語法對比
首先我們來對MySQL、Easy-Es和RestHighLevelClient的語法做過對比,來快速學(xué)習(xí)下Easy-Es的語法。
| MySQL | Easy-Es | es-DSL/es java api |
|---|---|---|
| and | and | must |
| or | or | should |
| = | eq | term |
| != | ne | boolQueryBuilder.mustNot(queryBuilder) |
| > | gt | QueryBuilders.rangeQuery('es field').gt() |
| >= | ge | QueryBuilders.rangeQuery('es field').gte() |
| < | lt | QueryBuilders.rangeQuery('es field').lt() |
| <= | le | QueryBuilders.rangeQuery('es field').lte() |
| like '%field%' | like | QueryBuilders.wildcardQuery(field,value) |
| not like '%field%' | notLike | must not wildcardQuery(field,value) |
| like '%field' | likeLeft | QueryBuilders.wildcardQuery(field,*value) |
| like 'field%' | likeRight | QueryBuilders.wildcardQuery(field,value*) |
| between | between | QueryBuilders.rangeQuery('es field').from(xx).to(xx) |
| notBetween | notBetween | must not QueryBuilders.rangeQuery('es field').from(xx).to(xx) |
| is null | isNull | must not QueryBuilders.existsQuery(field) |
| is notNull | isNotNull | QueryBuilders.existsQuery(field) |
| in | in | QueryBuilders.termsQuery(" xx es field", xx) |
| not in | notIn | must not QueryBuilders.termsQuery(" xx es field", xx) |
| group by | groupBy | AggregationBuilders.terms() |
| order by | orderBy | fieldSortBuilder.order(ASC/DESC) |
| min | min | AggregationBuilders.min |
| max | max | AggregationBuilders.max |
| avg | avg | AggregationBuilders.avg |
| sum | sum | AggregationBuilders.sum |
| order by xxx asc | orderByAsc | fieldSortBuilder.order(SortOrder.ASC) |
| order by xxx desc | orderByDesc | fieldSortBuilder.order(SortOrder.DESC) |
| - | match | matchQuery |
| - | matchPhrase | QueryBuilders.matchPhraseQuery |
| - | matchPrefix | QueryBuilders.matchPhrasePrefixQuery |
| - | queryStringQuery | QueryBuilders.queryStringQuery |
| select * | matchAllQuery | QueryBuilders.matchAllQuery() |
| - | highLight | HighlightBuilder.Field |
1.3 集成及配置
1.3.1 pom.xml
<dependency>
<groupId>cn.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>1.0.2</version>
</dependency>
由于底層使用了ES官方提供的RestHighLevelClient,這里ES的相關(guān)依賴版本需要統(tǒng)一下,這里使用的ES客戶端版本為7.14.0,ES版本為7.17.3;
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
</dependency>
</dependencies>
</dependencyManagement>
1.3.2 配置
再修改配置文件application.yml對Easy-Es進(jìn)行配置。
easy-es:
# 是否開啟EE自動配置
enable: true
# ES連接地址+端口
address: localhost:9200
# 關(guān)閉自帶banner
banner: false
添加Easy-Es的Java配置,使用@EsMapperScan配置好Easy-Es的Mapper接口和文檔對象路徑,如果使用了MyBatis-Plus的話,需要和它的掃描路徑區(qū)分開來。
/**
* EasyEs配置類
*/
@Configuration
@EsMapperScan("com.test.easyes")
public class EasyEsConfig {
}
1.4 使用
Easy-Es集成和配置完成后,就可以開始使用了。
1.4.1 注解的使用
下面我們來學(xué)習(xí)下Easy-Es中注解的使用。
首先我們需要創(chuàng)建文檔對象EsProduct,然后給類和字段添加上Easy-Es的注解;
/**
* 搜索商品的信息
*/
@Data
@EqualsAndHashCode
@IndexName(value = "pms", shardsNum = 1, replicasNum = 0)
public class EsProduct implements Serializable {
private static final long serialVersionUID = -1L;
@IndexId(type = IdType.CUSTOMIZE)
private Long id;
@IndexField(fieldType = FieldType.KEYWORD)
private String productSn;
private Long brandId;
@IndexField(fieldType = FieldType.KEYWORD)
private String brandName;
private Long productCategoryId;
@IndexField(fieldType = FieldType.KEYWORD)
private String productCategoryName;
private String pic;
@IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
private String name;
@IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
private String subTitle;
@IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
private String keywords;
private BigDecimal price;
private Integer sale;
private Integer newStatus;
private Integer recommandStatus;
private Integer stock;
private Integer promotionType;
private Integer sort;
@IndexField(fieldType = FieldType.NESTED, nestedClass = EsProductAttributeValue.class)
private List<EsProductAttributeValue> attrValueList;
@Score
private Float score;
}
/**
* 嵌套類型EsProductAttributeValue
* 搜索商品的屬性信息
*/
@Data
@EqualsAndHashCode
public class EsProductAttributeValue implements Serializable {
private static final long serialVersionUID = 1L;
@IndexField(fieldType = FieldType.LONG)
private Long id;
@IndexField(fieldType = FieldType.KEYWORD)
private Long productAttributeId;
//屬性值
@IndexField(fieldType = FieldType.KEYWORD)
private String value;
//屬性參數(shù):0->規(guī)格;1->參數(shù)
@IndexField(fieldType = FieldType.INTEGER)
private Integer type;
//屬性名稱
@IndexField(fieldType=FieldType.KEYWORD)
private String name;
}
EsProduct 中的注解具體說明如下:
-
@IndexName:索引名注解,value是指定索引名;shardsNum:分片數(shù);replicasNum:副本數(shù) -
@IndexId:ES主鍵注解,type指定注解類型,CUSTOMIZE表示自定義 -
@IndexField:ES字段注解,fieldType字段在索引中的類型,analyzer索引文檔時用的分詞器,nestedClass嵌套類 -
@Score:得分注解decimalPlaces得分保留小數(shù)位,實(shí)體類中被作為ES查詢得分返回的字段使用
1.4.2 EsMapper接口
下面我們來實(shí)現(xiàn)幾個簡單的商品信息維護(hù)接口,包括商品信息的導(dǎo)入、創(chuàng)建和刪除。
首先我們需要定義一個Mapper,繼承BaseEsMapper;
/**
* 商品ES操作類
*/
public interface EsProductMapper extends BaseEsMapper<EsProduct> {
}
然后在Service實(shí)現(xiàn)類中直接使用EsProductMapper內(nèi)置方法實(shí)現(xiàn)即可
/**
* 搜索商品管理Service實(shí)現(xiàn)類
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductDao productDao;
@Autowired
private EsProductMapper esProductMapper;
@Override
public int importAll() {
List<EsProduct> esProductList = productDao.getAllEsProductList(null);
return esProductMapper.insertBatch(esProductList);
}
@Override
public void delete(Long id) {
esProductMapper.deleteById(id);
}
@Override
public EsProduct create(Long id) {
EsProduct result = null;
List<EsProduct> esProductList = productDao.getAllEsProductList(id);
if (esProductList.size() > 0) {
result = esProductList.get(0);
esProductMapper.insert(result);
}
return result;
}
@Override
public void delete(List<Long> ids) {
if (!CollectionUtils.isEmpty(ids)) {
esProductMapper.deleteBatchIds(ids);
}
}
}
1.4.3 簡單搜索
下面我們來實(shí)現(xiàn)一個最簡單的商品搜索,分頁搜索商品名稱、副標(biāo)題、關(guān)鍵詞中包含指定關(guān)鍵字的商品。
通過QueryWrapper來構(gòu)造查詢條件,然后使用Mapper中的方法來進(jìn)行查詢,使用過MyBatis-Plus的小伙伴應(yīng)該很熟悉了
/**
* 搜索商品管理Service實(shí)現(xiàn)類
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public PageInfo<EsProduct> search(String keyword, Integer pageNum, Integer pageSize) {
LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
if(StrUtil.isEmpty(keyword)){
wrapper.matchAllQuery();
}else{
wrapper.multiMatchQuery(keyword,EsProduct::getName,EsProduct::getSubTitle,EsProduct::getKeywords);
}
return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
}
}
在控制臺輸出查看生成的DSL語句

把DSL語句直接復(fù)制到 Kibana 中即可執(zhí)行查看結(jié)果了,這和我們手寫DSL語句沒什么兩樣的。

1.5 使用案例
1.5.1 綜合商品搜索
下面我們來實(shí)現(xiàn)一個復(fù)雜的商品搜索,涉及到過濾、不同字段匹配權(quán)重不同以及可以進(jìn)行排序。
首先來說需求,按輸入的關(guān)鍵字搜索商品名稱(權(quán)重10)、副標(biāo)題(權(quán)重5)和關(guān)鍵詞(權(quán)重2),可以按品牌和分類進(jìn)行篩選,可以有5種排序方式,默認(rèn)按相關(guān)度進(jìn)行排序
下面是使用Easy-Es的實(shí)現(xiàn)方式
/**
* 搜索商品管理Service實(shí)現(xiàn)類
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public PageInfo<EsProduct> search(String keyword, Long brandId, Long productCategoryId, Integer pageNum, Integer pageSize,Integer sort) {
LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
//過濾
if (brandId != null || productCategoryId != null) {
if (brandId != null) {
wrapper.eq(EsProduct::getBrandId,brandId);
}
if (productCategoryId != null) {
wrapper.eq(EsProduct::getProductCategoryId,productCategoryId).enableMust2Filter(true);
}
}
//搜索
if (StrUtil.isEmpty(keyword)) {
wrapper.matchAllQuery();
} else {
wrapper.and(i -> i.match(EsProduct::getName, keyword, 10f)
.or().match(EsProduct::getSubTitle, keyword, 5f)
.or().match(EsProduct::getKeywords, keyword, 2f));
}
//排序
if(sort==1){
//按新品從新到舊
wrapper.orderByDesc(EsProduct::getId);
}else if(sort==2){
//按銷量從高到低
wrapper.orderByDesc(EsProduct::getSale);
}else if(sort==3){
//按價格從低到高
wrapper.orderByAsc(EsProduct::getPrice);
}else if(sort==4){
//按價格從高到低
wrapper.orderByDesc(EsProduct::getPrice);
}else{
//按相關(guān)度
wrapper.sortByScore(SortOrder.DESC);
}
return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
}
}
1.5.2 相關(guān)商品推薦
當(dāng)我們查看相關(guān)商品的時候,一般底部會有一些商品推薦,這里簡單來實(shí)現(xiàn)下。
首先來說下需求,可以根據(jù)指定商品的ID來查找相關(guān)商品
這里我們的實(shí)現(xiàn)原理是這樣的:首先根據(jù)ID獲取指定商品信息,然后以指定商品的名稱、品牌和分類來搜索商品,并且要過濾掉當(dāng)前商品,調(diào)整搜索條件中的權(quán)重以獲取最好的匹配度;
/**
* 搜索商品管理Service實(shí)現(xiàn)類
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public PageInfo<EsProduct> recommend(Long id, Integer pageNum, Integer pageSize) {
LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
List<EsProduct> esProductList = productDao.getAllEsProductList(id);
if (esProductList.size() > 0) {
EsProduct esProduct = esProductList.get(0);
String keyword = esProduct.getName();
Long brandId = esProduct.getBrandId();
Long productCategoryId = esProduct.getProductCategoryId();
//用于過濾掉相同的商品
wrapper.ne(EsProduct::getId,id);
//根據(jù)商品標(biāo)題、品牌、分類進(jìn)行搜索
wrapper.and(i -> i.match(EsProduct::getName, keyword, 8f)
.or().match(EsProduct::getSubTitle, keyword, 2f)
.or().match(EsProduct::getKeywords, keyword, 2f)
.or().match(EsProduct::getBrandId, brandId, 5f)
.or().match(EsProduct::getProductCategoryId, productCategoryId, 3f));
return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
}
return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
}
}
1.5.3 聚合搜索商品相關(guān)信息
在搜索商品時,經(jīng)常會有一個篩選界面來幫助我們找到想要的商品,這里我們來簡單實(shí)現(xiàn)下。
首先來說下需求,可以根據(jù)搜索關(guān)鍵字獲取到與關(guān)鍵字匹配商品相關(guān)的分類、品牌以及屬性
這里我們可以使用ES的聚合來實(shí)現(xiàn),搜索出相關(guān)商品,聚合出商品的品牌、商品的分類以及商品的屬性,只要出現(xiàn)次數(shù)最多的前十個即可;
由于Easy-Es目前只用groupBy實(shí)現(xiàn)了簡單的聚合,對于我們這種有嵌套對象的聚合無法支持,所以需要使用RestHighLevelClient來實(shí)現(xiàn),如果對照之前的Spring Data實(shí)現(xiàn)方式的話,可以發(fā)現(xiàn)用法差不多,看樣子Spring Data只是做了簡單的封裝而已。
/**
* 搜索商品管理Service實(shí)現(xiàn)類
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public EsProductRelatedInfo searchRelatedInfo(String keyword) {
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("pms_*");
SearchSourceBuilder builder = new SearchSourceBuilder();
//搜索條件
if (StrUtil.isEmpty(keyword)) {
builder.query(QueryBuilders.matchAllQuery());
} else {
builder.query(QueryBuilders.multiMatchQuery(keyword, "name", "subTitle", "keywords"));
}
//聚合搜索品牌名稱
builder.aggregation(AggregationBuilders.terms("brandNames").field("brandName"));
//集合搜索分類名稱
builder.aggregation(AggregationBuilders.terms("productCategoryNames").field("productCategoryName"));
//聚合搜索商品屬性,去除type=1的屬性
AbstractAggregationBuilder<NestedAggregationBuilder> aggregationBuilder = AggregationBuilders.nested("allAttrValues", "attrValueList")
.subAggregation(AggregationBuilders.filter("productAttrs", QueryBuilders.termQuery("attrValueList.type", 1))
.subAggregation(AggregationBuilders.terms("attrIds")
.field("attrValueList.productAttributeId")
.subAggregation(AggregationBuilders.terms("attrValues")
.field("attrValueList.value"))
.subAggregation(AggregationBuilders.terms("attrNames")
.field("attrValueList.name"))));
builder.aggregation(aggregationBuilder);
searchRequest.source(builder);
try {
SearchResponse searchResponse = esProductMapper.search(searchRequest, RequestOptions.DEFAULT);
return convertProductRelatedInfo(searchResponse);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 將返回結(jié)果轉(zhuǎn)換為對象
*/
private EsProductRelatedInfo convertProductRelatedInfo(SearchResponse response) {
EsProductRelatedInfo productRelatedInfo = new EsProductRelatedInfo();
Map<String, Aggregation> aggregationMap = response.getAggregations().asMap();
//設(shè)置品牌
Aggregation brandNames = aggregationMap.get("brandNames");
List<String> brandNameList = new ArrayList<>();
for(int i = 0; i<((Terms) brandNames).getBuckets().size(); i++){
brandNameList.add(((Terms) brandNames).getBuckets().get(i).getKeyAsString());
}
productRelatedInfo.setBrandNames(brandNameList);
//設(shè)置分類
Aggregation productCategoryNames = aggregationMap.get("productCategoryNames");
List<String> productCategoryNameList = new ArrayList<>();
for(int i=0;i<((Terms) productCategoryNames).getBuckets().size();i++){
productCategoryNameList.add(((Terms) productCategoryNames).getBuckets().get(i).getKeyAsString());
}
productRelatedInfo.setProductCategoryNames(productCategoryNameList);
//設(shè)置參數(shù)
Aggregation productAttrs = aggregationMap.get("allAttrValues");
List<? extends Terms.Bucket> attrIds = ((ParsedStringTerms) ((ParsedFilter) ((ParsedNested) productAttrs).getAggregations().get("productAttrs")).getAggregations().get("attrIds")).getBuckets();
List<EsProductRelatedInfo.ProductAttr> attrList = new ArrayList<>();
for (Terms.Bucket attrId : attrIds) {
EsProductRelatedInfo.ProductAttr attr = new EsProductRelatedInfo.ProductAttr();
attr.setAttrId(Long.parseLong((String) attrId.getKey()));
List<String> attrValueList = new ArrayList<>();
List<? extends Terms.Bucket> attrValues = ((ParsedStringTerms) attrId.getAggregations().get("attrValues")).getBuckets();
List<? extends Terms.Bucket> attrNames = ((ParsedStringTerms) attrId.getAggregations().get("attrNames")).getBuckets();
for (Terms.Bucket attrValue : attrValues) {
attrValueList.add(attrValue.getKeyAsString());
}
attr.setAttrValues(attrValueList);
if(!CollectionUtils.isEmpty(attrNames)){
String attrName = attrNames.get(0).getKeyAsString();
attr.setAttrName(attrName);
}
attrList.add(attr);
}
productRelatedInfo.setProductAttrs(attrList);
return productRelatedInfo;
}
}
使用 Easy-Es 確實(shí)簡單,但是對于復(fù)雜的聚合搜索功能,需要使用原生的 RestHighLevelClient 用法來實(shí)現(xiàn)。使用Easy-Es來操作ES確實(shí)足夠優(yōu)雅,它類似MyBatis-Plus的用法能大大降低我們的學(xué)習(xí)成本