一、文章内容搜索思路
上一篇讲了在怎么在 Spring Boot 2.0 上整合 ES 5 ,这一篇聊聊具体实战。简单讲下如何实现文章、问答这些内容搜索的具体实现。实现思路很简单:
- 基于「短语匹配」并设置最小匹配权重值
- 哪来的短语,利用 IK 分词器分词
- 基于 Fiter 实现筛选
- 基于 Pageable 实现分页排序
这里直接调用搜索的话,容易搜出不尽人意的东西。因为内容搜索关注内容的连接性。所以这里处理方法比较 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 分词后的返回值对象列表
第四步,优化分词结果,比如都为词,则保留全部;有词有字,则保留词;只有字,则保留字
核心实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
/**
* 搜索内容分词
*/
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 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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。核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 构造查询条件
*/
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 方法添加过滤。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* 构建筛选条件
*/
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 中搜索呢?具体实现代码很简单:
1
2
3
4
|
typeFilterBuilder
.should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, 1 )
.should(QueryBuilders.matchQuery(SearchConstant.TYPE_NAME, 2 )
.lenient( true ));
|
通过链式表达式,两个 should 实现或,即 SQL 对应的 OR 语句。通过两个 BoolQueryBuilder 实现与,即 SQL 对应的 AND 语句。
五、分页、排序条件
分页排序代码就很简单了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
@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)即可。
六、小结
这个思路比较简单。希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://zhuanlan.zhihu.com/p/32562131