一、文章内容搜索思路
上一篇讲了在怎么在 Spring Boot 2.0 上整合 ES 5 ,这一篇聊聊具体实战。简单讲下如何实现文章、问答这些内容搜索的具体实现。实现思路很简单:
基于「短语匹配」并设置最小匹配权重值
哪来的短语,利用 IK 分词器分词
基于 Fiter 实现筛选
基于 Pageable 实现分页排序
这里直接调用搜索的话,容易搜出不尽人意的东西。因为内容搜索关注内容的连接性。所以这里处理方法比较 low ,希望多交流一起实现更好的搜索方法。就是通过分词得到很多短语,然后利用短语进行短语精准匹配。
二、搜索内容分词
安装好 IK ,如何调用呢?
第一步,我这边搜搜内容会以 逗号 拼接传入。所以会先将逗号分割
第二步,在搜索词中加入自己本身,因为有些词经过 ik 分词后就没了... 这是个 bug
第三步,利用 AnalyzeRequestBuilder 对象获取 IK 分词后的返回值对象列表
第四步,优化分词结果,比如都为词,则保留全部;有词有字,则保留词;只有字,则保留字
核心实现代码如下:
/**
* 搜索内容分词
*/
protected List handlingSearchContent(String searchContent) {
List searchTermResultList = new ArrayList<>();
// 按逗号分割,获取搜索词列表
List 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 getIkAnalyzeSearchTerms(String searchContent) {
AnalyzeRequestBuilder ikRequest = new AnalyzeRequestBuilder(elasticsearchTemplate.getClient(),
AnalyzeAction.INSTANCE, SearchConstant.INDEX_NAME, searchContent);
ikRequest.setTokenizer(SearchConstant.TOKENIZER_IK_MAX);
List ikTokenList = ikRequest.execute().actionGet().getTokens();
// 循环赋值
List searchTermList = new ArrayList<>();
ikTokenList.forEach(ikToken -> {
searchTermList.add(ikToken.getTerm());
});
return handlingIkResultTerms(searchTermList);
}
/**
* 如果分词结果:洗发水(洗发、发水、洗、发、水)
* - 均为词,保留
* - 词 + 字,只保留词
* - 均为字,保留字
*/
private List handlingIkResultTerms(List 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 phraseList = new ArrayList<>();
searchTermList.forEach(term -> {
if (term.length() > SearchConstant.SEARCH_TERM_LENGTH) {
phraseList.add(term);
}
});
return phraseList;
}
return searchTermList;
}
三、搜索查询语句
构造内容枚举对象,罗列需要搜索的字段,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 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 resultPageBean = new PageBean<>();
resultPageBean.setPageNumber(pageNumber);
resultPageBean.setPageSize(pageSize);
// 构建搜索短语
String searchContent = contentSearchBean.getSearchContent();
List 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(" ContentServiceImpl.searchContent() [" + searchContent
+ "] DSL = " + searchQuery.getQuery().toString());
Page 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)即可。