SpringBoot 整合 ES 實現(xiàn) CRUD 操作

Hi磅摹,我是空夜,好久不見壮池!

本文介紹 Spring Boot 項目中整合 ElasticSearch 并實現(xiàn) CRUD 操作偏瓤,包括分頁、滾動等功能椰憋。
之前在公司使用 ES厅克,一直用的是前輩封裝好的包,最近希望能夠從原生的 Spring Boot/ES 語法角度來學習 ES 的相關(guān)技術(shù)橙依。希望對大家有所幫助证舟。

本文為 spring-boot-examples 系列文章節(jié)選硕旗,示例代碼已上傳至 https://github.com/laolunsi/spring-boot-examples


安裝 ES 與可視化工具

前往 ES 官方 https://www.elastic.co/cn/downloads/elasticsearch 進行,如 windows 版本只需要下載安裝包女责,啟動 elasticsearch.bat 文件漆枚,瀏覽器訪問 http://localhost:9200

image

如此,表示 ES 安裝完畢抵知。
為更好地查看 ES 數(shù)據(jù)墙基,再安裝一下 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/

可能會出現(xiàn)如下情況:


image

發(fā)現(xiàn)是跨域的問題刷喜。
解決辦法是在 elasticsearch 的 config 文件夾中的 elasticsearch.yml 中添加如下兩行配置:

http.cors.enabled: true
http.cors.allow-origin: "*"

刷新頁面:


image

這里的 article 索引就是我通過 spring boot 項目自動創(chuàng)建的索引残制。
下面我們進入正題。


Spring Boot 引入 ES

創(chuàng)建一個 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

創(chuàng)建一個測試的對象初茶,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 數(shù)據(jù)的三種方式:

  • 實現(xiàn) ElasticsearchRepository 接口
  • 引入 ElasticsearchRestTemplate
  • 引入 ElasticsearchOperations

實現(xiàn)對應(yīng)的 Repository:

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ArticleRepository extends ElasticsearchRepository<Article, String> {

}

下面可以使用這個 ArticleRepository 來操作 ES 中的 Article 數(shù)據(jù)。
我們這里沒有手動創(chuàng)建這個 Article 對應(yīng)的索引浊闪,由 elasticsearch 默認生成恼布。

下面的接口,實現(xiàn)了 spring boot 中對 es 數(shù)據(jù)進行插入搁宾、更新折汞、分頁查詢、滾動查詢盖腿、刪除等操作字支。可以作為一個參考奸忽。其中,使用了 Repository 來獲取揖庄、保存栗菜、刪除 ES 數(shù)據(jù),使用 ElasticsearchRestTemplate 或 ElasticsearchOperations 來進行分頁/滾動查詢蹄梢。

根據(jù) id 獲取/刪除數(shù)據(jù)

    @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) {
        // 根據(jù) id 刪除
        articleRepository.deleteById(id);
        return new JsonResult(true, "刪除成功");
    }

保存數(shù)據(jù)

    @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 "內(nèi)容不能為空";
        }

        return null;
    }

分頁查詢數(shù)據(jù)

    @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, "請輸入合法的分頁參數(shù)");
        }
        // 分頁列表查詢
        // 舊版本的 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;
    }

滾動查詢數(shù)據(jù)

    @GetMapping("scroll")
    public JsonResult scroll(String scrollId, Integer size) {
        // 滾動查詢 scroll api
        if (size == null || size <= 0) {
            return new JsonResult(false, "請輸入每頁查詢數(shù)");
        }
        NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
        query.setPageable(PageRequest.of(0, size));
        SearchHits<Article> searchHits = null;
        if (StringUtils.isEmpty(scrollId)) {
            // 開啟一個滾動查詢,設(shè)置該 scroll 上下文存在 60s
            // 同一個 scroll 上下文禁炒,只需要設(shè)置一次 query(查詢條件)
            searchHits = elasticsearchRestTemplate.searchScrollStart(60000, query, Article.class, IndexCoordinates.of("article"));
            if (searchHits instanceof SearchHitsImpl) {
                scrollId = ((SearchHitsImpl) searchHits).getScrollId();
            }
        } else {
            // 繼續(xù)滾動
            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) {
            // 結(jié)束滾動
            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 滾動查詢

上次遇到一個問題而咆,同事跟我說日志檢索的接口太慢了,問我能不能優(yōu)化一下幕袱。開始使用的是深度分頁暴备,即 1,2,3..10, 這樣的分頁查詢,查詢條件較多(十多個參數(shù))们豌、查詢數(shù)據(jù)量較大(單個日志索引約 2 億條數(shù)據(jù))涯捻。

分頁查詢速度慢的原因在于:ES 的分頁查詢浅妆,如查詢第 100 頁數(shù)據(jù),每頁 10 條障癌,是先從每個分區(qū) (shard凌外,一個索引默認是 5 個 shard) 中把命中的前 100 * 10 條數(shù)據(jù)查出來,然后由協(xié)調(diào)節(jié)點進行合并等操作涛浙,最后給出第 100 頁的數(shù)據(jù)康辑。也就是說,實際被加載到內(nèi)存中的數(shù)據(jù)遠超過理想情況轿亮。

這樣疮薇,索引的 shard 越大,查詢頁數(shù)越多哀托,查詢速度就越慢惦辛。
ES 默認的 max_result_window 是 10000 條,也就是正常情況下仓手,用分頁查詢到 10000 條數(shù)據(jù)時胖齐,就不會再返回下一頁數(shù)據(jù)了。

如果不需要進行跳頁嗽冒,比如直接查詢第 100 頁數(shù)據(jù)呀伙,或者數(shù)據(jù)量非常大,那么可以考慮用 scroll 查詢添坊。
在 scroll 查詢下剿另,第一次需要根據(jù)查詢參數(shù)開啟一個 scroll 上下文,設(shè)置上下文緩存時間贬蛙。以后的滾動只需要根據(jù)第一次返回的 scrollId 來進行即可雨女。

scroll 只支持往下滾動,如果想要往回滾動阳准,還可以根據(jù) scrollId 緩存查詢結(jié)果氛堕,這樣就可以實現(xiàn)上下滾動查詢了 —— 就像大家經(jīng)常使用的淘寶商品檢索時上下滾動一樣。


最近在系統(tǒng)地學習 Redis野蝇、RabbitMQ讼稚、ES 等技術(shù)的知識,著重關(guān)注原理绕沈、底層锐想、并發(fā)等問題,關(guān)于相關(guān)技術(shù)分享后續(xù)會逐漸發(fā)布出來乍狐。歡迎關(guān)注公眾號:猿生物語(ID:JavaApes

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赠摇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝉稳,老刑警劉巖抒蚜,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耘戚,居然都是意外死亡嗡髓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門收津,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饿这,“玉大人,你說我怎么就攤上這事撞秋〕づ酰” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵吻贿,是天一觀的道長串结。 經(jīng)常有香客問我,道長舅列,這世上最難降的妖魔是什么肌割? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮帐要,結(jié)果婚禮上把敞,老公的妹妹穿的比我還像新娘。我一直安慰自己榨惠,他們只是感情好奋早,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赠橙,像睡著了一般耽装。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上期揪,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天剂邮,我揣著相機與錄音,去河邊找鬼横侦。 笑死,一個胖子當著我的面吹牛绰姻,可吹牛的內(nèi)容都是我干的枉侧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼狂芋,長吁一口氣:“原來是場噩夢啊……” “哼榨馁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起帜矾,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤翼虫,失蹤者是張志新(化名)和其女友劉穎屑柔,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珍剑,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡掸宛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了招拙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唧瘾。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖别凤,靈堂內(nèi)的尸體忽然破棺而出饰序,到底是詐尸還是另有隱情,我是刑警寧澤规哪,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布求豫,位于F島的核電站,受9級特大地震影響诉稍,放射性物質(zhì)發(fā)生泄漏蝠嘉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一均唉、第九天 我趴在偏房一處隱蔽的房頂上張望是晨。 院中可真熱鬧,春花似錦舔箭、人聲如沸罩缴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箫章。三九已至,卻和暖如春镜会,著一層夾襖步出監(jiān)牢的瞬間檬寂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工戳表, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桶至,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓匾旭,卻偏偏與公主長得像镣屹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子价涝,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348