Spring Boot 2.0 整合 ES 5 文章內容搜索實戰

出自:Spring For All 社區 《Spring Boot 2.0 整合 ES 5 文章內容搜索實戰http://www.spring4all.com/article/396java

本章內容git

  1. 文章內容搜索思路
  2. 搜索內容分詞
  3. 搜索查詢語句
  4. 篩選條件
  5. 分頁、排序條件
  6. 小結

閱讀時間:8 分鐘github

摘錄:打算起手不凡寫出鴻篇巨做的,每每堅持不了完成第一章節spring

1、文章內容搜索思路

上一篇講了在怎麼在 Spring Boot 2.0 上整合 ES 5 ,這一篇聊聊具體實戰。簡單講下如何實現文章、問答這些內容搜索的具體實現。實現思路很簡單:elasticsearch

  • 基於「短語匹配」並設置最小匹配權重值
  • 哪來的短語,利用 IK 分詞器分詞
  • 基於 Fiter 實現篩選
  • 基於 Pageable 實現分頁排序

這裏直接調用搜索的話,容易搜出不盡人意的東西。由於內容搜索關注內容的鏈接性。因此這裏處理方法比較 low ,但願多交流一塊兒實現更好的搜索方法。就是經過分詞獲得不少短語,而後利用短語進行短語精準匹配。ide

ES 安裝 IK 分詞器插件很簡單。第一步,在下載對應版本 https://github.com/medcl/elasticsearch-analysis-ik/releases。第二步,在 elasticsearch-5.5.3/plugins 目錄下,新建一個文件夾 ik,把 elasticsearch-analysis-ik-5.5.3.zip 解壓後的文件拷貝到 elasticsearch-5.1.1/plugins/ik 目錄下。最後重啓 ES 便可。優化

2、搜索內容分詞

安裝好 IK ,如何調用呢?ui

第一步,我這邊搜搜內容會以 逗號 拼接傳入。因此會先將逗號分割this

第二步,在搜索詞中加入本身自己,由於有些詞通過 ik 分詞後就沒了... 這是個 bug插件

第三步,利用 AnalyzeRequestBuilder 對象獲取 IK 分詞後的返回值對象列表

第四步,優化分詞結果,好比都爲詞,則保留所有;有詞有字,則保留詞;只有字,則保留字

核心實現代碼以下:

/**
     * 搜索內容分詞
     */
    protected List<String> handlingSearchContent(String searchContent) {

        List<String> searchTermResultList = new ArrayList<>();
        // 按逗號分割,獲取搜索詞列表
        List<String> searchTermList = Arrays.asList(searchContent.split(SearchConstant.STRING_TOKEN_SPLIT));

        // 若是搜索詞大於 1 個字,則通過 IK 分詞器獲取分詞結果列表
        searchTermList.forEach(searchTerm -> {
            // 搜索詞 TAG 自己加入搜索詞列表,並解決 will 這種問題
            searchTermResultList.add(searchTerm);
            // 獲取搜索詞 IK 分詞列表
            searchTermResultList.addAll(getIkAnalyzeSearchTerms(searchTerm));
        });

        return searchTermResultList;
    }

    /**
     * 調用 ES 獲取 IK 分詞後結果
     */
    protected List<String> getIkAnalyzeSearchTerms(String searchContent) {
        AnalyzeRequestBuilder ikRequest = new AnalyzeRequestBuilder(elasticsearchTemplate.getClient(),
                AnalyzeAction.INSTANCE, SearchConstant.INDEX_NAME, searchContent);
        ikRequest.setTokenizer(SearchConstant.TOKENIZER_IK_MAX);
        List<AnalyzeResponse.AnalyzeToken> ikTokenList = ikRequest.execute().actionGet().getTokens();

        // 循環賦值
        List<String> searchTermList = new ArrayList<>();
        ikTokenList.forEach(ikToken -> {
            searchTermList.add(ikToken.getTerm());
        });

        return handlingIkResultTerms(searchTermList);
    }

    /**
     * 若是分詞結果:洗髮水(洗髮、發水、洗、發、水)
     * - 均爲詞,保留
     * - 詞 + 字,只保留詞
     * - 均爲字,保留字
     */
    private List<String> handlingIkResultTerms(List<String> searchTermList) {
        Boolean isPhrase = false;
        Boolean isWord = false;
        for (String term : searchTermList) {
            if (term.length() > SearchConstant.SEARCH_TERM_LENGTH) {
                isPhrase = true;
            } else {
                isWord = true;
            }
        }

        if (isWord & isPhrase) {
            List<String> phraseList = new ArrayList<>();
            searchTermList.forEach(term -> {
                if (term.length() > SearchConstant.SEARCH_TERM_LENGTH) {
                    phraseList.add(term);
                }
            });
            return phraseList;
        }

        return searchTermList;
    }

3、搜索查詢語句

構造內容枚舉對象,羅列須要搜索的字段,ContentSearchTermEnum 代碼以下:

import lombok.AllArgsConstructor;

@AllArgsConstructor
public enum ContentSearchTermEnum {

    // 標題
    TITLE("title"),
    // 內容
    CONTENT("content");

    /**
     * 搜索字段
     */
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

循環進行「短語搜索匹配」搜索字段,而後並設置最低權重值爲 1。核心代碼以下:

/**
     * 構造查詢條件
     */
    private void buildMatchQuery(BoolQueryBuilder queryBuilder, List<String> searchTermList) {
        for (String searchTerm : searchTermList) {
            for (ContentSearchTermEnum searchTermEnum : ContentSearchTermEnum.values()) {
                queryBuilder.should(QueryBuilders.matchPhraseQuery(searchTermEnum.getName(), searchTerm));
            }
        }
        queryBuilder.minimumShouldMatch(SearchConstant.MINIMUM_SHOULD_MATCH);
    }

4、篩選條件

搜到東西不止,有時候需求是這樣的。須要在某個品類下搜索,好比電商須要在某個 品牌 下搜索商品。那麼須要構造一些 fitler 進行篩選。對應 SQL 語句的 Where 下的 OR 和 AND 兩種語句。在 ES 中使用 filter 方法添加過濾。代碼以下:

/**
     * 構建篩選條件
     */
    private void buildFilterQuery(BoolQueryBuilder boolQueryBuilder, Integer type, String category) {
        // 內容類型篩選
        if (type != null) {
            BoolQueryBuilder typeFilterBuilder = QueryBuilders.boolQuery();
            typeFilterBuilder.should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, type).lenient(true));
            boolQueryBuilder.filter(typeFilterBuilder);
        }

        // 內容類別篩選
        if (!StringUtils.isEmpty(category)) {
            BoolQueryBuilder categoryFilterBuilder = QueryBuilders.boolQuery();
            categoryFilterBuilder.should(QueryBuilders.matchQuery(SearchConstant.CATEGORY_NAME, category).lenient(true));
            boolQueryBuilder.filter(categoryFilterBuilder);
        }
    }

type 是大類,category 是小類,這樣就能夠支持 大小類 篩選。可是若是須要在 type = 1 或者 type = 2 中搜索呢?具體實現代碼很簡單:

typeFilterBuilder
	.should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, 1)
	.should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, 2)
	.lenient(true));

經過鏈式表達式,兩個 should 實現或,即 SQL 對應的 OR 語句。經過兩個 BoolQueryBuilder 實現與,即 SQL 對應的 AND 語句。

5、分頁、排序條件

分頁排序代碼就很簡單了:

@Override
    public PageBean searchContent(ContentSearchBean contentSearchBean) {

        Integer pageNumber = contentSearchBean.getPageNumber();
        Integer pageSize = contentSearchBean.getPageSize();

        PageBean<ContentEntity> resultPageBean = new PageBean<>();
        resultPageBean.setPageNumber(pageNumber);
        resultPageBean.setPageSize(pageSize);

        // 構建搜索短語
        String searchContent = contentSearchBean.getSearchContent();
        List<String> searchTermList = handlingSearchContent(searchContent);

        // 構建查詢條件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        buildMatchQuery(boolQueryBuilder, searchTermList);

        // 構建篩選條件
        buildFilterQuery(boolQueryBuilder, contentSearchBean.getType(), contentSearchBean.getCategory());

        // 構建分頁、排序條件
        Pageable pageable = PageRequest.of(pageNumber, pageSize);
        if (!StringUtils.isEmpty(contentSearchBean.getOrderName())) {
            pageable = PageRequest.of(pageNumber, pageSize, Sort.Direction.DESC, contentSearchBean.getOrderName());
        }
        SearchQuery searchQuery = new NativeSearchQueryBuilder().withPageable(pageable)
                .withQuery(boolQueryBuilder).build();

        // 搜索
        LOGGER.info("\n ContentServiceImpl.searchContent() [" + searchContent
                + "] \n DSL  = \n " + searchQuery.getQuery().toString());
        Page<ContentEntity> contentPage = contentRepository.search(searchQuery);

        resultPageBean.setResult(contentPage.getContent());
        resultPageBean.setTotalCount((int) contentPage.getTotalElements());
        resultPageBean.setTotalPage((int) contentPage.getTotalElements() / resultPageBean.getPageSize() + 1);
        return resultPageBean;
    }

利用 Pageable 對象,構造分頁參數以及指定對應的 排序字段、排序順序(DESC ASC)便可。

6、小結

這個思路比較簡單。若是你們有更吊的實現方法,歡迎交流討論。

更多 ES 文章,關注便可得系列教程文章哦!

相關文章
相關標籤/搜索