第 4-8 课:Spring Boot 集成 ElasticSearch

ElasticSearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库——无论是开源还是私有。

ElasticSearch 使用 Java 编写的,它的内部使用的是 Lucene 做索引与搜索,它的目的是使全文检索变得简单,通过隐藏 Lucene 的复杂性,取而代之提供了一套简单一致的 RESTful API。

然而,ElasticSearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎,它可以被下面这样准确地形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

ElasticSearch 已经被各大互联网公司验证其抢到的检索能力:

  • Wikipedia 使用 ElasticSearch 提供带有高亮片段的全文搜索,还有 search-as-you-type 和 did-you-mean 的建议;
  • 《卫报》使用 ElasticSearch 将网络社交数据结合到访客日志中,实时的给编辑们提供公众对于新文章的反馈;
  • Stack Overflow 将地理位置查询融入全文检索中去,并且使用 more-like-this 接口去查找相关的问题与答案;
  • GitHub 使用 ElasticSearch 对 1300 亿行代码进行查询。

小故事

关于 ElasticSearch 有一个小故事,在这里也分享给大家:

多年前,Shay Banon 是一位刚结婚不久的失业开发者,由于妻子要去伦敦学习厨师,他便跟着也去了。在他找工作的过程中,为了给妻子构建一个食谱的搜索引擎,他开始构建一个早期版本的 Lucene。

直接基于 Lucene 工作会比较困难,因此 Shay 开始抽象 Lucene 代码以便 Java 程序员可以在应用中添加搜索功能,他发布了他的第一个开源项目,叫做“Compass”。

后来 Shay 找到了一份工作,这份工作处在高性能和内存数据网格的分布式环境中,因此高性能的、实时的、分布式的搜索引擎也是理所当然需要的,然后他决定重写 Compass 库使其成为一个独立的服务叫做 ElasticSearch。

第一个公开版本出现在 2010 年 2 月,在那之后 ElasticSearch 已经成为 GitHub上最受欢迎的项目之一,代码贡献者超过 300 人。一家主营 ElasticSearch 的公司就此成立,他们一边提供商业支持一边开发新功能,不过 ElasticSearch 将永远开源且对所有人可用。

Shay 的妻子依旧等待着她的食谱搜索……

在没有 Spring Boot 之前 Java 程序员使用 ElasticSearch 非常痛苦,需要对接链接资源、进行一些列的封装等操作。 Spring Boot 在 spring-data-elasticsearch 的基础上进行了封装,让 Spring Boot 项目非常方便的去操作 ElasticSearch,如果前面了解过 JPA 技术的话,会发现他的操作语法和 JPA 非常的类似。

值得注意的是,Spring Data ElasticSearch 和 ElasticSearch 是有对应关系的,不同的版本之间不兼容,Spring Boot 2.1 对应的是 Spring Data ElasticSearch 3.1.2 版本。

Spring Data ElasticSearch ElasticSearch
3.1.x 6.2.2
3.0.x 5.5.0
2.1.x 2.4.0
2.0.x 2.2.0
1.3.x 1.5.2

Spring Boot 集成 ElasticSearch

相关配置

在 Pom 中添加 ElasticSearch 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置 ElasticSearch 集群地址:

# 集群名(默认值: elasticsearch,配置文件`cluster.name`: es-mongodb)
spring.data.elasticsearch.cluster-name=es-mongodb
# 集群节点地址列表,用逗号分隔
spring.data.elasticsearch.cluster-nodes=localhost:9300

相关配置

@Document(indexName = "customer", type = "customer", shards = 1, replicas = 0, refreshInterval = "-1")
public class Customer {
    @Id
    private String id;
    private String userName;
    private String address;
    private int age;
    //省略部分 getter/setter
}
  • @Document 注解会对实体中的所有属性建立索引
  • indexName = "customer" 表示创建一个名称为 "customer" 的索引
  • type = "customer" 表示在索引中创建一个名为 "customer" 的 type
  • shards = 1 表示只使用一个分片
  • replicas = 0 表示不使用复制
  • refreshInterval = "-1" 表示禁用索引刷新

创建操作的 repository

public interface CustomerRepository extends ElasticsearchRepository<Customer, String> {
    public List<Customer> findByAddress(String address);
    public Customer findByUserName(String userName);
    public int  deleteByUserName(String userName);
}

我们创建了两个查询和一个删除的方法,从语法可以看出和前面 JPA 的使用方法非常类似,跟踪 ElasticsearchRepository 的代码会发现:

ElasticsearchRepository 继承于 ElasticsearchCrudRepository:

public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {...}

而 ElasticsearchCrudRepository 继承于 PagingAndSortingRepository:

public interface ElasticsearchCrudRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>{...} 

最后 PagingAndSortingRepository 继承于 CrudRepository:

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID>{...}

类图如下:

通过查看源码发现,ElasticsearchRepository 最终使用和 JPA 操作数据库使用的父类是一样的。通过这些也可以发现,Spring Data 项目中的成员在最上层有着统一的接口标准,只是在最终的实现层对不同的数据库进行了差异化封装。

以上简单配置完成之后我们在业务中就可以使用 ElasticSearch 了。

测试 CustomerRepository

创建一个测试类引入 CustomerRepository:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepositoryTest {
    @Autowired
    private CustomerRepository repository;
}

做一个数据插入测试:

@Test
public void saveCustomers() {
    repository.save(new Customer("Alice", "北京",13));
    repository.save(new Customer("Bob", "北京",23));
    repository.save(new Customer("neo", "西安",30));
    repository.save(new Customer("summer", "烟台",22));
}

repository 已经帮我们默认实现了很多的方法,其中就包括 save(); 。

我们对插入的数据做一个查询:

@Test
public void fetchAllCustomers() {
    System.out.println("Customers found with findAll():");
    System.out.println("-------------------------------");
    for (Customer customer : repository.findAll()) {
        System.out.println(customer);
    }
}

输出:

Customers found with findAll():
-------------------------------
Customer{id='aBVS7WYB8U8_i9prF8qm', userName='Alice', address='北京', age=13}
Customer{id='aRVS7WYB8U8_i9prF8rw', userName='Bob', address='北京', age=23}
Customer{id='ahVS7WYB8U8_i9prGMot', userName='neo', address='西安', age=30}
Customer{id='axVS7WYB8U8_i9prGMp2', userName='summer', address='北京市海淀区西直门', age=22}

通过查询可以发现,插入时自动生成了 ID 信息。

对插入的数据进行删除:

@Test
public void fetchAllCustomers() {
    @Test
    public void deleteCustomers() {
        repository.deleteAll();
        repository.deleteByUserName("neo");
    }
}

可以根据属性条件来删除,也可以全部删除。

对属性进行修改:

@Test
public void updateCustomers() {
    Customer customer= repository.findByUserName("summer");
    System.out.println(customer);
    customer.setAddress("北京市海淀区西直门");
    repository.save(customer);
    Customer xcustomer=repository.findByUserName("summer");
    System.out.println(xcustomer);
}

输出:

Customer[id=AWKVYFY4vPQX0UVGnJ7o, userName='summer', address='烟台']
Customer[id=AWKVYFY4vPQX0UVGnJ7o, userName='summer', address='北京市海淀区西直门']

通过输出发现 summer 用户的地址信息已经被变更。

我们可以根据地址信息来查询在北京的顾客信息:

@Test
public void fetchIndividualCustomers() {
    for (Customer customer : repository.findByAddress("北京")) {
        System.out.println(customer);
    }
}

输出:

Customer{id='aBVS7WYB8U8_i9prF8qm', userName='Alice', address='北京', age=13}
Customer{id='aRVS7WYB8U8_i9prF8rw', userName='Bob', address='北京', age=23}
Customer{id='axVS7WYB8U8_i9prGMp2', userName='summer', address='北京市海淀区西直门', age=22}

通过输出可以发现 ElasticSearch 默认给我们进行的就是字段全文(模糊)查询。

通过以上的示例发现使用 Spring Boot 操作 ElasticSearch 非常简单,通过少量代码即可实现我们日常大部分的业务需求。

高级使用

上面演示了在 Spring Boot 项目中对 ElasticSearch 的增、删、改、查操作,通过上面的操作也可以发现操作 ElasticSearch 的语法和 Spring Data JPA 的语法非常类似,下面介绍一些复杂的使用场景。

分页查询

分页查询有两种实现方式,第一种是使用 Spring Data 自带的分页方案,另一种是自行组织查询条件最后封装进行查询。我们先来看第一个方案:

@Test
public void fetchPageCustomers() {
    Sort sort = new Sort(Sort.Direction.DESC, "address.keyword");
    Pageable pageable = PageRequest.of(0, 10, sort);
    Page<Customer> customers=repository.findByAddress("北京", pageable);
    System.out.println("Page customers "+customers.getContent().toString());
}

这段代码的含义是,分页查询地址包含“北京”的客户信息,并且按照地址进行排序,每页显示 10 条。需要注意的是排序是使用的关键字是 address.keyword,而不是 address,属性后面带 .keyword 代表了精确匹配。

QueryBuilder

我们也可以使用 QueryBuilder 来构建分页查询,QueryBuilder 是一个功能强大的多条件查询构建工具,可以使用 QueryBuilder 构建出各种各样的查询条件。

@Test
public void fetchPage2Customers() {
QueryBuilder customerQuery = QueryBuilders.boolQuery()
            .must(QueryBuilders.matchQuery("address", "北京"));
    Page<Customer> page = repository.search(customerQuery, PageRequest.of(0, 10));
    System.out.println("Page customers "+page.getContent().toString());
}

使用 QueryBuilder 可以构建多条件查询,再结合 PageRequest 最后使用 search() 方法完成分页查询。BoolQueryBuilder 有一些关键字和 AND、OR、NOT一一对应:

  • must(QueryBuilders):AND
  • mustNot(QueryBuilders):NOT
  • should::OR

QueryBuilder 是一个强大的多条件构建工具,有以下几种用法。

精确查询

单个匹配:

//不分词查询 参数1: 字段名,参数2:字段查询值,因为不分词,所以汉字只能查询一个字,英语是一个单词
QueryBuilder queryBuilder=QueryBuilders.termQuery("fieldName", "fieldlValue");
//分词查询,采用默认的分词器
QueryBuilder queryBuilder2 = QueryBuilders.matchQuery("fieldName", "fieldlValue");

多个匹配:

//不分词查询,参数1:字段名,参数2:多个字段查询值,因为不分词,因此汉字只能查询一个字,英语是一个单词
QueryBuilder queryBuilder=QueryBuilders.termsQuery("fieldName", "fieldlValue1","fieldlValue2...");
//分词查询,采用默认的分词器
QueryBuilder queryBuilder= QueryBuilders.multiMatchQuery("fieldlValue", "fieldName1", "fieldName2", "fieldName3");
//匹配所有文件,相当于就没有设置查询条件
QueryBuilder queryBuilder=QueryBuilders.matchAllQuery();

模糊查询

模糊查询常见的 5 个方法如下:

//1.常用的字符串查询
QueryBuilders.queryStringQuery("fieldValue").field("fieldName");//左右模糊
//2.常用的用于推荐相似内容的查询
QueryBuilders.moreLikeThisQuery(new String[] {"fieldName"}).addLikeText("pipeidhua");//如果不指定filedName,则默认全部,常用在相似内容的推荐上
//3.前缀查询,如果字段没分词,就匹配整个字段前缀
QueryBuilders.prefixQuery("fieldName","fieldValue");
//4.fuzzy query:分词模糊查询,通过增加 fuzziness 模糊属性来查询,如能够匹配 hotelName 为 tel 前或后加一个字母的文档,fuzziness 的含义是检索的 term 前后增加或减少 n 个单词的匹配查询
QueryBuilders.fuzzyQuery("hotelName", "tel").fuzziness(Fuzziness.ONE);
//5.wildcard query:通配符查询,支持* 任意字符串;?任意一个字符
QueryBuilders.wildcardQuery("fieldName","ctr*");//前面是fieldname,后面是带匹配字符的字符串
QueryBuilders.wildcardQuery("fieldName","c?r?");

范围查询

//闭区间查询
QueryBuilder queryBuilder0 = QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2");
//开区间查询
QueryBuilder queryBuilder1 = QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2").includeUpper(false).includeLower(false);//默认是 true,也就是包含
//大于
QueryBuilder queryBuilder2 = QueryBuilders.rangeQuery("fieldName").gt("fieldValue");
//大于等于
QueryBuilder queryBuilder3 = QueryBuilders.rangeQuery("fieldName").gte("fieldValue");
//小于
QueryBuilder queryBuilder4 = QueryBuilders.rangeQuery("fieldName").lt("fieldValue");
//小于等于
QueryBuilder queryBuilder5 = QueryBuilders.rangeQuery("fieldName").lte("fieldValue");

多条件查询

QueryBuilders.boolQuery()
QueryBuilders.boolQuery().must();//文档必须完全匹配条件,相当于 and
QueryBuilders.boolQuery().mustNot();//文档必须不匹配条件,相当于 not

聚合查询

聚合查询分为五步来实现,我们以统计客户总年龄为例进行演示。

第一步,使用 QueryBuilder 构建查询条件:

QueryBuilder customerQuery = QueryBuilders.boolQuery()
         .must(QueryBuilders.matchQuery("address", "北京"));

第二步,使用 SumAggregationBuilder 指明需要聚合的字段:

SumAggregationBuilder sumBuilder = AggregationBuilders.sum("sumAge").field("age");

第三步,以前两部分的内容为参数构建成 SearchQuery:

SearchQuery searchQuery = new NativeSearchQueryBuilder()
        .withQuery(customerQuery)
        .addAggregation(sumBuilder)
        .build();

第四步,使用 Aggregations 进行查询:

Aggregations aggregations = elasticsearchTemplate.query(searchQuery, new ResultsExtractor<Aggregations>() {
        @Override
        public Aggregations extract(SearchResponse response) {
            return response.getAggregations();
        }
    });

第五步,解析聚合查询结果:

//转换成 map 集合
Map<String, Aggregation> aggregationMap = aggregations.asMap();
//获得对应的聚合函数的聚合子类,该聚合子类也是个 map 集合,里面的 value 就是桶 Bucket,我们要获得 Bucket
InternalSum sumAge = (InternalSum) aggregationMap.get("sumAge");
System.out.println("sum age is "+sumAge.getValue());

以上就是聚合查询的使用方式。

总结

Spring Boot 对 ElasticSearch 的集成延续了 Spring Data 的思想,通过继承对应的 repository 默认帮我们实现了很多常用操作,通过注解也非常的方便设置索引映射在 ElasticSearch 的数据使用。在大规模搜索中使用 Spring Boot 操作 ElasticSearch 是一个最佳的选择。

猜你喜欢

转载自blog.csdn.net/weixin_42047790/article/details/85211647