Elasticsearch使用實戰以及程式碼詳解

waynaqua發表於2024-02-29

Elasticsearch 是一個使用 Java 語言編寫、遵守 Apache 協議、支援 RESTful 風格的分散式全文搜尋和分析引擎,它基於 Lucene 庫構建,並提供多種語言的 API。Elasticsearch 可以對任何型別的資料進行索引、查詢和聚合分析,無論是文字、數字、地理空間、結構化還是非結構化的。Elasticsearch 的核心功能是搜尋,它可以對資料進行分詞匹配、相關性評分、高亮顯示等操作,返回相關度高的結果列表。Elasticsearch 也可以用作資料分析,它可以對資料進行統計、分類、聚類等操作,返回聚合結果或圖表。

本文將用我開源的 waynboot-mall 專案作於程式碼講解,Elasticsearch 版本是 7.10.1。

waynboot-mall 是一套全部開源的微商城專案,包含三個專案:運營後臺、H5 商城和後端介面。實現了一套完整的商城業務,有首頁展示、商品分類、商品詳情、sku 詳情、商品搜尋、加入購物車、結算下單、支付寶/微信支付、訂單列表、商品評論等一系列功能。

本文大綱如下,

image

應用場景

Elasticsearch 的典型應用場景有以下幾種:

  • 全文搜尋:Elasticsearch 提供了全文搜尋的功能,適用於電商商品搜尋、App 搜尋、企業內部資訊搜尋、IT 系統搜尋等。例如我們可以為每一個商品作為文件儲存進 Elasticsearch,然後使用 Elasticsearch 的查詢語言來對文件進行分詞匹配、相關性評分、高亮顯示等操作,返回相關度高的結果列表。
  • 日誌分析:Elasticsearch 可以用來收集、儲存和分析海量的日誌資料,如專案日誌、Nginx log、MySQL Log 等,往往很難從繁雜的日誌中獲取有價值的資訊。Elasticsearch 能夠藉助 Beats、Logstash 等工具快速對接各種常見的資料來源,並透過整合的 Kibana 高效地完成日誌的視覺化分析,讓日誌產生價值。
  • 運維監控:Elasticsearch 也可以用來監控和管理 IT 系統的執行狀態和效能指標,如 CPU、記憶體、磁碟、網路等。可以使用 Beats、Logstash 將這些資料實時採集並索引到 Elasticsearch 中,然後透過 Kibana 構建自定義的儀表盤和告警規則,實現實時的運維監控和預警。
  • 資料視覺化:Elasticsearch 與 Kibana 的結合提供了強大的資料視覺化能力,可以使用 Kibana 來建立各種型別的圖表和儀表盤,展示 Elasticsearch 中儲存或聚合的資料,如直方圖、餅圖、地圖、時間線等。還可以使用 Kibana 的 Canvas 功能來製作動態的資料展示頁面,或者使用 Kibana 的 Lens 功能來進行互動式的資料探索。

waynboot-mall 商城選擇使用 Elasticsearch 作為搜尋引擎,負責對商品資料進行索引和檢索,選擇 Elasticsearch 的原因有以下幾點,

  1. Elasticsearch 是一個開源的分散式搜尋引擎,基於 Lucene 開發,支援全文檢索、結構化檢索、地理位置檢索等多種型別的檢索,功能豐富。
  2. Elasticsearch 本身具有高效能和高可用性的設計,可以透過叢集和分片機制實現水平擴充套件,支援海量資料的儲存和處理,適合大規模的商城搜尋場景。
  3. Elasticsearch 網上社群活躍,現有網際網路上有大量的使用文件和案例,方便入門使用和問題排查。
  4. Elasticsearch 有眾多分詞器外掛,關於中文分詞器的使用非常成熟,拿來即用,支援自定義字典等。

waynboot 專案使用的 Elasticsearch 外掛

Elasticsearch 的外掛非常豐富,我給大家介紹其中 waynboot 專案使用的 Elasticsearch 外掛。

IK Analyzer

IK Analyzer 是一個開源的中文分詞器,由阿里巴巴集團釋出。它採用了細粒度切分和歧義處理等技術,能夠較好地處理各種中文文字。IK Analyzer 支援普通模式、搜尋模式和拼音模式三種分詞方式,並可以根據需要自定義字典。

Pinyin Analyzer

Pinyin Analyzer 外掛是一個用於將中文字元轉換為拼音的外掛,它整合了 NLP 工具(nlp-lang)。該外掛包含了分析器:pinyin,分詞器:pinyin 和 token-filter:pinyin。該外掛還提供了一些可選的引數,可以控制拼音的輸出格式,例如是否保留首字母,是否保留全拼,是否保留非中文字元等。

目錄結構

在 waynboot-mall 專案中,給 Elasticsearch 定義了專門的資料訪問層 waynboot-data-elastic,該層目錄結構如下,

    |-- waynboot-data                    // 資料訪問層
    |   |-- waynboot-data-elastic        // Elasticsearch訪問配置模組
    |       |-- config
    |       |-- constant
    |       |-- mananger

包目錄說明如下,

  • config:Elasticsearch 相關的配置類,包含 ElasticConfig 連線配置類 以及 ElasticClientConfig 客戶端配置相關類,ElasticClientConfig 類可以設定訪問密碼。
  • constants:Elasticsearch 訪問層的相關常量類,這裡面定義了商品同步資料的索引名稱等資訊。
  • mananger:Elasticsearch 訪問層的相關操作類,定義了 ElasticDocument 文件操作類,用於操作 Elasticsearch。

程式碼實戰

在 waynboot-mall 專案中,Elasticsearch 主要用於支援首頁商品的分詞搜尋、分頁排序等功能。Elasticsearch 版本是 7.0,以下實戰講解都是在 7.0 版本基礎上進行。

要使用 Elasticsearch ik 分詞器進行中文分詞搜尋,首先需要安裝相應的外掛 elasticsearch-analysis-ik,然後在建立索引時指定使用中文分詞器作為欄位的 analyzer 屬性。

在日常對 Elasticsearch 的操作中,我們可以透過 rest api 的方式進行操作。

Elasticsearch rest api 操作

如下我們可以建立一個索引名稱為 goods,包含兩個屬性 title、content。並且 這兩個屬性都使用 ik 分詞器。注意這裡我用的 Elasticsearch 提供 Rest api 方式建立索引。

    PUT /goods
    {
        "settings": {
            "index": {
                "number_of_shards": 1,
                "number_of_replicas": 0
            }
        },
        "mappings": {
            "properties": {
                "title": {
                    "type": "text",
                    "analyzer": "ik_max_word"
                },
                "content": {
                    "type": "text",
                    "analyzer": "ik_max_word"
                }
            }
        }
    }

建立索引後,就可以向索引中新增兩條資料,例如:

    POST /books/_doc/1
    {
        "title": "格林童話",
        "content": "這本書介紹了很多童話故事,有白雪公主、獅子王、美人魚等。"
    }

    POST /books/_doc/2
    {
        "title": "中國童話故事",
        "content": "這本書介紹了很多中國童話故事。"
    }

然後我們就可以使用 match 語法來進行中文分詞檢索,這裡我查詢 goods 索引中,title 屬性是 "動畫" 的記錄。如下,

    GET /books/_search
    {
        "query":{
            "match":{
                "title": "童話"
            }
        }
    }

查詢結果如下,

    {
        "took": 0,
        "timed_out": false,
        "_shards": {
            "total": 1,
            "successful": 1,
            "skipped": 0,
            "failed": 0
        },
        "hits": {
            "total": {
                "value": 2,
                "relation": "eq"
            },
            "max_score": 0.11190013,
            "hits": [
                {
                    "_index": "books",
                    "_type": "_doc",
                    "_id": "1",
                    "_score": 0.11190013,
                    "_source": {
                        "title": "格林童話",
                        "content": "這本書介紹了很多童話故事,有白雪公主、獅子王、美人魚等。"
                    }
                },
                {
                    "_index": "books",
                    "_type": "_doc",
                    "_id": "2",
                    "_score": 0.099543065,
                    "_source": {
                        "title": "中國童話故事",
                        "content": "這本書介紹了很多中國童話故事。"
                    }
                }
            ]
        }
    }

可以看到,查詢結果中匹配了標題包含“童話”的文件,這說明 Elasticsearch 使用了中文分詞器對查詢字串和文件進行了分詞,並根據相關性得分返回了結果。

全文搜尋以及篩選排序

在 waynboot-mall 專案中,商城首頁頂部提供了商品搜尋欄,使用者可以輸入商品名稱搜尋自己想要的商品,搜尋結果展示後,還可以進行熱門、新品過濾以及價格、銷量等進行排序。

image

可以看到搜尋功能還是比較複雜的,在 waynboot-mall 專案中,這些邏輯全部在 Elasticsearch 內部進行處理,程式碼如下,

    @RestController
    @AllArgsConstructor
    @RequestMapping("search")
    public class SearchController extends BaseController {
        private IGoodsService iGoodsService;
        private ElasticDocument elasticDocument;

        @GetMapping("result")
        public R result(SearchVO searchVO) throws IOException {
            // 獲取篩選、排序條件
            Long memberId = MobileSecurityUtils.getUserId();
            String keyword = searchVO.getKeyword();
            Boolean filterNew = searchVO.getFilterNew();
            Boolean filterHot = searchVO.getFilterHot();
            Boolean isNew = searchVO.getIsNew();
            Boolean isHot = searchVO.getIsHot();
            Boolean isPrice = searchVO.getIsPrice();
            Boolean isSales = searchVO.getIsSales();
            String orderBy = searchVO.getOrderBy();
            SearchHistory searchHistory = new SearchHistory();
            if (memberId != null && StringUtils.isNotEmpty(keyword)) {
                searchHistory.setCreateTime(LocalDateTime.now());
                searchHistory.setUserId(memberId);
                searchHistory.setKeyword(keyword);
            }
            Page<SearchVO> page = getPage();
            // 查詢包含關鍵字、已上架商品
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            MatchQueryBuilder matchFiler = QueryBuilders.matchQuery("isOnSale", true);
            MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("name", keyword);
            MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("keyword", keyword);
            boolQueryBuilder.filter(matchFiler).should(matchQuery).should(matchPhraseQueryBuilder).minimumShouldMatch(1);
            searchSourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));
            // 按是否新品排序
            if (isNew) {
                searchSourceBuilder.sort(new FieldSortBuilder("isNew").order(SortOrder.DESC));
            }
            // 按是否熱品排序
            if (isHot) {
                searchSourceBuilder.sort(new FieldSortBuilder("isHot").order(SortOrder.DESC));
            }
            // 按價格高低排序
            if (isPrice) {
                searchSourceBuilder.sort(new FieldSortBuilder("retailPrice").order("asc".equals(orderBy) ? SortOrder.ASC : SortOrder.DESC));
            }
            // 按銷量排序
            if (isSales) {
                searchSourceBuilder.sort(new FieldSortBuilder("sales").order(SortOrder.DESC));
            }
            // 篩選新品
            if (filterNew) {
                MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isNew", true);
                boolQueryBuilder.filter(filterQuery);
            }
            // 篩選熱品
            if (filterHot) {
                MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isHot", true);
                boolQueryBuilder.filter(filterQuery);
            }

            // 組裝Elasticsearch查詢條件
            searchSourceBuilder.query(boolQueryBuilder);
            // Elasticsearch分頁相關
            searchSourceBuilder.from((int) (page.getCurrent() - 1) * (int) page.getSize());
            searchSourceBuilder.size((int) page.getSize());
            // 執行Elasticsearch查詢
            List<JSONObject> list = elasticDocument.search("goods", searchSourceBuilder, JSONObject.class);
            List<Integer> goodsIdList = list.stream().map(jsonObject -> (Integer) jsonObject.get("id")).collect(Collectors.toList());
            if (goodsIdList.isEmpty()) {
                return R.success().add("goods", Collections.emptyList());
            }
            // 根據Elasticsearch中返回商品ID查詢商品詳情並保持es中的排序
            List<Goods> goodsList = iGoodsService.searchResult(goodsIdList);
            Map<Integer, Goods> goodsMap = goodsList.stream().collect(Collectors.toMap(goods -> Math.toIntExact(goods.getId()), o -> o));
            List<Goods> returnGoodsList = new ArrayList<>(goodsList.size());
            for (Integer goodsId : goodsIdList) {
                returnGoodsList.add(goodsMap.get(goodsId));
            }
            if (CollectionUtils.isNotEmpty(goodsList)) {
                AsyncManager.me().execute(new TimerTask() {
                    @Override
                    public void run() {
                        searchHistory.setHasGoods(true);
                        iSearchHistoryService.save(searchHistory);
                    }
                });
            }
            return R.success().add("goods", returnGoodsList);
        }
    }

這裡對上面商城的搜尋程式碼給大家做一個講解:

  • 第一步:獲取篩選、排序條件
  • 第二步:獲取查詢條件-使用者搜尋關鍵字、商品已上架
  • 第三步:獲取排序條件-按是否新品排序、按是否熱品排序、按價格高低排序、按銷量排序
  • 第四步:獲取過濾條件-篩選新品、篩選熱品
  • 第五步:組裝 Elasticsearch 查詢條件以及分頁條件
  • 第六步:執行 Elasticsearch 查詢操作
  • 第七步:獲取 Elasticsearch 中返回的商品 ID ,並根據商品 id 查詢商品詳情,最後商品保持 es 中的排序

總結一下

本文給大家講解了 waynboot-mall 專案中對於 elasticsearch 的使用以及程式碼實戰講解。希望能幫助大家更好理解 elasticsearch,大家在自己的專案中如果要引入 elasticsearch,可以直接參照本文的示例程式碼即可使用。

想要獲取 waynboot-mall 專案原始碼的同學,可以關注我公眾號【程式設計師 wayn】,傳送 waynboot-mall 即可領取。

如果覺得這篇文章寫的不錯的話,不妨點贊加關注,我會更新更多技術乾貨、專案教學、經驗分享的文章。

相關文章