ElasticSearch8 - SpringBoot整合ElasticSearch

2024年3月25日 868点热度 0人点赞 2条评论

前言

springboot 整合 ES 有两种方案,ES 官方提供的 Elasticsearch Java API Client 和 spring 提供的 [Spring Data Elasticsearch](Spring Data Elasticsearch)

两种方案各有优劣

Spring:高度封装,用着舒服。缺点是更新不及时,有可能无法使用 ES 的新 API

ES 官方:更新及时,灵活,缺点是太灵活了,基本是一比一复制 REST APIs,项目中使用需要二次封装。

Elasticsearch Java API Client

目前最新版本 ES8.12,要求 jdk8 以上,API 里面使用了大量的 builder 和 lambda

官方也提供了 测试用例

兼容

翻了不少博客,大部分都是使用 High Level Rest Client,这是旧版本的 api,新版本使用 Elasticsearch Java API Client,如何兼容旧版本,官方也提供了解决方案)

下文描述的均是新版 API

添加 jar 包

官方文档:[installation](安装| Elasticsearch Java API 客户端 [8.12] |松紧带 --- Installation | Elasticsearch Java API Client [8.12] | Elastic)

使用的是 maven,在 pom.xml 中添加

<dependency>  
    <groupId>co.elastic.clients</groupId>  
    <artifactId>elasticsearch-java</artifactId>  
    <version>8.12.2</version>  
</dependency>

<!-- 如果有添加springmvc,此包可不引入 -->
<dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-databind</artifactId>  
    <version>2.12.3</version>  
</dependency>

如果报错 ClassNotFoundException: jakarta.json.spi.JsonProvider,则还需要添加

<dependency>
    <groupId>jakarta.json</groupId>
    <artifactId>jakarta.json-api</artifactId>
    <version>2.0.1</version>
</dependency>

打印请求

在 application. yml 中添加配置,打印 es 的 http 请求(建议在开发调试时使用)

logging:  
  level:  
    tracer: TRACE

连接 ES

配置文件如下,后续所有 ES 操作都通过 ElasticsearchClient 对象

更多配置请看 Common configuration

@Configuration  
public class ElasticSearchConfig {  

    @Bean  
    public ElasticsearchClient esClient() {  
        // ES服务器URL  
        String serverUrl = "http://127.0.0.1:9200";  
        // ES用户名和密码  
        String userName = "xxx";  
        String password = "xxx";  

        BasicCredentialsProvider credsProv = new BasicCredentialsProvider();  
        credsProv.setCredentials(  
                AuthScope.ANY, new UsernamePasswordCredentials(userName, password)  
        );  

        RestClient restClient = RestClient  
                .builder(HttpHost.create(serverUrl))  
                .setHttpClientConfigCallback(hc -> hc.setDefaultCredentialsProvider(credsProv))  
                .build();  

        ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());  
        return new ElasticsearchClient(transport);  
    }  

}

索引操作

代码中的 esClient 就是 ElasticsearchClient,请自行注入 bean

// 索引名字  
String indexName = "student";  

// 索引是否存在  
BooleanResponse books = esClient.indices().exists(e -> e.index(indexName));  
System.out.println("索引是否存在:" + books.value());  

// 创建索引  
esClient.indices().create(c -> c  
        .index(indexName)  
        .mappings(mappings -> mappings  // 映射  
                .properties("name", p -> p  
                        .text(t -> t // text类型,index=false  
                                .index(false)  
                        )  
                )  
                .properties("age", p -> p  
                        .long_(t -> t) // long类型  
                )  
        )  
);  

// 删除索引  
esClient.indices().delete(d -> d.index(indexName));

文档操作 (CRUD)

下文以官方测试数据 account. json 为例

实体类

首先定义实体类,用于 ES 中的字段

public class Account {
    private String id;
    // 解决ES中字段与实体类字段不一致的问题  
    @JsonProperty("account_number")
    private Long account_number;  
    private String address;  
    private Integer age;  
    private Long balance;  
    private String city;  
    private String email;  
    private String employer;  
    private String firstname;  
    private String lastname;  
    private String gender;  
    private String state;  
    ... 省略get、set方法
}

新增

String indexName = "account";  // 索引名字
Account account = new Account();
account.setId("1");
account.setLastname("guyu");   

// 新增
CreateResponse createResponse = esClient.create(c -> c  
        .index(indexName) // 索引名字  
        .id(account.getId()) // id  
        .document(account) // 实体类  
);

修改

UpdateResponse<Account> updateResp = esClient.update(u -> u  
                .index(indexName)  
                .id(account.getId())  
                .doc(account),  
        Account.class  
);

删除

DeleteResponse deleteResp = esClient.delete(d -> d.index(indexName).id("1"));

批量新增

批量操作需要使用到 bulk

List<Account> accountList = ...
BulkRequest.Builder br = new BulkRequest.Builder();  
for (Account acc : accountList) {  
    br.operations(op -> op  
            .create(c -> c  
                    .index(indexName)  
                    .id(acc.getId())  
                    .document(acc)  
            )  
    );  
}  
BulkResponse bulkResp = esClient.bulk(br.build());

有没有觉得批量新增的 .create () 里面的参数很眼熟,批量删除和更新请举一反三

根据 id 查询

// 定义实体类
GetResponse<Account> getResp = esClient.get(g -> g.index(indexName).id("1"), Account.class);  
if (getResp.found()) {  
    Account source = getResp.source();  // 这就是得到的实体类
    source.setId(getResp.id());  
}

// 不定义实体类
GetResponse<ObjectNode> getResp = esClient.get(g -> g  
                .index(indexName)  
                .id("1"),  
        ObjectNode.class  
);  
if (getResp.found()) {  
    ObjectNode json = getResp.source();  
    String firstname = json.get("firstname").asText();  
    System.out.println(firstname);  
}

搜索

搜索全部

SearchResponse<Account> searchResp = esClient.search(s -> s  
        .index(indexName)  
        .query(q -> q.matchAll(m -> m))  // 搜索全部
        , Account.class  
);

HitsMetadata<Account> hits = searchResp.hits();  
long totalValue = hits.total().value(); // 匹配到的数量  
hits.hits().forEach(h -> {  
    Account acc = h.source(); // 这就是得到的实体类  
    acc.setId(h.id());  
});

ES API 的对象定义,基本与返回的 json 一一对应的,所以 SearchResponse 就不过多赘述。

搜索 firstname = Amber

SearchResponse<Account> searchResp = esClient.search(s -> s  
        .index(indexName)  
        .query(q -> q  // 查询  
                .match(t -> t  
                        .field("firstname")  
                        .query("Amber")  
                )  
        )  
        , Account.class  
);

// 也可以这样写
Query firstNameQuery = MatchQuery.of(m -> m.field("firstname").query("Amber"))._toQuery();
SearchResponse<Account> searchResp = esClient.search(s -> s  
        .index(indexName)  
        .query(firstNameQuery)  
        , Account.class  
);

嵌套查询,比如搜索 firstname = Amber AND age = 32

Query firstNameQuery = MatchQuery.of(m -> m.field("firstname").query("Amber"))._toQuery();  
Query ageQuery = MatchQuery.of(m -> m.field("age").query(32))._toQuery();  

SearchResponse<Account> searchResp = esClient.search(s -> s  
        .index(indexName)  
        .query(q -> q  
                .bool(b -> b.must(firstNameQuery, ageQuery))  
        )  
        , Account.class  
);

浅分页

from 和 size 参数类似于 mysql 的 limit,详细说明见 Paginate search results

SearchResponse<Account> searchResp = esClient.search(s -> s  
        .index(indexName)   
        .from(0)  // 分页参数
        .size(20)  // 分页参数
        , Account.class  
);

排序

SearchResponse<Account> searchResp = esClient.search(s -> s  
        .index(indexName)   
        .sort(so -> so  // 排序字段1
                .field(f -> f  
                        .field("age")  
                        .order(SortOrder.Asc)  
                )  
        )  
        .sort(so -> so  // 排序字段2
                .field(f -> f  
                        .field("account_number")  
                        .order(SortOrder.Desc)  
                )  
        )  
        , Account.class  
);

Spring Data Elasticsearch

文档: Spring Data Elasticsearch

添加 jar 和配置

pom.xml添加依赖

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

yml 配置

spring:  
  elasticsearch:  
    uris: http://xxx:9200  
    username: xxx  
    password: xxx
logging:  
  level:  
  # 输出es的查询参数(调试用)
    tracer: TRACE  

索引操作

实体类

@Data  
@Document(indexName = "account")  
public class Account {  

    @Id  
    private String id;  
    // 解决ES中字段与实体类字段不一致的问题  
    @Field(name = "account_number", type = FieldType.Long)  
    private Long accountNumber;  
    @Field(type = FieldType.Text)  
    private String address;  
    @Field(type = FieldType.Integer)  
    private Integer age;  
    @Field(type = FieldType.Long)  
    private Long balance;  
    @Field(type = FieldType.Text)  
    private String city;  
    @Field(type = FieldType.Text)  
    private String email;  
    @Field(type = FieldType.Text)  
    private String employer;  
    @Field(type = FieldType.Text)  
    private String firstname;  
    @Field(type = FieldType.Text)  
    private String lastname;  
    @Field(type = FieldType.Text)  
    private String gender;  
    @Field(type = FieldType.Text)  
    private String state;  
    ... 省略get、set 方法
}
@Resource
private ElasticsearchTemplate template;

IndexOperations idxOpt = template.indexOps(Account.class);  
// 索引是否存在  
boolean idxExist = idxOpt.exists();  

// 创建索引  
boolean createSuccess = idxOpt.createWithMapping();  
System.out.println(createSuccess);  

// 删除索引  
boolean deleted = idxOpt.delete();

文档操作(CRUD)

Account account = new Account();  
account.setId("1");  
account.setLastname("guyu");  

// 这是插入或覆盖,如果id存在了就是覆盖  
template.save(account);  

// 修改,用的是es的_update  
template.update(account);

// 删除  
template.delete(account)

// 批量新增(用的是es的_bulk)
List<Account> accountList = ...
template.save(accountList);

// 根据id查询
Account account = template.get("1", Account.class);

搜索 + 排序 + 分页

// 搜索 firstname = Amber AND age = 32
Criteria criteria = new Criteria();  
criteria.and(new Criteria("firstname").is("Amber"));  
criteria.and(new Criteria("age").is(32));  

// 分页  
int pageNum = 1; // 页码  
int pageSize = 20; // 每页数量  
Query query = new CriteriaQueryBuilder(criteria)  
        .withSort(Sort.by(new Order(Sort.Direction.ASC, "age"))) // 排序字段1  
        .withSort(Sort.by(new Order(Sort.Direction.DESC, "balance"))) // 排序字段1  
        .withPageable(PageRequest.of(pageNum - 1, pageSize)) // 浅分页  
        // 不需要查询的字段
        .withSourceFilter(new FetchSourceFilterBuilder().withExcludes("email", "address").build())
        .build();  

SearchHits<Account> searchHits = template.search(query, Account.class);  
long totalValue = searchHits.getTotalHits(); // 匹配到的数量  
for (SearchHit<Account> searchHit : searchHits.getSearchHits()) {  
    Account account = searchHit.getContent(); // 这就是得到的实体类  
}

总结

本文介绍了 SpringBoot 整合 ElasticSearch 的两种方案,但均只是简单提及,更详细的用法需要自行查看官方文档。

王谷雨

一个苟且偷生的java程序员

文章评论

  • wwww

    ?哇塞大佬,你和我学姐的名字一样。。

    2024年4月26日