Spring Boot2.0整合ES5实现文章内容搜索实战
一、文章内容搜索思路
上一篇讲了在怎么在 spring boot 2.0 上整合 es 5 ,这一篇聊聊具体实战。简单讲下如何实现文章、问答这些内容搜索的具体实现。实现思路很简单:
- 基于「短语匹配」并设置最小匹配权重值
- 哪来的短语,利用 ik 分词器分词
- 基于 fiter 实现筛选
- 基于 pageable 实现分页排序
这里直接调用搜索的话,容易搜出不尽人意的东西。因为内容搜索关注内容的连接性。所以这里处理方法比较 low ,希望多交流一起实现更好的搜索方法。就是通过分词得到很多短语,然后利用短语进行短语精准匹配。
es 安装 ik 分词器插件很简单。第一步,在下载对应版本 。第二步,在 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; @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); }
四、筛选条件
搜到东西不止,有时候需求是这样的。需要在某个品类下搜索,比如电商需要在某个 品牌 下搜索商品。那么需要构造一些 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)即可。
六、小结
这个思路比较简单。希望对大家的学习有所帮助,也希望大家多多支持。