Spring Boot2.0整合ES5實(shí)現(xiàn)文章內(nèi)容搜索實(shí)戰(zhàn)
一、文章內(nèi)容搜索思路
上一篇講了在怎么在 Spring Boot 2.0 上整合 ES 5 ,這一篇聊聊具體實(shí)戰(zhàn)。簡(jiǎn)單講下如何實(shí)現(xiàn)文章、問答這些內(nèi)容搜索的具體實(shí)現(xiàn)。實(shí)現(xiàn)思路很簡(jiǎn)單:
- 基于「短語匹配」并設(shè)置最小匹配權(quán)重值
- 哪來的短語,利用 IK 分詞器分詞
- 基于 Fiter 實(shí)現(xiàn)篩選
- 基于 Pageable 實(shí)現(xiàn)分頁排序
這里直接調(diào)用搜索的話,容易搜出不盡人意的東西。因?yàn)閮?nèi)容搜索關(guān)注內(nèi)容的連接性。所以這里處理方法比較 low ,希望多交流一起實(shí)現(xiàn)更好的搜索方法。就是通過分詞得到很多短語,然后利用短語進(jìn)行短語精準(zhǔn)匹配。
ES 安裝 IK 分詞器插件很簡(jiǎn)單。第一步,在下載對(duì)應(yīng)版本 https://github.com/medcl/elasticsearch-analysis-ik/releases。第二步,在 elasticsearch-5.5.3/plugins 目錄下,新建一個(gè)文件夾 ik,把 elasticsearch-analysis-ik-5.5.3.zip 解壓后的文件拷貝到 elasticsearch-5.1.1/plugins/ik 目錄下。最后重啟 ES 即可。
二、搜索內(nèi)容分詞
安裝好 IK ,如何調(diào)用呢?
第一步,我這邊搜搜內(nèi)容會(huì)以 逗號(hào) 拼接傳入。所以會(huì)先將逗號(hào)分割
第二步,在搜索詞中加入自己本身,因?yàn)橛行┰~經(jīng)過 ik 分詞后就沒了... 這是個(gè) bug
第三步,利用 AnalyzeRequestBuilder 對(duì)象獲取 IK 分詞后的返回值對(duì)象列表
第四步,優(yōu)化分詞結(jié)果,比如都為詞,則保留全部;有詞有字,則保留詞;只有字,則保留字
核心實(shí)現(xiàn)代碼如下:
/**
* 搜索內(nèi)容分詞
*/
protected List<String> handlingSearchContent(String searchContent) {
List<String> searchTermResultList = new ArrayList<>();
// 按逗號(hào)分割,獲取搜索詞列表
List<String> searchTermList = Arrays.asList(searchContent.split(SearchConstant.STRING_TOKEN_SPLIT));
// 如果搜索詞大于 1 個(gè)字,則經(jīng)過 IK 分詞器獲取分詞結(jié)果列表
searchTermList.forEach(searchTerm -> {
// 搜索詞 TAG 本身加入搜索詞列表,并解決 will 這種問題
searchTermResultList.add(searchTerm);
// 獲取搜索詞 IK 分詞列表
searchTermResultList.addAll(getIkAnalyzeSearchTerms(searchTerm));
});
return searchTermResultList;
}
/**
* 調(diào)用 ES 獲取 IK 分詞后結(jié)果
*/
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();
// 循環(huán)賦值
List<String> searchTermList = new ArrayList<>();
ikTokenList.forEach(ikToken -> {
searchTermList.add(ikToken.getTerm());
});
return handlingIkResultTerms(searchTermList);
}
/**
* 如果分詞結(jié)果:洗發(fā)水(洗發(fā)、發(fā)水、洗、發(fā)、水)
* - 均為詞,保留
* - 詞 + 字,只保留詞
* - 均為字,保留字
*/
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;
}
三、搜索查詢語句
構(gòu)造內(nèi)容枚舉對(duì)象,羅列需要搜索的字段,ContentSearchTermEnum 代碼如下:
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum ContentSearchTermEnum {
// 標(biāo)題
TITLE("title"),
// 內(nèi)容
CONTENT("content");
/**
* 搜索字段
*/
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
循環(huán)進(jìn)行「短語搜索匹配」搜索字段,然后并設(shè)置最低權(quán)重值為 1。核心代碼如下:
/**
* 構(gòu)造查詢條件
*/
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);
}
四、篩選條件
搜到東西不止,有時(shí)候需求是這樣的。需要在某個(gè)品類下搜索,比如電商需要在某個(gè) 品牌 下搜索商品。那么需要構(gòu)造一些 fitler 進(jìn)行篩選。對(duì)應(yīng) SQL 語句的 Where 下的 OR 和 AND 兩種語句。在 ES 中使用 filter 方法添加過濾。代碼如下:
/**
* 構(gòu)建篩選條件
*/
private void buildFilterQuery(BoolQueryBuilder boolQueryBuilder, Integer type, String category) {
// 內(nèi)容類型篩選
if (type != null) {
BoolQueryBuilder typeFilterBuilder = QueryBuilders.boolQuery();
typeFilterBuilder.should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, type).lenient(true));
boolQueryBuilder.filter(typeFilterBuilder);
}
// 內(nèi)容類別篩選
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 中搜索呢?具體實(shí)現(xiàn)代碼很簡(jiǎn)單:
typeFilterBuilder .should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, 1) .should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, 2) .lenient(true));
通過鏈?zhǔn)奖磉_(dá)式,兩個(gè) should 實(shí)現(xiàn)或,即 SQL 對(duì)應(yīng)的 OR 語句。通過兩個(gè) BoolQueryBuilder 實(shí)現(xiàn)與,即 SQL 對(duì)應(yīng)的 AND 語句。
五、分頁、排序條件
分頁排序代碼就很簡(jiǎn)單了:
@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);
// 構(gòu)建搜索短語
String searchContent = contentSearchBean.getSearchContent();
List<String> searchTermList = handlingSearchContent(searchContent);
// 構(gòu)建查詢條件
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
buildMatchQuery(boolQueryBuilder, searchTermList);
// 構(gòu)建篩選條件
buildFilterQuery(boolQueryBuilder, contentSearchBean.getType(), contentSearchBean.getCategory());
// 構(gòu)建分頁、排序條件
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 對(duì)象,構(gòu)造分頁參數(shù)以及指定對(duì)應(yīng)的 排序字段、排序順序(DESC ASC)即可。
六、小結(jié)
這個(gè)思路比較簡(jiǎn)單。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot基于Redis實(shí)現(xiàn)生成全局唯一ID的方法
在項(xiàng)目中生成全局唯一ID有很多好處,生成全局唯一ID有助于提高系統(tǒng)的可用性、數(shù)據(jù)的完整性和安全性,同時(shí)也方便數(shù)據(jù)的管理和分析,所以本文給大家介紹了SpringBoot基于Redis實(shí)現(xiàn)生成全局唯一ID的方法,文中有詳細(xì)的代碼講解,需要的朋友可以參考下2023-12-12
Java實(shí)現(xiàn)LeetCode(1.兩數(shù)之和)
這篇文章主要介紹了Java實(shí)現(xiàn)LeetCode(兩數(shù)之和),本文使用java采用多種發(fā)放實(shí)現(xiàn)了LeetCode的兩數(shù)之和題目,需要的朋友可以參考下2021-06-06
解決springboot集成rocketmq關(guān)于tag的坑
這篇文章主要介紹了解決springboot集成rocketmq關(guān)于tag的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Java如何將Excel數(shù)據(jù)導(dǎo)入到數(shù)據(jù)庫
這篇文章主要為大家詳細(xì)介紹了Java將Excel數(shù)據(jù)導(dǎo)入到數(shù)據(jù)庫的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
詳解Spring框架之基于Restful風(fēng)格實(shí)現(xiàn)的SpringMVC
這篇文章主要介紹了詳解Spring框架之基于Restful風(fēng)格實(shí)現(xiàn)的SpringMVC,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
Spring中@Autowired @Resource @Inject三個(gè)注解有什么區(qū)別
在我們使用Spring框架進(jìn)行日常開發(fā)過程中,經(jīng)常會(huì)使用@Autowired, @Resource, @Inject注解來進(jìn)行依賴注入,下面來介紹一下這三個(gè)注解有什么區(qū)別2023-03-03
javax.validation自定義日期范圍校驗(yàn)注解操作
這篇文章主要介紹了javax.validation自定義日期范圍校驗(yàn)注解操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09

