美文网首页Java学习笔记Java 杂谈
年终总结:Spring Boot 2.0 整合 ES 5 文章内

年终总结:Spring Boot 2.0 整合 ES 5 文章内

作者: Java高级架构狮 | 来源:发表于2018-12-10 15:49 被阅读2次

    一、文章内容搜索思路

    上一篇讲了在怎么在 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 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)即可。

    相关文章

      网友评论

        本文标题:年终总结:Spring Boot 2.0 整合 ES 5 文章内

        本文链接:https://www.haomeiwen.com/subject/srxdhqtx.html