比较详细的Springboot整合Elasticsearch使用ElasticsearchRestTemplate处理搜索结果的分段显示、高光显示

1. 前言

         前一段时间我写过一篇文章,也是关于Elasticsearch的,那篇文章使用的方法确实邪门,用的是ElasticsearchRepository的自动依据方法名返回搜索结果,比如在ElasticsearchRepository接口中定义一个名为findPostByTitleOrContent(),Elasticsearch就会返回内容或结果相关的帖子,但是分词、高光、分段全部失效了,只能返回全词匹配的结果,算是一个半成品,所以我改进了一下,研究了源码之后,在chatGPT的辅助下,我终于完美实现了所有的功能,下面我会从安装开始,详细的阐述一下Elasticsearch的用法,话不多说,直接开始。

2. 配置准备

2.1 安装Elasticsearch

        这里我使用的是7.15.2的版本,适配2.6.11版本的Springboot(因为我使用的是jdk1.8),如果需要更高版本则需要自己去官网找对应的版本号。

        这里附上下载地址Past Releases of Elastic Stack Software | Elastic,如果速度比较慢可以挂个梯子。

        下载完成以后就是按部就班的安装,安装完成以后可以顺便配置一下Elasticsearch的环境变量,方便后续的一些操做,可以用

curl -X GET "localhost:9200/_cat/health?v"

 检验是否安装成功,如果成功就会像下面这样,可以看到服务器的健康程度:

       

        确认成功安装就可以进入安装目录,打开elasticsearch.bat就可以启动Elasticsearch了,但是我想弄点骚操作,因为项目整合了Kafka以后每次想要运行项目都要先启动Kafka敲一长串命令,所以我想偷个懒,写个bat来自动执行,顺便也把执行elasticsearch.bat加进去一次性启动:

@echo off
cd /d D:\kafka\kafka_2.13-3.4.0
start /B bin\windows\zookeeper-server-start.bat config\zookeeper.properties
start /B bin\windows\kafka-server-start.bat config\server.properties
cd /d D:\elasticsearch\elasticsearch-7.15.2\bin
start /B elasticsearch.bat

        将上面的内容复制到txt文件中,参照我上面的目录修改自己的安装目录,然后修改文件后缀为.bat即可食用,注意: 要先把Kafka和Zookeeper添加到环境变量,否则可能不能运行。

        但是要像愉快的使用中文搜索,我们还需要安装ik分词:

直接上链接:Releases · medcl/elasticsearch-analysis-ik · GitHub

        选择7.15.2

        然后把下载好的ik放到elasticsearch的plugins目录下就可以了:

 

 

2.2 Springboot的配置

        首先注入依赖:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

        其次是配置application.properties:

#ElasticsearchProperties
spring.data.elasticsearch.repositories.enabled=true

3. 实体类的处理

        下面以实体类帖子为例,演示一下实体类的处理:

package com.newcoder.community.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
@Document(indexName = "discusspost", shards = 6 ,replicas = 3)
public class DiscussPost {
    // Elasticsearch的ID
    @Id
    private int id;

    // Elasticsearch映射数据类型
    @Field(type = FieldType.Integer)
    private int userId;
    // 这里的analyzer是分词器,searchAnalyzer指的是分词策略
    // 如果需要将某个属性作为关键字那么就需要加上分词器与分词策略
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String title;
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") 
    // 分词器analyzer
    private String content;
    @Field(type = FieldType.Integer)
    private int type;
    @Field(type = FieldType.Integer)
    private int status;
    @Field(type = FieldType.Date)
    private Date createTime;
    @Field(type = FieldType.Integer)
    private int commentCount;
    @Field(type = FieldType.Double)
    private int score;

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "DiscussPost{" +
                "id=" + id +
                ", userId=" + userId +
                ", title='" + title + '\'' +
                ", content='" + content + '\'' +
                ", type=" + type +
                ", status=" + status +
                ", createTime=" + createTime +
                ", commentCount=" + commentCount +
                ", commentCount=" + score +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public int getCommentCount() {
        return commentCount;
    }

    public void setCommentCount(int commentCount) {
        this.commentCount = commentCount;
    }
}

4. Dao层的处理

        主要是定义一个接口,用来声明需要存取的实体类,这里是DiscussPost

package com.newcoder.community.dao.elasticsearch;
import com.newcoder.community.entity.DiscussPost;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}

        声明完这个接口以后方便后续调用Elasticsearch的save()方法和delete()方法还有update()方法来对Elasticsearch数据库中的内容进行增删改操作。

5. Service层的处理

5.1 前期准备

        在这里我为了让方法可以同时返回帖子和帖子条数(因为调用.size()方法得到了错误的帖子条数,导致搜素结果大量缺失)新建了一个类来存储帖子和帖子条数:

package com.newcoder.community.entity;

import java.util.List;

public class SearchResult {
    private long rows;
    private List<DiscussPost> posts;

    public SearchResult(long rows, List<DiscussPost> posts) {
        this.rows = rows;
        this.posts = posts;
    }

    public long getRows() {
        return rows;
    }

    public void setRows(long rows) {
        this.rows = rows;
    }

    public List<DiscussPost> getPosts() {
        return posts;
    }

    public void setPosts(List<DiscussPost> posts) {
        this.posts = posts;
    }
}

 5.2 使用  ElasticsearchRestTemplate获得并处理搜索结果  

        因为ElasticsearchTemplate被弃用了,所以我选择使用的是ElasticsearchRestTemplate来完成构建Service层。

package com.newcoder.community.services;
import com.newcoder.community.dao.DiscussPostMapper;
import com.newcoder.community.dao.elasticsearch.DiscussPostRepository;
import com.newcoder.community.entity.DiscussPost;
import com.newcoder.community.entity.SearchResult;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Service
public class SearchService {
    @Autowired
    private DiscussPostRepository discussPostRepository;

    @Autowired
    private DiscussPostMapper discussPostMapper;
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /*
    每次项目重新部署就把mysql里的帖子全部导入elasticsearch
     */
    @PostConstruct
    public void init() {
        discussPostRepository.deleteAll();
        System.out.println("自动删除所有帖子");
        discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(0,0,discussPostMapper.selectDiscussPostRows(0)));
        System.out.println("自动注入所有帖子");
    }

    public SearchResult search(String keyword,  Pageable pageable) {
        List<DiscussPost> posts = new ArrayList<>();

        // 构建一个NativeSearchQuery并添加分页条件、排序条件以及高光区域
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
                .withPageable(pageable)
                .withSorts(
                        SortBuilders.fieldSort("type").order(SortOrder.DESC),
                        SortBuilders.fieldSort("score").order(SortOrder.DESC),
                        SortBuilders.fieldSort("createTime").order(SortOrder.DESC)
                )
                .withHighlightFields(
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
                )
                .build();
        // 调用ElasticsearchRestTemplate的search()方法进行查询
        // 使用SearchHits存储搜索结果
        SearchHits<DiscussPost> searchHits = elasticsearchRestTemplate.search(query, DiscussPost.class);
        long rows = searchHits.getTotalHits();
        
        // 遍历搜索结果设置帖子的各个参数
        if (searchHits.getTotalHits() != 0) {
            for (SearchHit<DiscussPost> searchHit : searchHits) {
                DiscussPost post = new DiscussPost();

                int id = searchHit.getContent().getId();
                post.setId(id);

                int userId = searchHit.getContent().getUserId();
                post.setUserId(userId);

                String title = searchHit.getContent().getTitle();
                post.setTitle(title);

                String content = searchHit.getContent().getContent();
                post.setContent(content);

                int status = searchHit.getContent().getStatus();
                post.setStatus(status);

                int type = searchHit.getContent().getType();
                post.setType(type);

                Date createTime = searchHit.getContent().getCreateTime();
                post.setCreateTime(createTime);

                int commentCount = searchHit.getContent().getCommentCount();
                post.setCommentCount(commentCount);
                
                // 获得刚刚构建的高光区域,填到帖子的内容和标题上
                List<String> contentField = searchHit.getHighlightFields().get("content");
                if (contentField != null) {
                    post.setContent(contentField.get(0));
                }

                List<String> titleField = searchHit.getHighlightFields().get("title");
                if (titleField != null) {
                    post.setTitle(titleField.get(0));
                }
                posts.add(post);
            }
        }

        return new SearchResult(rows, posts);
    }

    public void saveDiscussPost(DiscussPost discussPost) {
         discussPostRepository.save(discussPost);
    }

    public void deleteDiscussPost(DiscussPost discussPost) {
        discussPostRepository.delete(discussPost);
    }

}

        值得一提的是,貌似在7.x之后Elasticsearch就把getHighlightFields().get()方法返回类型设置为了List,但是很智能的将分段和高光全部集成了,所以还是很简单的。

6. Controller层

        这一层比较简单,没啥好说的

package com.newcoder.community.controller;

import com.newcoder.community.entity.DiscussPost;
import com.newcoder.community.entity.Page;
import com.newcoder.community.entity.User;
import com.newcoder.community.services.LikeService;
import com.newcoder.community.services.SearchService;
import com.newcoder.community.services.UserService;
import com.newcoder.community.util.CommunityConstant;
import com.newcoder.community.util.CommunityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class SearchController implements CommunityConstant {
    @Autowired
    private SearchService searchService;

    @Autowired
    private UserService userService;

    @Autowired
    private LikeService likeService;

    @RequestMapping(path = "/search", method = RequestMethod.GET)
    public String search(String keyword, Model model, Page page) {
        
        Sort sort = CommunityUtil.getSearchSort();
        Pageable pageable = PageRequest.of(page.getCurrent() - 1, page.getLimit(),sort);
        page.setPath("/search?keyword=" + keyword);
        long rows = searchService.search(keyword, pageable).getRows();
        page.setRows((int) rows);

        List<DiscussPost> list = searchService.search(keyword, pageable).getPosts();
        List<Map<String,Object>> discussPosts = new ArrayList<>();
        if (list != null) {
            for (DiscussPost discussPost : list) {
                Map<String,Object> map = new HashMap<>();
                map.put("post",discussPost);

                User user = userService.findUserById(discussPost.getUserId());
                map.put("user",user);

                long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPost.getId());
                map.put("likeCount",likeCount);
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts",discussPosts);
        model.addAttribute("keyword",keyword);
        model.addAttribute("rows",rows);
        return "site/search";
    }

}

7. 最终效果

7.1 单一关键词

7.2 多关键词

      

8. 后记

         这篇文章是小白学习路上的一些记录,如果能帮助到你的话,请帮我点个赞!

  • 13
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值