Spring Boot 整合 ElasticSearch
ElasticSearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene 基础之上。(Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库。)
ElasticSearch 使用 Java 编写的,它的内部使用的是 Lucene 做索引与搜索,它的目的是使全文检索变得简单(因为 Lucene 只是个库),通过隐藏 Lucene 的复杂性,取而代之提供了一套简单一致的 RESTful API 。
然而,ElasticSearch 不仅仅是 Lucene 的一层『壳子』,并且也不仅仅只是一个全文搜索引擎,更多的时候它被当作 NoSQL 数据库来使用。
#1. 版本问题
Spring Data ElasticSearch 和 ElasticSearch 是有对应关系的,不同的版本之间不兼容。
具体版本对应关键见最后。
简而言之:
- 
spring-boot 2.1.x ,仅支持 ES6 ;
 - 
pring-boot 2.2.x ,同时支持 ES6 和 ES7 。
 
#2. 回顾 Elasticsearch 搜索功能
Elastic Search 搜索没命中的原因无非关系到三点:
- 
中文分词器
 - 
document 的字段类型
 - 
执行的是 term 查询,还是 match 查询
 
#中文分词器的影响
text 类型的字符串数据存储进 Elastic Search 时,会被分词器分词。如果没有指定分词器,那么 Elastic Search 使用的就是默认的 Standard 分词器。
以 张三丰 为例,『默认的分词器』会将其分为 张、三 和 丰(注意,甚至都没有 张三丰!),此时,你以 张三丰 作为条件,进行 term 查询,Elastic Search 会告诉你:没有。因为 张、三 和 丰 都不是 张三丰 。
#document 的字段类型
Elastic Search 中的字符串类型(以前有 String,现在细)分为 text 和 keyword 两种 。
text 类型的字符串数据会被分词器分词,细化为多个 term;而 keyword 类型的字符串数据,则不会被分词器分词,或者说整体就是有且仅有的一个 term 。当然,至于 string 被分词器分为 term 的结果科不科学?给不给力?合不合理?那就是另一码事了,见上文。
以 张三丰 为例(假设使用了 IK 中文分词器),以 text 类型存储时,会被分词器分为 张三丰、张三、三丰、张、三、丰 这好几个 term,也就是说,但凡以上述的哪个 term 作为关键字(例如,张三)进行搜索,都能定位到这条数据。
但是 张三丰 以 keyword 类型存储时,分词器不参与其中工作,张三丰 就是一个整体,张三、三丰、张、三、丰 跟它半毛钱关系都没有。
#term 查询和 match 查询
进行查询时,你提供的查询的关键字/线索,也有可能被分词器进行分词,这取决于你进行的是 term 查询,还是 match 查询。
你进行 term 查询时,你提供的线索,例如 张三丰,不会被分词器分词,也即是说,Elastic Search 就是实实在在地拿 张三丰 作为条件,去数据库中查找数据。
简单来说,你提供的一个查询线索就是一个查询线索。
你进行 match 查询时,你提供的线索,还是 张三丰,分词器会对其分词,也就是说,你以为你是在以 张三丰 为线索进行查询?不,实际上 Elastic Search 是分别以 张三丰、张三、三丰、张、三、丰 这几个,去数据库中查了好几次。
简单来说,你以为你提供的是一个查询线索,实际上是好几个。这样的查询命中自然就高了(至于命中的是不是你想要的,那就是另一个问题了)。
想知道中文字符串被分词器解析出几个词项,可以通过下面命令简介知道:
POST _analyze { "analyzer" : "ik_max_word 或 ik_smart", "text" : "内容" } # 查看某个字段的分词结果 GET 索引名/_doc/指定ID/_termvectors?fields=字段名Copied!
#3. Spring Boot 集成 ElasticSearch

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency><!-- 因为 spring-data-es 要发出 HTTP 请求,所以还需要 spring web 的包 -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 
老版本配置方式(已被废弃,不再推荐使用)
略。
 - 
新版本配置方式(推荐使用)
新的配置方式使用的是 High Level REST Client 的方式来替代之前的 Transport Client 方式,使用的是 HTTP 请求,和 Kibana 一样使用的是 Elasticsearch 的 9200 端口。
这种配置方案中,你使用的不是配置文件,而是自定义配置类:
/** * 你也可以不继承 AbstractElasticsearchConfiguration 类,而将 ESConfig 写成一般的配置类的型式。 * 不过继承 AbstractElasticsearchConfiguration 好处在于,它已经帮我们配置好了 elasticsearchTemplate 直接使用。 * * 在配置文件中配置 spring.elasticsearch.rest.uris=http://localhost:9200 */ @Configuration public class ESConfig extends AbstractElasticsearchConfiguration { @Override public RestHighLevelClient elasticsearchClient() { ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("localhost:9200") .build(); return RestClients.create(clientConfiguration).rest(); } }Copied!另外,由于 High Level REST Client 的方式是向 Elasticsearch 发出 HTTP 请求,因此无论你用不用得到 spring webmvc,spring-data-elasticsearch 它是一定要用的,所以,这就是为什么你的 pom 依赖中还要加上 spring-webmvc 的依赖。
 
Elasticsearch 中的 PO 类:
@Document(indexName = "books", shards = 1, replicas = 0)
public class Book {
    @Id
    @Field(name="id", type = FieldType.Keyword)
    private String id;
    @Field(name="title", type = FieldType.Text, analyzer = "ik_smart")
    private String title;
    @Field(name="language", type = FieldType.Keyword)
    private String language;
    @Field(name="author", type = FieldType.Keyword)
    private String author;
    @Field(name="price", type = FieldType.Float)
    private Float price;
    @Field(name = "publish_date", type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd")
    private String publishDate;
    @Field(name="description", type = FieldType.Text, analyzer = "ik_max_word")
    private String description;
    // getter / setter
}
- 
@Document :注解会对实体中的所有属性建立索引;
 - 
indexName = "books" :表示创建一个名称为 "books" 的索引;
 - 
shards = 1 : 表示只使用一个分片;
 - 
replicas = 0 : 表示不使用复制备份;
 - 
@Field(type = FieldType.Keyword) : 用以指定字段的数据类型。
 
#4. 创建操作的 Repository
@Repository
public interface CustomerRepository extends ElasticsearchRepository<Customer, String> {
    // SearchHits<Customer> findByAddress(String address); 
    List<Customer> findByAddress(String address); 
    Page<Customer> findByAddress(String address, Pageable pageable);
    Customer findByUserName(String userName);
    int deleteByUserName(String userName);
    // 更多示例见笔记的最后
}

我们自定义的 CustomerRepository 接口,从它的祖先们那里继承了大量的现成的方法,除此之外,它还可以按 spring data 的规则定义特定的方法。
#5. 测试 CustomerRepository
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class CustomerRepositoryTest {
    @Autowired
    private CustomerRepository repository;
    @Test
    public void svaeCustomers() {
        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(new Customer("Tommy", "湖北宜昌", 22));
        repository.save(new Customer("Jerry", "湖北荆州", 22));
        repository.save(new Customer("关羽", "大意失荆州", 22));
    }
}
很显然,上面执行的是插入操作(在此之前还执行了建库建表操作)。
注意:由于我们没有指定 id 属性的值,这意味着,我们希望由 Elasticsearch 来为每一个文档的 id 属性赋值。
不过 JavaBean 中的这个 id 属性是 ES 文档(Document)的元属性 _id ,而不是 id 。从 Kibana 中可以看到 id 属性的值是 null 。
如果我们指定了 id 属性值,那么,文档的原属性 _id 和我们自己的 id 属性值是一样的,都是这个我们指定的这个值。
- 
查询所有
for (Customer customer : repository.findAll()) { log.info("{}", customer); }Copied! - 
删除
repository.deleteAll(); repository.deleteByUserName("neo"); // 注意,大小写敏感。Copied! - 
修改
Customer customer = repository.findByUserName("Summer"); log.info("{}", customer); customer.setAddress("湖北武汉武昌区"); repository.save(customer); Customer other = repository.findByUserName("Summer"); log.info("{}", other);Copied! 
#6. 高级查询
#分页查询
Spring Data 自带的分页方案:
Pageable pageable = PageRequest.of(0, 2);
Page<Book> books = bookDao.findAll(pageable);
log.info("总共有 {} 条数据", books.getTotalElements());
log.info("总共有 {} 页", books.getTotalPages());
log.info("本页的页号是 {}", books.getNumber());
log.info("本页的页面大小 {}", books.getSize());
log.info("本页的实际数量 {}", books.getContent().size());
// 本页的内容:List<Book>
books.getContent().forEach((item) -> log.debug("{}", item));
#排序
Spring Data 自带了排序方案:
Sort sort  =  Sort.by(Sort.Direction.DESC, "price");
Iterable<Book> books = bookDao.findAll(sort);
books.forEach((book) -> log.info("{}", book));
#使用 @Query 注解
你可以在 Repository 接口的方法上标注 @Query 注解,并明确指定执行怎样的查询:
@Query("{ \"match\" : { \"description\" : \"?0\" }}")
SearchHits<Book> xxx(String content);
注意:
- 
@Query 注解的字符串内容是
{ ... }形式的,不要忘记了大括号。 - 
@Query 注解的字符串内容中没有
"query",不要多复制了东西; 
假设你传入的 content 参数值为 java ,那么调用上述方法等价于执行了下面的查询:
{
  "query": {
    "match": { "description": "java" }
  }
}
#term 查询
// 不建议使用。官方建议使用上述的 @Query 替代。
QueryBuilder queryBuilder = QueryBuilders.termQuery("description", "java");
Iterable<Book> it = bookDao.search(queryBuilder);
it.forEach((item) -> log.info("{}", item));
#match 查询
// 不建议使用。官方建议使用上述的 @Query 替代。
QueryBuilder queryBuilder = QueryBuilders.matchQuery("description", "java编程");
Iterable<Book> it = bookDao.search(queryBuilder);
it.forEach((item) -> log.info("{}", item));
#多条件查询
QueryBuilders.boolQuery()QueryBuilders.boolQuery().must():相当于 andQueryBuilders.boolQuery().should():相当于 orQueryBuilders.boolQuery().mustNot():相当于 not
注意,合理的 and 和 or 的嵌套关系,有助于你理清逻辑。建议像 Mybatis 的 Example 对象学习,组织成 (... and ...) or (... and ...) 的形式。
// 不建议使用。官方建议使用上述的 @Query 替代。
QueryBuilder query1 = QueryBuilders.termQuery("", "");
QueryBuilder query2 = QueryBuilders.termQuery("", "");
QueryBuilder query3 = QueryBuilders.termQuery("", "");
QueryBuilder query4 = QueryBuilders.termQuery("", "");
// ... and ...
QueryBuilder all = QueryBuilders.boolQuery()
    .must(query1)
    .must(query2);
// ... or ...
QueryBuilder all = QueryBuilders.boolQuery()
    .should(query1)
    .should(query2);
// (... and ...) or (... and ...)
QueryBuilder all = QueryBuilders.boolQuery()
    .should(
        QueryBuilders.boolQuery()
        .must(query1)
        .must(query2))
    .should(
        QueryBuilders.boolQuery()
        .must(query3)
        .must(query4));
#7. 附-版本对应关系
Spring Data ElasticSearch 和 ElasticSearch 是有对应关系的,不同的版本之间不兼容。
官网描述的对应关系如下表:
| Spring Boot | Spring Data Elasticsearch | Elasticsearch | 
|---|---|---|
| 2.2.x | 3.2.x | 6.8.4 | 
| 2.1.x | 3.1.x | 6.2.2 | 
| 2.0.x | 3.0.x | 5.5.0 | 
注意,Spring Boot(Spring Data Elasticsearch)和 Elasticsearch 的版本匹配问题是网上反映较多的问题一定要注意。以上是官网列出的版本对应关系,但是实施情况好像也并非如此,实际情况比较混乱。
总体而言规则如下:
| spring-data-es / spring-boot | ES7 | ES6 | ES5 | 
|---|---|---|---|
| 3.2.x / 2.2.x | 支持 | 支持 | 不支持 | 
| 3.1.x / 2.1.x | 不支持 | 支持 | 不支持 | 
| 3.0.x / 2.0.x | 不支持 | 支持 | 支持 | 
#8. 示例
- 
And: findByNameAndPrice
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } } }Copied! - 
Or:findByNameOrPrice
{ "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } } }Copied! - 
Is: findByName
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } } }Copied! - 
Not: findByNameNot
{ "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } } }Copied! - 
Between: findByPriceBetween
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } } }Copied! - 
LessThan: findByPriceLessThan
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } } }Copied! - 
LessThanEqual: findByPriceLessThanEqual
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } } }Copied! - 
GreaterThan: findByPriceGreaterThan
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } } }Copied! - 
GreaterThanEqual: findByPriceGreaterThan
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } } }Copied! - 
Before: findByPriceBefore
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } } }Copied! - 
After: findByPriceAfter
{ "query" : { "bool" : { "must" : [ { "range" : { "price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } } }Copied! - 
Like: findByNameLike
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } } }Copied! - 
StartingWith: findByNameStartingWith
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } } }Copied! - 
EndingWith: findByNameEndingWith
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } } }Copied! - 
Contains/Containing: findByNameContaining
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } } }Copied! - 
In (when annotated as FieldType.Keyword): findByNameIn(Collection<String>names)
{ "query" : { "bool" : { "must" : [ { "bool" : { "must" : [ { "terms" : {"name" : ["?","?"]} } ] } } ] } } }Copied! - 
In: findByNameIn(Collection<String>names)
{ "query": {"bool": {"must": [{"query_string":{"query": "\"?\" \"?\"", "fields": ["name"]}}]}}}Copied! - 
NotIn (when annotated as FieldType.Keyword): findByNameNotIn(Collection<String>names)
{ "query" : { "bool" : { "must" : [ { "bool" : { "must_not" : [ { "terms" : {"name" : ["?","?"]} } ] } } ] } } }Copied! - 
NotIn: findByNameNotIn(Collection<String>names)
{ "query": { "bool": { "must": [{ "query_string": { "query": "NOT(\"?\" \"?\")", "fields": ["name"] } }] } } }Copied! - 
Near: findByStoreNear
暂不支持
 - 
True: findByAvailableTrue
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } } }Copied! - 
False: findByAvailableFalse
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } } }Copied! - 
OrderBy: findByAvailableTrueOrderByNameDesc
{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{ "name":{"order":"desc"} }] } 
                    
                
                
            
        
浙公网安备 33010602011771号