Easy-Es操作Elasticsearch

1 Easy-Es

使用過Spring Data操作ES的小伙伴應(yīng)該有所了解,它只能實(shí)現(xiàn)一些非?;镜臄?shù)據(jù)管理工作,一旦遇到稍微復(fù)雜點(diǎn)的查詢,基本都要依賴ES官方提供的RestHighLevelClientSpring 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ā)、提高效率而生。EEMybatis-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語法對比

首先我們來對MySQLEasy-EsRestHighLevelClient的語法做過對比,來快速學(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.ymlEasy-Es進(jìn)行配置。

easy-es:
  # 是否開啟EE自動配置
  enable: true
  # ES連接地址+端口
  address: localhost:9200
  # 關(guān)閉自帶banner
  banner: false

添加Easy-EsJava配置,使用@EsMapperScan配置好Easy-EsMapper接口和文檔對象路徑,如果使用了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ù)
  • @IndexIdES主鍵注解,type 指定注解類型,CUSTOMIZE 表示自定義
  • @IndexFieldES字段注解,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語句

image.png

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

image.png

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í)成本

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容