一、文章內容搜索思路
上一篇講了在怎麼在Spring Boot 2.0 上整合ES 5 ,這一篇聊聊具體實戰。簡單講下如何實現文章、問答這些內容搜索的具體實現。實現思路很簡單:
這裡直接調用搜索的話,容易搜出不盡人意的東西。因為內容搜索關注內容的連接性。所以這里處理方法比較low ,希望多交流一起實現更好的搜索方法。就是通過分詞得到很多短語,然後利用短語進行短語精準匹配。
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 即可。
二、搜索內容分詞
安裝好IK ,如何調用呢?
第一步,我這邊搜搜內容會以逗號拼接傳入。所以會先將逗號分割
第二步,在搜索詞中加入自己本身,因為有些詞經過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; }三、搜索查詢語句
構造內容枚舉對象,羅列需要搜索的字段,ContentSearchTermEnum 代碼如下:
import lombok.AllArgsConstructor;@AllArgsConstructorpublic 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); }四、篩選條件
搜到東西不止,有時候需求是這樣的。需要在某個品類下搜索,比如電商需要在某個品牌下搜索商品。那麼需要構造一些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 語句。
五、分頁、排序條件
分頁排序代碼就很簡單了:
@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)即可。
六、小結
這個思路比較簡單。希望對大家的學習有所幫助,也希望大家多多支持武林網。