Spring Data Elasticsearch 用户指南

翻译自官方文档英文版,有删减。

BioMed Central Development Team version 2.1.3.RELEASE, 2017-04-19

Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

要求Elasticsearch 0.20.2或者以上版本。

一、使用Spring Data Repositories

首先添加依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-elasticsearch</artifactId>
        <version>2.1.3.RELEASE</version>
    </dependency>
</dependencies>

首先我们介绍通用的Spring Data repository,Elasticsearch repository是构建在它的基础之上。它的目标是针对不同的持久化存储显著的减少数据访问层实现的样板代码的数量。

1.1 核心概念

Spring Data repository抽象的中央接口是Repository。它需要一个域类来管理也需要这个域类的id类型作为类型参数。这个接口主要扮演一个标记者接口来捕获需要处理的类型并且帮助你发现拓展的接口。CrudRepository接口为它管理的实体类提供了复杂的CRUD功能。

CrudRepository 接口

public interface CrudRepository<T, ID extends Serializable>
    extends Repository<T, ID> {

    // 保存实体
    <S extends T> S save(S entity); 

    // 根据id查询实体
    T findOne(ID primaryKey);       

    // 返回所有实体类
    Iterable<T> findAll();          

    // 返回所有实体类的数量
    Long count();                   

    // 删除指定的实体类
    void delete(T entity);          

    // 根据指定的id判断某个实体是否存在
    boolean exists(ID primaryKey);  

    // … 更多的方法省略
}

CrudRepository接口还有一个子类PagingAndSortingRepository添加了额外的方法来简化分页访问实体:

public interface PagingAndSortingRepository<T, ID extends Serializable>
  extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

访问User表第二页的数据,每页20条记录,你可以这样写:

PagingAndSortingRepository<User, Long> repository =
Page<User> users = repository.findAll(new PageRequest(1, 20));

作为对查询方法的补充,查询派生出了计数和删除查询。

派生计数查询

public interface UserRepository extends CrudRepository<User, Long> {

  Long countByLastname(String lastname);
}

派生删除查询

public interface UserRepository extends CrudRepository<User, Long> {

  Long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);

}

1.2 查询方法

标准的CRUD功能repositories通常包含了查询。通过Spring Data,声明查询需要4步处理:

1、声明一个接口继承Repository或者它的一个子接口,传入域类和id类型:

interface PersonRepository extends Repository<Person, Long> { … }

2、声明一个查询方法:

interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}

3、配置Spring为这些接口创建代理实例。

JavaConfig:

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config {}

XML 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jpa="http://www.springframework.org/schema/data/jpa"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/data/jpa
     http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

   <jpa:repositories base-package="com.acme.repositories"/>

</beans>

这里使用了jpa命名空间。如果你想使用别的存储技术,假如是mongodb,你只需要将jpa替换为mongodb

注意在JavaConfig配置方式里并没有明确指定注解类默认扫描的包,可以通过@EnableJpaRepositories注解的basePackage属性来指定。

4、获得注入的repository实例并且使用它。

public class SomeClient {

  @Autowired
  private PersonRepository repository;

  public void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
}

下面的章节将详细的解释上面每个步骤。

1.3 定义repository接口

作为第一步你定义一个特定域类repository接口。接口必须继承Repository并且输入域类和其ID的类型。如果你想曝露这个域类的CRUD方法,你可以继承CrudRepository来替代Repository

1.3.1 微调repository接口定义

典型的,你的接口一般会继承Repository, CrudRepository 或者 PagingAndSortingRepository。二者选一,如果你不想继承Spring Data的接口,你可以在你的repository接口增加@RepositoryDefinition注解。继承CrudRepository曝露了一套完整的方法来操作你的实体类。如果你想选择性的曝露一些方法,只需要将你想要曝露的方法从CrudRepository里复制到你的域repository。

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {

  T findOne(ID id);

  T save(T entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

第一步为你的所有的域repositories定义一个通用的基础接口,并且曝露findOne(…)save(…)方法。这样一来UserRepository接口将只能保存用户,根据id查询单个用户和根据电子邮件地址查询用户。

注意,中间的repository接口被@NoRepositoryBean注解标记。确保Spring Data不会为你的基本接口创建实例。

1.3.2 Repositories与多个Spring Data模块

如果只使用一个Spring Data模块让事情变得简单,所有的repository接口在定义的范围内被约束到一个Spring Data模块。有时候应用程序需要使用更多的Spring Data模块。例如,需要一个repository定义区分开两个不同的持久化技术。Spring Data如果发现在类路径下有多个repository工厂那么它会进入严格repository配置模式。对于某个repository定义严格配置需要repository的详细信息或者域类来决定Spring Data模块绑定:

  1. 如果repository定义继承自特定模块的repository,那么他就是这个模块一个有效的候选者。
  2. 如果域类使用了特定模块的注解,那么他就是这个模块一个有效的候选者。Spring Data接收第三方的注解,例如JPA的@Entity或者Spring Data自己提供的注解,例如Spring Data MongoDB/Spring Data Elasticsearch的@Document

使用特定的模块接口定义Repository:

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
  …
}

interface UserRepository extends MyBaseRepository<User, Long> {
  …
}

MyRepositoryUserRepository继承自JpaRepository。它们都是Spring Data JPA有效的候选者。

使用通用接口定义Repository:

interface AmbiguousRepository extends Repository<User, Long> {
 …
}

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
  …
}

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> {
  …
}

AmbiguousRepositoryAmbiguousUserRepository继承自RepositoryCrudRepository。然而使用一个独一无二的Spring Data接口才是完美的,否则无法区分这些repositories属于那个特定的Spring Data模块。

使用域类加注解定义Repository:

interface PersonRepository extends Repository<Person, Long> {
 …
}

@Entity
public class Person {
  …
}

interface UserRepository extends Repository<User, Long> {
 …
}

@Document
public class User {
  …
}

PersonRepository引用了Person并且它使用了JPA注解@Entity那么这个repository显然属于Spring Data JPA。UserRepository使用了User并且被Spring Data MongoDB的@Document注解(我私下里认为这部分文档是从Spring Data MongoDB那复制过来的)。

将上面讲到的这两个注解混合使用:

interface JpaPersonRepository extends Repository<Person, Long> {
 …
}

interface MongoDBPersonRepository extends Repository<Person, Long> {
 …
}

@Entity
@Document
public class Person {
  …
}

这将使Spring Data无法区分出到底该用哪个Spring Data模块,导致不明确的行为。

最后一种区分repositories的方式是限定repository所在的包。例如之前的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jpa="http://www.springframework.org/schema/data/jpa"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/data/jpa
     http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

   <jpa:repositories base-package="com.acme.repositories"/>

</beans>

JavaConfig:

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }

1.4 定义查询方法

从方法的名称中repository代理有两种方式去派生一个指定存储技术的查询。它可以直接从方法的名称派生查询,或者使用一个手动定义的查询。可用的选项依赖于使用的存储技术。但是这里有一个策略决定了什么样的查询被创建。让我们看下这些选项。

1.4.1 查询创建策略

下面的策略对于repository底层解析查询是可用的。你可以配置这些策略,在XML配置中使用query-lookup-strategy属性或者使用Enable${store}Repositories注解的queryLookupStrategy属性在java 配置中。一些策略可能不支持某些特别的数据存储。

  • CREATE 尝试从查询方法的名称构建一个特定存储技术的查询。一般的方法是从查询方法名称中移除一系列约定好的前缀并且解析剩下的字符。

  • USE_DECLARED_QUERY 尝试去找到一个已声明的查询并且在找不到时抛出一个异常。这个查询可以使用一个注解定义或者通过其他方式来声明。查询特定存储技术的文档来获得更多可用的选项。如果repository底层找不到某个方法对应的已声明的查询,启动时将会失败。

  • CREATE_IF_NOT_FOUND(默认)组合了CREATEUSE_DECLARED_QUERY策略。它首先查找是否有已经声明的查询语句,如果没有它将会根据方法名称创建一个查询。

1.4.2 查询创建

查询构建器机制内建于Spring Data repository底层,对所有repository实体构建约束查询是很有用的。这个机制祛除方法名称中的前缀:find…By,read…By,query…By, count…Byget…By然后开始解析剩下的字符。这个逻辑还可以包含一些更多的表达式,例如Distinct表示创建一个没有重复数据的查询。但是第一个By扮演着一个分隔符,指明了真正的条件。最基本的用法是可以定义一些条件在实体类的属性上,然后用And或者Or链接。

一些例子:

public interface PersonRepository extends Repository<User, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // 启用distinct查询
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // 忽略某个字段大小写
  List<Person> findByLastnameIgnoreCase(String lastname);
  
  // 忽略多个字段大小写
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // 启用排序
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

真正的解析结果依赖于你使用的持久化存储技术。如果是JPA那么就会转成相应的SQL语句(译者注)。但是这里有一些通用的事情需要注意。

  • 表达式通常是实体类的属性加上操作符。你可以用And或者Or链接属性。类似的还有Between, LessThan, GreaterThanLike

  • IgnoreCase这个并不是所有持久化存储技术都支持,所以请查询特定的用户指南文档。

1.4.3 属性表达式

属性表达式只能引用实体类中的属性。假设一个Person有一个Address对象,Address里有一个ZipCode对象。如果想根据ZipCode查询人那么可能会这样写:

List<Person> findByAddressZipCode(ZipCode zipCode);

解析算法会开始解释AddressZipCode整体作为一个属性。如果没找到会根据驼峰写法从继续进行分割为AddressZipCode。如果还是没有找到怎会继续进行分割,这次结果是AddressZipCode。这显然不是理想情况。

为了解决模棱两可的属性名称,你可以使用_在你的方法名称中手动的进行分割,那么上面的语句将会是:

List<Person> findByAddress_ZipCode(ZipCode zipCode);

1.4.4 特殊参数处理

上面的查询方法中,仅仅定义了方法的参数来指定查询的参数。除此之外还会识别PageableSort这两个参数来动态的进行分页与排序。下面是一些例子:

Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Pageable pageable);

第一个方法允许你传递一个org.springframework.data.domain.Pageable实例到查询方法来动态的进行分页。一个Page知道总数量和一共多少页可用。它之所以知道这些是因为底层框架触发了一个技计数查询来计算全部的数量。根据使用的数据存储技术不同这可能消耗很大时间,可以使用Slice来替代它。Slice只知道是否存在下一个Slice可用,这将会非常适合查询超大结果集。

排序操作也可以通过Pageable接口来处理。如果你仅仅需要排序那么可以使用org.springframework.data.domain.Sort实例作为方法参数。如你所见,查询方法也可以仅仅返回一个List。这将约束查询方法只查询给定范围的数据。

1.4.5 限制查询结果数量

查询的结果数量可以通过关键字first或者top来限制。一个可选的数字可以追加到first或者top后面来指定查询结果确切的数量。如果这个数字被省略了,将会默认指定1为返回结果的数量。下面是一些例子:

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

1.4.6 将查询结果包装成Stream

通过使用Java 8 Stream<T>作为查询方法的结果,可以递增地对结果处理。下面是一些例子:

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

注意Stream使用完后要调用close()方法关闭它。不是所有的Spring Data 模块都支持Stream<T>作为返回结果。

1.5 创建repository接口

本章讲创建接口和repository接口bean的定义。

1.5.1 XML配置

每一个Spring Data模块都包含一个repositories元素,允许你简单的定义Spring将要自动扫描的基础包。

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

  <repositories base-package="com.acme.repositories" />

</beans:beans>

对于每个发现的接口,底层框架注册特定持久化技术的FactoryBean来创建合适的代理处理查询方法的调用。例如UserRepository将会被注册为一个名为userRepository的bean。base-package属性支持通配符。

1.5.2 JavaConfig

使用@Enable${store}Repositories注解可以触发指定数据存储技术的repository。

一个简单的配置:

@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  public EntityManagerFactory entityManagerFactory() {
    // …
  }
}

这个例子表明我们使用的Spring Data JPA。

1.6 自定义Spring Data repositories实现

1.6.1 为所有repository接口添加自定义行为

为了添加一个适用于所有repository的自定义行为,首先添加一个中介接口来声明通用的行为。

@NoRepositoryBean
public interface MyRepository<T, ID extends Serializable>
  extends PagingAndSortingRepository<T, ID> {

  void sharedCustomMethod(ID id);
}

现在你自己的repository接口将会继承这个中介接口而不是继承Repository接口。然后创建这个中介接口的实现类继承特定持久化技术的repository父类。这个类将会扮演一个repository代理的自定义父类。

public class MyRepositoryImpl<T, ID extends Serializable>
  extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID> {

  private final EntityManager entityManager;

  public MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  public void sharedCustomMethod(ID id) {
    // implementation goes here
  }
}

Spring <repositories />命名空间默认的行为是为base-package下所有接口提供一个实现类。这意味着MyRepository接口的实现将会由Spring创建。这不是我们想要的。为了避免这种情况的发生你可以增加一个@NoRepositoryBean注解或者将它移出base-package

最后一步是让Spring Data底层框架认识这个自定义repository父类。下面是一个JavaConfig的例子:

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

XML配置的例子:

<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />

二、Elasticsearch Repositories

终于要开始介绍Elasticsearch repository实现的详细内容了。

2.1 简介

2.1.1 命名空间

Spring Data Elasticsearch模块包含一个自定义命名空间允许用户定义repository bean和ElasticsearchServer实例。

使用命名空间设置Elasticsearch repositories的例子:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/data/elasticsearch
http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch-1.0.xsd">

    <elasticsearch:repositories base-package="com.acme.repositories" />

</beans>

使用Transport Client或者Node Client元素类注册一个Elasticsearch Server实例。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/data/elasticsearch
http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch-1.0.xsd">

    <elasticsearch:transport-client id="client" cluster-nodes="localhost:9300,someip:9300" />

</beans>

2.1.2 基于注解的配置

直接上例子:

@Configuration
@EnableElasticsearchRepositories(basePackages = "com/acme/repositories")
static class Config {

    @Bean
    public ElasticsearchOperations elasticsearchTemplate() {
        return new ElasticsearchTemplate(nodeBuilder().local(true).node().client());
    }
}

2.2 查询方法

2.2.1 查询创建策略

Elasticsearch module支持所有的基本查询构建特性,比如从方法名称或者手动声明一个查询条件。

2.2.2 查询创建

一般的Elasticsearch生成查询机制与1.2 查询方法章节描述的一样。下面是一些小例子:

根据方法名称创建查询

public interface BookRepository extends Repository<Book, String>
{
    List<Book> findByNameAndPrice(String name, Integer price);
}

它将会被转换成Elasticsearch json查询:

{ "bool" :
    { "must" :
        [
            { "field" : {"name" : "?"} },
            { "field" : {"price" : "?"} }
        ]
    }
}

下面的表格是Elasticsearch module根据方法名创建查询的关键字。

关键字 示例 Elasticsearch json查询
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(Collection<String>names) {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}
NotIn findByNameNotIn(Collection<String>names) {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}
Near findByStoreNear 暂不支持
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}}}}

2.2.3 使用@Query注解

使用@Query注解查询的例子:

public interface BookRepository extends ElasticsearchRepository<Book, String> {
    @Query("{\"bool\" : {\"must\" : {\"field\" : {\"name\" : \"?0\"}}}}")
    Page<Book> findByName(String name,Pageable pageable);
}

三、多样的Elasticsearch操作支持

3.1 条件构建器

条件构建器提高了查询的效率。

private ElasticsearchTemplate elasticsearchTemplate;

SearchQuery searchQuery = new NativeSearchQueryBuilder()
    .withQuery(matchAllQuery())
    .withFilter(boolFilter().must(termFilter("id", documentId)))
    .build();

Page<SampleEntity> sampleEntities =
    elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class);

3.2 为大结果集准备的Scan和Scroll

Elasticsearch针对超大结果集提供了scan 和 scroll特性。

ElasticsearchTemplate有scan 和 scroll方法,请看下面的例子:

SearchQuery searchQuery = new NativeSearchQueryBuilder()
    .withQuery(matchAllQuery())
    .withIndices("test-index")
    .withTypes("test-type")
    .withPageable(new PageRequest(0,1))
    .build();
String scrollId = elasticsearchTemplate.scan(searchQuery,1000,false);
List<SampleEntity> sampleEntities = new ArrayList<SampleEntity>();
boolean hasRecords = true;
while (hasRecords){
    Page<SampleEntity> page = elasticsearchTemplate.scroll(scrollId, 5000L , new ResultsMapper<SampleEntity>()
    {
        @Override
        public Page<SampleEntity> mapResults(SearchResponse response) {
            List<SampleEntity> chunk = new ArrayList<SampleEntity>();
            for(SearchHit searchHit : response.getHits()){
                if(response.getHits().getHits().length <= 0) {
                    return null;
                }
                SampleEntity user = new SampleEntity();
                user.setId(searchHit.getId());
                user.setMessage((String)searchHit.getSource().get("message"));
                chunk.add(user);
            }
            return new PageImpl<SampleEntity>(chunk);
        }
    });
    if(page != null) {
        sampleEntities.addAll(page.getContent());
        hasRecords = page.hasNextPage();
    }
    else{
        hasRecords = false;
    }
    }
}

到此告一段落。

推荐阅读更多精彩内容