ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch

1. 简介

1.1.什么是SpringDataElasticsearch

SpringDataElasticsearch(以后简称SDE)是Spring Data项目下的一个子模块。

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

ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch_第1张图片

Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提高开发效率。

包含很多不同数据操作的模块:

ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch_第2张图片

Spring Data Elasticsearch的页面:https://projects.spring.io/spring-data-elasticsearch/

ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch_第3张图片

特征:

  • 支持Spring的基于@Configuration的java配置方式,或者XML配置方式
  • 提供了用于操作ES的便捷工具类**ElasticsearchTemplate**。包括实现文档到POJO之间的自动智能映射。
  • 利用Spring的数据转换服务实现的功能丰富的对象映射
  • 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
  • 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询

2.配置SpringDataElasticsearch

我们在pom文件中,引入SpringDataElasticsearch的启动器:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>

然后,只需要在resources下新建application.yml文件,引入elasticsearch的host和port即可:

spring:
  data:
    elasticsearch:
      cluster-name: elastic
      cluster-nodes: 192.168.150.101:9300,192.168.150.101:9301,192.168.150.101:9302

需要注意的是,SpringDataElasticsearch底层使用的不是Elasticsearch提供的RestHighLevelClient,而是TransportClient,并不采用Http协议通信,而是访问elasticsearch对外开放的tcp端口,我们之前集群配置中,设置的分别是:9301,9302,9300

另外,SpringBoot已经帮我们配置好了各种SDE配置,并且注册了一个ElasticsearchTemplate供我们使用。接下来一起来试试吧。

2.1.索引库操作

2.1.1.创建索引库

我们先创建一个测试类,然后注入ElasticsearchTemplate:

/**
 * @author 虎哥
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringElasticsearchTest {
    
    @Autowired
    private ElasticsearchTemplate esTemplate;
    
}

然后准备一个新的实体类,作为下面与索引库对应的文档:

package cn.itcast.es.pojo;

public class Goods {
    private Long id;
    private String title; //标题
    private String category;// 分类
    private String brand; // 品牌
    private Double price; // 价格
    private String images; // 图片地址

    public Goods() {
    }

    public Goods(Long id, String title, String category, String brand, Double price, String images) {
        this.id = id;
        this.title = title;
        this.category = category;
        this.brand = brand;
        this.price = price;
        this.images = images;
    }
	//  getter和setter略
}

下面是创建索引库的API示例:

@Test
public void testCreateIndex(){
    // 创建索引库,并制定实体类的字节码
    esTemplate.createIndex(Goods.class);
}

发现没有,创建索引库需要指定的信息,比如:索引库名、类型名、分片、副本数量、还有映射信息都没有填写,这是怎么回事呢?

实际上,与我们自定义工具类类似,SDE也是通过实体类上的注解来配置索引库信息的,我们需要在Goods上添加下面的一些注解:

@Document(indexName = "goods", type = "docs", shards = 3, replicas = 1)
public class Goods {
    @Id
    private Long id;
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title; //标题
    @Field(type = FieldType.Keyword)
    private String category;// 分类
    @Field(type = FieldType.Keyword)
    private String brand; // 品牌
    @Field(type = FieldType.Double)
    private Double price; // 价格
    @Field(type = FieldType.Keyword, index = false)
    private String images; // 图片地址
    // 。。。略
}

几个用到的注解:

  • @Document:声明索引库配置
    • indexName:索引库名称
    • type:类型名称,默认是“docs”
    • shards:分片数量,默认5
    • replicas:副本数量,默认1
  • @Id:声明实体类的id
  • @Field:声明字段属性
    • type:字段的数据类型
    • analyzer:指定分词器类型
    • index:是否创建索引

2.1.2.创建映射

刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样:

@Test
public void testMapping(){
    // 创建索引库,并制定实体类的字节码
    esTemplate.putMapping(Goods.class);
}

查看索引库:

ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch_第4张图片

3. 索引数据CRUD

SDE的索引数据CRUD并没有封装在ElasticsearchTemplate中,而是有一个叫做ElasticsearchRepository的接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9aLfTnqQ-1614864431921)(assets/1554378139181.png)]

我们需要自定义接口,继承ElasticsearchRespository:

package cn.itcast.es.repository;

import cn.itcast.es.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

/**
 * @author 虎哥
 */
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}

3.1.创建索引数据

创建索引有单个创建和批量创建之分,先来看单个创建

@Autowired
private GoodsRepository goodsRepository;

@Test
public void addDocument(){
    Goods goods = new Goods(1L, "小米手机9", " 手机",
                            "小米", 3499.00, "http://image.leyou.com/13123.jpg");
    // 添加索引数据
    goodsRepository.save(goods);
}

再来看批量创建:

@Test
public void addDocuments(){
    // 准备文档数据:
    List<Goods> list = new ArrayList<>();
    list.add(new Goods(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));
    list.add(new Goods(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));
    list.add(new Goods(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));
    list.add(new Goods(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));
    list.add(new Goods(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));

    // 添加索引数据
    goodsRepository.saveAll(list);
}

通过elasticsearch-head查看:

ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch_第5张图片

3.2.查询索引数据

默认提供了根据id查询,查询所有两个功能:

根据id查询

@Test
public void testQueryById(){
    Optional<Goods> goodsOptional = goodsRepository.findById(3L);
    System.out.println(goodsOptional.orElse(null));
}

结果:

Item{id=3, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.leyou.com/13123.jpg'}

查询所有:

@Test
public void testQueryAll(){
    Iterable<Goods> list = goodsRepository.findAll();
    list.forEach(System.out::println);
}

结果:

Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.leyou.com/13123.jpg'}
Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=3, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.leyou.com/13123.jpg'}

3.3.自定义方法查询

GoodsRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能:

只要遵循SpringData提供的语法,我们可以任意定义方法声明:

public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {

    /**
     * 根据价格区间查询
     * @param from 开始价格
     * @param to 结束价格
     * @return 符合条件的goods
     */
    List<Goods> findByPriceBetween(Double from, Double to);
}

无需写实现,SDE会自动帮我们实现该方法,我们只需要用即可:

@Test
public void testQueryByPrice(){
    List<Goods> list = goodsRepository.findByPriceBetween(1000d, 4000d);
    list.forEach(System.out::println);
}

结果:

Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.leyou.com/13123.jpg'}
Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}

支持的一些语法示例:

Keyword Sample Elasticsearch Query String
And findByNameAndPrice {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
Or findByNameOrPrice {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
Is findByName {"bool" : {"must" : {"field" : {"name" : "?"}}}}
Not findByNameNot {"bool" : {"must_not" : {"field" : {"name" : "?"}}}}
Between findByPriceBetween {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
LessThanEqual findByPriceLessThan {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
GreaterThanEqual findByPriceGreaterThan {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
Before findByPriceBefore {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
After findByPriceAfter {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
Like findByNameLike {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
StartingWith findByNameStartingWith {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
EndingWith findByNameEndingWith {"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}}
Contains/Containing findByNameContaining {"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}}
In findByNameIn(Collectionnames) {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}
NotIn findByNameNotIn(Collectionnames) {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}
Near findByStoreNear Not Supported Yet !
True findByAvailableTrue {"bool" : {"must" : {"field" : {"available" : true}}}}
False findByAvailableFalse {"bool" : {"must" : {"field" : {"available" : false}}}}
OrderBy findByAvailableTrueOrderByNameDesc {"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

4.原生查询

如果觉得上述接口依然不符合你的需求,SDE也支持原生查询,这个时候还是使用ElasticsearchTemplate

而查询条件的构建是通过一个名为NativeSearchQueryBuilder的类来完成的,不过这个类的底层还是使用的原生API中的QueryBuildersAggregationBuildersHighlightBuilders等工具。

示例:

@Test
public void testNativeQuery(){
    // 原生查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 1.1 source过滤
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
    // 1.2搜索条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
    // 1.3分页及排序条件
    queryBuilder.withPageable(
        PageRequest.of(0, 2,
                       Sort.by(Sort.Direction.ASC, "price")));
    // 1.4高亮显示
    // queryBuilder.withHighlightBuilder(new HighlightBuilder().field("title"));
    // 1.5聚合
    queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));

    // 构建查询条件,并且查询
    AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class);

    // 2、解析结果:

    // 2.1分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List<Goods> list = result.getContent();
    System.out.println("总条数 = " + total);
    System.out.println("总页数 = " + totalPages);
    list.forEach(System.out::println);

    // 2.2.聚合结果
    Aggregations aggregations = result.getAggregations();
    Terms terms = aggregations.get("brandAgg");
    terms.getBuckets().forEach(b -> {
        System.out.println("品牌 = " + b.getKeyAsString());
        System.out.println("count = " + b.getDocCount());
    });
}

上述查询不支持高亮结果,悲剧。

5.自定义结果处理器

要支持高亮,必须自定义结果处理器来实现,结果处理器是一个接口:

ES基础篇-11-Spring提供的elasticsearch组件:Spring Data Elasticsearch_第6张图片

可以看到,处理器中的方法接受3个参数:

  • SearchResponse:搜索的Response,原生查询中就见到过
  • Class clazz:结果的实体类的字节码,本例中的是Goods.class
  • Pageable:分页参数,就是我们定义的PageRequest

返回值一个:AggregatedPage,就是带聚合的分页结果

我们可以实现这个方法,在方法内部对响应处理,带上高亮结果:

package cn.itcast.mapper;

import com.google.gson.Gson;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchResponse;
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 org.springframework.util.CollectionUtils;

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

public class HighlightResultMapper implements SearchResultMapper {
    Gson gson = new Gson();
    
    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
        String scrollId = response.getScrollId();
        long total = response.getHits().getTotalHits();
        float maxScore = response.getHits().getMaxScore();

        List<T> list = new ArrayList<>();
        for (SearchHit hit : response.getHits()) {
            String source = hit.getSourceAsString();
            T t = gson.fromJson(source, clazz);
            // 处理高亮
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (!CollectionUtils.isEmpty(highlightFields)) {
                for (Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) {
                    String fieldName = entry.getKey();
                    String value = StringUtils.join(entry.getValue().getFragments());
                    try {
                        BeanUtils.setProperty(t, fieldName, value);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            list.add(t);
        }
        return new AggregatedPageImpl<>(list, pageable, total, response.getAggregations(), scrollId, maxScore);
    }
}

需要额外3个依赖:

<dependency>
    <groupId>com.google.code.gsongroupId>
    <artifactId>gsonartifactId>
dependency>
<dependency>
    <groupId>commons-beanutilsgroupId>
    <artifactId>commons-beanutilsartifactId>
    <version>1.9.3version>
dependency>
<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-lang3artifactId>
dependency>

然后再次编写带高亮查询:

@Test
public void testNativeQueryAndHighlight() {
    // 原生查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 1.1 source过滤
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
    // 1.2搜索条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
    // 1.3高亮显示
    queryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));

    // 构建查询条件,并且自定义结果处理器
    AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class, new HighlightResultMapper());

    // 2.1分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List<Goods> list = result.getContent();
    System.out.println("总条数 = " + total);
    System.out.println("总页数 = " + totalPages);
    list.forEach(System.out::println);
}

结果:

总条数 = 3
总页数 = 1
Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}

nse.getHits()) {
String source = hit.getSourceAsString();
T t = gson.fromJson(source, clazz);
// 处理高亮
Map highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
for (Map.Entry entry : highlightFields.entrySet()) {
String fieldName = entry.getKey();
String value = StringUtils.join(entry.getValue().getFragments());
try {
BeanUtils.setProperty(t, fieldName, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
list.add(t);
}
return new AggregatedPageImpl<>(list, pageable, total, response.getAggregations(), scrollId, maxScore);
}
}


需要额外3个依赖:

```xml

    com.google.code.gson
    gson


    commons-beanutils
    commons-beanutils
    1.9.3


    org.apache.commons
    commons-lang3

然后再次编写带高亮查询:

@Test
public void testNativeQueryAndHighlight() {
    // 原生查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 1.1 source过滤
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
    // 1.2搜索条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
    // 1.3高亮显示
    queryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));

    // 构建查询条件,并且自定义结果处理器
    AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class, new HighlightResultMapper());

    // 2.1分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List<Goods> list = result.getContent();
    System.out.println("总条数 = " + total);
    System.out.println("总页数 = " + totalPages);
    list.forEach(System.out::println);
}

结果:

总条数 = 3
总页数 = 1
Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.leyou.com/13123.jpg'}
Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.leyou.com/13123.jpg'}
Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}

你可能感兴趣的:(elasticsearch,spring,spring,boot)