Spring Boot 2.0 整合 ES 5 文章内容搜索实战


声明:本文转载自https://my.oschina.net/u/3677020/blog/1596017,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

出自:Spring For All 社区 《Spring Boot 2.0 整合 ES 5 文章内容搜索实战http://www.spring4all.com/article/396

本章内容

  1. 文章内容搜索思路
  2. 搜索内容分词
  3. 搜索查询语句
  4. 筛选条件
  5. 分页、排序条件
  6. 小结

阅读时间:8 分钟

摘录:打算起手不凡写出鸿篇巨作的,往往坚持不了完成第一章节

一、文章内容搜索思路

上一篇讲了在怎么在 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 分词后的返回值对象列表

第四步,优化分词结果,比如都为词,则保留全部;有词有字,则保留词;只有字,则保留字

核心实现代码如下:

    /**      * 搜索内容分词      */     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)即可。

六、小结

这个思路比较简单。如果大家有更吊的实现方法,欢迎交流讨论。

更多 ES 文章,关注即可得系列教程文章哦!

本文发表于2017年12月28日 12:32
(c)注:本文转载自https://my.oschina.net/u/3677020/blog/1596017,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1904 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1