SpringData Elasticsearch

SpringData介绍

Spring Data是一个用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持map-reduce框架和云计算数据服务。 Spring Data可以极大的简化JPA的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分页、排序等一些常用的功能。

Spring Data的官网:http://projects.spring.io/spring-data/

SpringData ES介绍

Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操作elasticSearch的客户端API 进行封装 。Spring Data为Elasticsearch项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储库数据访问层。 官方网站:http://projects.spring.io/spring-data-elasticsearch/

微服搭建
依赖pom.xml
<!-- SpringDataES依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置application.yml
spring:
  application:
    name: search

  data:
    elasticsearch:
      cluster-name: my-application  # es配置中的集群名字
      cluster-nodes: 192.168.169.140:9300

cluster-name:Elasticsearch的集群节点名称,这里需要和Elasticsearch集群节点名称保持一致
cluster-nodes:Elasticsearch节点通信地址,端口是9300

启动类
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class SearchApplication {

    public static void main(String[] args) {
        /**
        * Springboot整合Elasticsearch 在项目启动前设置一下的属性,防止报错
        * 解决netty冲突后初始化client时还会抛出异常
        * availableProcessors is already set to [12], rejecting [12]
        ***/
        System.setProperty("es.set.netty.runtime.available.processors", "false");
        SpringApplication.run(SearchApplication.class,args);
    }
}
model层

映射索引库

都是elasticsearch包

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.io.Serializable;
import java.util.Date;
import java.util.Map;

/**
 * Title:映射索引库的javabean
 * Description:
 * @author WZQ
 * @version 1.0.0
 * @date 2020/3/5
 */
@Document(indexName = "skuinfo",type = "docs") // 索引库名
@Data
@NoArgsConstructor
public class SkuInfo implements Serializable {

    //商品id,同时也是商品编号
    @Id
    private Long id;

    //SKU名称
    /**
     * Field注解属性
     * type = FieldType.Text:类型,Text文本,适用分词
     * index = true:添加数据的时候,是否分词
     * analyzer = "ik_smart":创建索引分词器
     * store = false:是否存储
     * searchAnalyzer = "ik_smart":搜索的时候是否使用分词
     */
    @Field(type = FieldType.Text, analyzer = "ik_smart")
    private String name;

    //商品价格,单位为:元
    @Field(type = FieldType.Double)
    private Long price;

    //库存数量
    private Integer num;

    //商品图片
    private String image;

    //商品状态,1-正常,2-下架,3-删除
    private String status;

    //创建时间
    private Date createTime;

    //更新时间
    private Date updateTime;

    //是否默认
    private String isDefault;

    //SPUID
    private Long spuId;

    //类目ID
    private Long categoryId;

    //类目名称
    /**
     * FieldType.Keyword:不分词
     */
    @Field(type = FieldType.Keyword)
    private String categoryName;

    //品牌名称
    @Field(type = FieldType.Keyword)
    private String brandName;

    //规格
    private String spec;

    //规格参数
    private Map<String,Object> specMap; // 数据不同地方,可以用map存数据

}
数据导入

查询数据库数据或是独立出来search微服务,则可以利用fegin调用其他微服务的方法查询出来数据,转化为索引映射javaBean,再把数据导入ES,数据太多的话,建议分页查询数据库数据

dao层

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

// 给索引映射javabean:skuinfo、该javabean的主键类型:long。自定义
@Repository
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo, Long> {

}

service层

public interface SkuService {

    /***
     * 导入SKU数据到ES
     */
    void importSku();
    
}
@Service
public class SkuServiceImpl implements SkuService {

    // 这里例子是利用fegin,也可以该微服务对数据库查询,根据业务
    @Resource
    private SkuFeign skuFeign;

    @Resource
    private SkuEsMapper skuEsMapper;

    /**
     * 导入sku数据到es
     */
    @Override
    public void importSku(){
        
        //根据业务拿到数据库数据
        
        //feign调用goods微服务
        ResponseResult<List<TbSku>> skuListResult = skuFeign.findByStatus("1");
        //将json数据转成对应的索引映射类,fastjson
        //字段名字匹配得上就可以转
        List<SkuInfo> skuInfos=  JSON.parseArray(JSON.toJSONString(skuListResult.getData()), SkuInfo.class);
        for(SkuInfo skuInfo:skuInfos){
            Map<String, Object> specMap= JSON.parseObject(skuInfo.getSpec()) ;
            skuInfo.setSpecMap(specMap);
        }
        // es通用mapper方法批量存入到es索引库中
        skuEsMapper.saveAll(skuInfos);
    }
}

controller层:

@RestController
@RequestMapping(value = "/search")
@CrossOrigin
public class SkuController {

    @Resource
    private SkuService skuService;

    /**
     * 导入数据
     * @return
     */
    @GetMapping("/import")
    public ResponseResult<Void> importData(){
        skuService.importSku();
        return new ResponseResult<Void>(true, StatusCode.OK,"导入数据到索引库中成功!");
    }
}

启动类添加:

@EnableElasticsearchRepositories(basePackages = "search.dao")// dao路径

kibana查看数据或者elasticsearch-head

安装如下:

docker pull mobz/elasticsearch-head:5

docker run -di --name elasticsearch-head -p 9100:9100 mobz/elasticsearch-head:5

http://192.168.169.140:9100/

在这里插入图片描述

数据搜索

对应DSL方法查询

must多条件

match标准单个关键词

term过滤

prefix前缀

等等

service层实现:

@Service
public class SkuServiceImpl implements SkuService {

    // ElasticsearchTemplate可以实现对索引库的增删改查
    @Resource
    private ElasticsearchTemplate esTemplate;

    @Override
    public Map<String, Object> search(Map<String, String> searchMap) {

        //创建查询对象 构建对象
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();

        //获取关键字的值
        //这里可以不止关键字,多条件也行,判断加条件进去
        String keywords = null;
        if (searchMap != null && searchMap.size() > 0){
            // 不为空
            if (!StringUtils.isEmpty(searchMap.get("keywords"))){
                keywords = searchMap.get("keywords");
                //条件一:设置关键词查询条件
                //match、term等对应dsl语句
                //域名,值
                nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
            }
        }

        //条件二:设置分组条件addAggregation-->group by  商品分类
        //categoryName是域名,代码自定义skuCategorygroup组名
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));

        //构建查询对象
        NativeSearchQuery query = nativeSearchQueryBuilder.build();

        //执行查询
        //带上索引映射类
        AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);

        //获取分组结果,对应分组名
        StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
        List<String> categoryList = getStringsCategoryList(stringTermsCategory);
        
        //获取每页显示个数和当前页码,一起返回
        Pageable pageable = query.getPageable();
        int pageSize = pageable.getPageSize();
        int pageNumber = pageable.getPageNumber()+1; //从0开始

        //返回结果
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("categoryList", categoryList); // 分组结果,几种类型
        resultMap.put("rows", skuPage.getContent()); // 数据
        resultMap.put("total", skuPage.getTotalElements()); // 总记录数
        resultMap.put("totalPages", skuPage.getTotalPages()); // 总页数
        resultMap.put("pageSize", pageSize); // 每页显示个数
        resultMap.put("pageNumber", pageNumber); // 当前页码

        return resultMap;
    }

    /**
     * 获取分类列表数据
     * @param stringTerms
     * @return
     */
    private List<String> getStringsCategoryList(StringTerms stringTerms) {
        List<String> categoryList = new ArrayList<>();
        if (stringTerms != null) {
            for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
                String keyAsString = bucket.getKeyAsString();//分组的值
                categoryList.add(keyAsString);
            }
        }
        return categoryList;
    }
}

controller层:

    /**
     * 搜索
     * @param searchMap
     * @return
     */
    @PostMapping
    public Map<String, Object> search(@RequestBody(required = false) Map<String, String> searchMap){
        return skuService.search(searchMap);
    }

在这里插入图片描述

在这里插入图片描述

上图是规格的索引存储格式,真实数据在spechMap.规格名字.keyword中,所以找数据也是按照如下格式去找:

spechMap.key值.keyword

域是map类型的数据,这样可以拿到key对应的value值

拿到map数据要:spechMap.keyword

搜索条件

重点

搜索模板
@Service
public class ServiceImpl implements Service {

    // ElasticsearchTemplate可以实现对索引库的增删改查
    @Resource
    private ElasticsearchTemplate esTemplate;

    // 搜索条件和返回数据类型根据业务定义
    @Override
    public Map<String, Object> search(Map<String, String> searchMap) {

        //创建查询对象 构建对象
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();

        //判断前端条件是否为空
        //多条件多个,组合条件得用bool
        //带上条件,看业务
        nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery(域名字符串, 前端searchMap中的关键词));

        //设置分组查询addAggregation-->SQL对应group by
        //这里的分组是拿查询出来的数据统计该域的种类并返回
        //可多个,可抽取分组查询方法,多个都是多个addAggregation,换域名组名
        //域名字符串看kibana,可以是值中带值,比如值是Map类型:spec.mapkey
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串).field(域名字符串).size(100)); // 默认10条数据,size添加分组的数据数
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串).field(域名字符串).size(100)); // 可多个,多个的话最好抽取方法出来

        //构建查询对象
        NativeSearchQuery query = nativeSearchQueryBuilder.build();

        //执行查询
        //带上索引映射类
        AggregatedPage<索引映射类> page = esTemplate.queryForPage(query, 索引映射类.class);

        //获取分组结果,对应分组名
        StringTerms stringTermsCategory = (StringTerms) page.getAggregation(组名字符串);
        List<String> list = getStringsList(stringTermsCategory);

        //返回结果
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("List", list); // 分组结果,几种类型
        resultMap.put("rows", page.getContent()); // 数据
        resultMap.put("total", page.getTotalElements()); // 总记录数
        resultMap.put("totalPages", page.getTotalPages()); // 总页数

        return resultMap;
    }
    
    /**
     * 获取分组后的列表数据
     * 一般String类型
     * @param stringTerms
     * @return
     */
    private List<String> getStringsList(StringTerms stringTerms) {
        List<String> list = new ArrayList<>();
        if (stringTerms != null) {
            for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
                String keyAsString = bucket.getKeyAsString();//分组的值
                list.add(keyAsString);
            }
        }
        return list;
    }
}

对应dsl语句,field指域名

标准match搜索

可分词搜索,前提是索引映射类上对应的变量域名有searchAnalyzer = “ik_smart”,支持搜索使用分词器,否则只是普通的模糊搜索like。

nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery(域名字符串, 关键词));
过滤term搜索

不分词精确匹配

nativeSearchQueryBuilder.withQuery(QueryBuilders.termQuery(域名字符串, 关键词));
组合bool搜索

bool组合过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:

  • must : 多个查询条件的完全匹配,相当于 and。
  • must_not : 多个查询条件的相反匹配,相当于 not。
  • should : 至少有一个查询条件匹配, 相当于 or。
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

    // 中文分词,如果多个中文分词关键词得用must
    if (!StringUtils.isEmpty(主关键词)) {
        boolQueryBuilder.must(QueryBuilders.matchQuery("域名", 主关键词));
    }

    // 多关键词可用map,或者dto
    if (!StringUtils.isEmpty(关键词1)) {
        boolQueryBuilder.must(QueryBuilders.termQuery("域名1", 关键词1));
    }

    if (!StringUtils.isEmpty(关键词2)) {
        boolQueryBuilder.must(QueryBuilders.termQuery("域名2", 关键词2));
    }

    // ...rangeQuery

    // 构建过滤查询
    // withQuery只能一个
    nativeSearchQueryBuilder.withQuery(boolQueryBuilder);

另一个方式:

PS说明: 以上,我们建议使用filter ,它的搜索效率要优于must.可以参考官方文档说明:

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html

如果多个中文分词关键词的话得用must(matchQuery),一个一下就用filter。

    //设置主关键字查询,中文分词
    nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("域名", 主关键词));

    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

    if (!StringUtils.isEmpty(关键词1)) {
        boolQueryBuilder.filter(QueryBuilders.termQuery("域名1", 关键词1));
    }

    if (!StringUtils.isEmpty(关键词2)) {
        boolQueryBuilder.filter(QueryBuilders.termQuery("域名2", 关键词2));
    }

    //...rangeQuery

    //构建过滤查询
    nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
区间range搜索

最好结合bool组合搜索,一般是数字大小,范围

  • gt:>
  • gte:>=
  • lt:<
  • lte:<=
   // 区间1    
   boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").gte(前端int类型参数小);
   boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").lte(前端int类型参数大);
   
   // 区间2
   boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").from(前端int类型参数小, true).to(前端int类型参数大, true));
                           
   // 单个以上,大于或小于选一个
   boolQueryBuilder.filter(QueryBuilders.rangeQuery("数字域名").lte(前端int类型参数);      
分组搜索

group by,可额外添加到搜索,拿到查询数据后该域名的种类。

nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串1).field("域名1").size(100)); 
   nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(组名字符串2).field("域名2").size(100)); 

   //...

List<String> list1 = getStringsList(page, 组名字符串1);
List<String> list2 = getStringsList(page, 组名字符串2);
 
    /**
     * 获取分组后的列表数据
     * 一般是String类型
     * page是查询后的返回数据
     */
    private List<String> getStringsList(AggregatedPage<索引映射类> page, String 组名字符串) {
        StringTerms stringTermsCategory = (StringTerms) page.getAggregation(组名字符串);
        List<String> list = new ArrayList<>();
        if (stringTerms != null) {
            for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
                String keyAsString = bucket.getKeyAsString(); //分组的值
                list.add(keyAsString);
            }
        }
        return list;
    }

这样会多次请求es,效率不高,改进如下,分组的合并一起请求

//多个域一起分组,以后多域用这个方法
//修改了getAggregations().get(组名字符串)

/**
     * 获取分组后的列表数据
     * 一般是String类型
     * page是查询后的返回数据
     */
    private List<String> getStringsList(AggregatedPage<索引映射类> page, String 组名字符串) {
        StringTerms stringTermsCategory = (StringTerms) page.getAggregations().get(组名字符串);
        List<String> list = new ArrayList<>();
        if (stringTerms != null) {
            for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
                String keyAsString = bucket.getKeyAsString(); //分组的值
                list.add(keyAsString);
            }
        }
        return list;
    }
分页查询
        //写在所有条件最后
        //构建分页查询
        int pageNum = 1; // 默认第一页
        if (!StringUtils.isEmpty(searchMap.get("pageNum"))) {
            try {
                // 前端传过来的当前页
                pageNum = Integer.parseInt(searchMap.get("pageNum"));
            } catch (NumberFormatException e) {
                e.printStackTrace();
                pageNum=1;
            }
        }
        int pageSize = 8; // 一页多少条数据,一般后台固定
        nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));

        //构建查询对象
        NativeSearchQuery query = nativeSearchQueryBuilder.build();
排序查询
//构建排序查询
String sortRule = searchMap.get("sortRule"); //排序的规则,升序还是降序DESC、ASC
String sortField = searchMap.get("sortField"); //前端传过来的指定排序域名
if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
    nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
}

//也可以这样
nativeSearchQueryBuilder.withSort(new FieldSortBuilder(sortField).order(SortOrder.valueOf(sortRule)));

//构建查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
高亮设置

某些数据需要高亮显示,比如含有关键词的语句中关键词变色

SkuInfo是对应的索引映射类

数据操作实现类

import com.alibaba.fastjson.JSON;
import com.changgou.model.es.SkuInfo;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Title:获取es搜索后的数据
 * Description:数据进行操作返回给前端
 *             SkuInfo是对应的索引映射类
 * @author WZQ
 * @version 1.0.0
 * @date 2020/3/6
 */
public class SearchResultMapperImpl implements SearchResultMapper {

    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {

        //存放修改后的数据
        List<T> content = new ArrayList<>();

        //如果没有结果返回为空
        if (response.getHits() == null || response.getHits().getTotalHits() <= 0) {
            return new AggregatedPageImpl<T>(content);
        }
        // 遍历数据
        for (SearchHit searchHit : response.getHits()) {
            String sourceAsString = searchHit.getSourceAsString();
            // 默认拿到的是非高亮数据,指定索引映射类
            SkuInfo skuInfo = JSON.parseObject(sourceAsString, SkuInfo.class);

            Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
            //跟条件设置的域名一致
            HighlightField highlightField = highlightFields.get("域名");

            //有高亮则读取高亮的值
            if (highlightField != null && highlightField.getFragments() != null) {
                StringBuilder stringBuilder = new StringBuilder();
                for (Text text : highlightField.getFragments()) {
                    stringBuilder.append(text.toString());
                }
                // 数据中指定域替换成高亮数据,默认非高亮的
                skuInfo.setName(stringBuilder.toString());
            }
            content.add((T) skuInfo);
        }
        /**
         * 1.携带高亮数据的内容
         * 2.分页对象信息
         * 3.搜索数据的总数
         */
        return new AggregatedPageImpl<T>(content, pageable, response.getHits().getTotalHits(), response.getAggregations(), response.getScrollId());
    }
}

serive中修改:

//高亮配置
HighlightBuilder.Field field = new HighlightBuilder.Field("域名");
//样式前缀
field.preTags("<em style=\"color:red;\">");
//后缀
field.postTags("</em>");
//碎片长度 关键词数据的长度,不超过设置的长度给到前端
field.fragmentSize(100);
//添加到条件
nativeSearchQueryBuilder.withHighlightFields(field); // 参数是可变数组,可多个传进去

//构查询对象
NativeSearchQuery query = nativeSearchQueryBuilder.build();

//记得修改成这个方法,带上实现类
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class, new SearchResultMapperImpl());
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐