Hi,我是空夜,好久不見!
本文介紹 Spring Boot 項目中整合 ElasticSearch 並實現 CRUD 操作,包括分頁、滾動等功能。
之前在公司使用 ES,一直用的是前輩封裝好的包,最近希望能夠從原生的 Spring Boot/ES 語法角度來學習 ES 的相關技術。希望對大家有所幫助。
本文為 spring-boot-examples 系列文章節選,示例代碼已上傳至 https://github.com/laolunsi/spring-boot-examples
安裝 ES 與可視化工具
前往 ES 官方 https://www.elastic.co/cn/downloads/elasticsearch 進行,如 windows 版本只需要下載安裝包,啟動 elasticsearch.bat 文件,瀏覽器訪問 http://localhost:9200
如此,表示 ES 安裝完畢。
為更好地查看 ES 數據,再安裝一下 elasticsearch-head 可視化插件。前往下載地址:https://github.com/mobz/elasticsearch-head
主要步驟:
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
open
http://localhost:9100/
可能會出現如下情況:
發現是跨域的問題。
解決辦法是在 elasticsearch 的 config 文件夾中的 elasticsearch.yml 中添加如下兩行配置:
http.cors.enabled: true
http.cors.allow-origin: "*"
刷新頁面:
這里的 article 索引就是我通過 spring boot 項目自動創建的索引。
下面我們進入正題。
Spring Boot 引入 ES
創建一個 spring-boot 項目,引入 es 的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置 application.yml:
server:
port: 8060
spring:
elasticsearch:
rest:
uris: http://localhost:9200
創建一個測試的對象,article:
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import java.util.Date;
@Document(indexName = "article")
public class Article {
@Id
private String id;
private String title;
private String content;
private Integer userId;
private Date createTime;
// ... igonre getters and setters
}
下面介紹 Spring Boot 中操作 ES 數據的三種方式:
- 實現 ElasticsearchRepository 接口
- 引入 ElasticsearchRestTemplate
- 引入 ElasticsearchOperations
實現對應的 Repository:
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ArticleRepository extends ElasticsearchRepository<Article, String> {
}
下面可以使用這個 ArticleRepository 來操作 ES 中的 Article 數據。
我們這里沒有手動創建這個 Article 對應的索引,由 elasticsearch 默認生成。
下面的接口,實現了 spring boot 中對 es 數據進行插入、更新、分頁查詢、滾動查詢、刪除等操作。可以作為一個參考。其中,使用了 Repository 來獲取、保存、刪除 ES 數據,使用 ElasticsearchRestTemplate 或 ElasticsearchOperations 來進行分頁/滾動查詢。
根據 id 獲取/刪除數據
@Autowired
private ArticleRepository articleRepository;
@GetMapping("{id}")
public JsonResult findById(@PathVariable String id) {
Optional<Article> article = articleRepository.findById(id);
JsonResult jsonResult = new JsonResult(true);
jsonResult.put("article", article.orElse(null));
return jsonResult;
}
@DeleteMapping("{id}")
public JsonResult delete(@PathVariable String id) {
// 根據 id 刪除
articleRepository.deleteById(id);
return new JsonResult(true, "刪除成功");
}
保存數據
@PostMapping("")
public JsonResult save(Article article) {
// 新增或更新
String verifyRes = verifySaveForm(article);
if (!StringUtils.isEmpty(verifyRes)) {
return new JsonResult(false, verifyRes);
}
if (StringUtils.isEmpty(article.getId())) {
article.setCreateTime(new Date());
}
Article a = articleRepository.save(article);
boolean res = a.getId() != null;
return new JsonResult(res, res ? "保存成功" : "");
}
private String verifySaveForm(Article article) {
if (article == null || StringUtils.isEmpty(article.getTitle())) {
return "標題不能為空";
} else if (StringUtils.isEmpty(article.getContent())) {
return "內容不能為空";
}
return null;
}
分頁查詢數據
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
ElasticsearchOperations elasticsearchOperations;
@GetMapping("list")
public JsonResult list(Integer currentPage, Integer limit) {
if (currentPage == null || currentPage < 0 || limit == null || limit <= 0) {
return new JsonResult(false, "請輸入合法的分頁參數");
}
// 分頁列表查詢
// 舊版本的 Repository 中的 search 方法被廢棄了。
// 這里采用 ElasticSearchRestTemplate 或 ElasticsearchOperations 來進行分頁查詢
JsonResult jsonResult = new JsonResult(true);
NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
query.setPageable(PageRequest.of(currentPage, limit));
// 方法1:
SearchHits<Article> searchHits = elasticsearchRestTemplate.search(query, Article.class);
// 方法2:
// SearchHits<Article> searchHits = elasticsearchOperations.search(query, Article.class);
List<Article> articles = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
jsonResult.put("count", searchHits.getTotalHits());
jsonResult.put("articles", articles);
return jsonResult;
}
滾動查詢數據
@GetMapping("scroll")
public JsonResult scroll(String scrollId, Integer size) {
// 滾動查詢 scroll api
if (size == null || size <= 0) {
return new JsonResult(false, "請輸入每頁查詢數");
}
NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
query.setPageable(PageRequest.of(0, size));
SearchHits<Article> searchHits = null;
if (StringUtils.isEmpty(scrollId)) {
// 開啟一個滾動查詢,設置該 scroll 上下文存在 60s
// 同一個 scroll 上下文,只需要設置一次 query(查詢條件)
searchHits = elasticsearchRestTemplate.searchScrollStart(60000, query, Article.class, IndexCoordinates.of("article"));
if (searchHits instanceof SearchHitsImpl) {
scrollId = ((SearchHitsImpl) searchHits).getScrollId();
}
} else {
// 繼續滾動
searchHits = elasticsearchRestTemplate.searchScrollContinue(scrollId, 60000, Article.class, IndexCoordinates.of("article"));
}
List<Article> articles = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
if (articles.size() == 0) {
// 結束滾動
elasticsearchRestTemplate.searchScrollClear(Collections.singletonList(scrollId));
scrollId = null;
}
if (scrollId == null) {
return new JsonResult(false, "已到末尾");
} else {
JsonResult jsonResult = new JsonResult(true);
jsonResult.put("count", searchHits.getTotalHits());
jsonResult.put("size", articles.size());
jsonResult.put("articles", articles);
jsonResult.put("scrollId", scrollId);
return jsonResult;
}
}
ES 深度分頁 vs 滾動查詢
上次遇到一個問題,同事跟我說日志檢索的接口太慢了,問我能不能優化一下。開始使用的是深度分頁,即 1,2,3..10, 這樣的分頁查詢,查詢條件較多(十多個參數)、查詢數據量較大(單個日志索引約 2 億條數據)。
分頁查詢速度慢的原因在於:ES 的分頁查詢,如查詢第 100 頁數據,每頁 10 條,是先從每個分區 (shard,一個索引默認是 5 個 shard) 中把命中的前 100 * 10 條數據查出來,然后由協調節點進行合並等操作,最后給出第 100 頁的數據。也就是說,實際被加載到內存中的數據遠超過理想情況。
這樣,索引的 shard 越大,查詢頁數越多,查詢速度就越慢。
ES 默認的 max_result_window 是 10000 條,也就是正常情況下,用分頁查詢到 10000 條數據時,就不會再返回下一頁數據了。
如果不需要進行跳頁,比如直接查詢第 100 頁數據,或者數據量非常大,那么可以考慮用 scroll 查詢。
在 scroll 查詢下,第一次需要根據查詢參數開啟一個 scroll 上下文,設置上下文緩存時間。以后的滾動只需要根據第一次返回的 scrollId 來進行即可。
scroll 只支持往下滾動,如果想要往回滾動,還可以根據 scrollId 緩存查詢結果,這樣就可以實現上下滾動查詢了 —— 就像大家經常使用的淘寶商品檢索時上下滾動一樣。
最近在系統地學習 Redis、RabbitMQ、ES 等技術的知識,着重關注原理、底層、並發等問題,關於相關技術分享后續會逐漸發布出來。歡迎關注公眾號:猿生物語(ID:JavaApes)