欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Boot2.0整合ES5实现文章内容搜索实战

程序员文章站 2023-12-17 20:38:34
一、文章内容搜索思路 上一篇讲了在怎么在 spring boot 2.0 上整合 es 5 ,这一篇聊聊具体实战。简单讲下如何实现文章、问答这些内容搜索的具体实现。实现思...

一、文章内容搜索思路

上一篇讲了在怎么在 spring boot 2.0 上整合 es 5 ,这一篇聊聊具体实战。简单讲下如何实现文章、问答这些内容搜索的具体实现。实现思路很简单:

  1. 基于「短语匹配」并设置最小匹配权重值
  2. 哪来的短语,利用 ik 分词器分词
  3. 基于 fiter 实现筛选
  4. 基于 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)即可。

六、小结

这个思路比较简单。希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: