8.Elasticsearch,分布式搜索引擎

1.Elasticsearch入门

Elasticsearch简介

  • 一个分布式的、Restful风格的搜索引擎。
  • 支持对各种类型的数据的检索。
  • 搜索速度快,可以提供实时的搜索服务。
  • 便于水平扩展,每秒可以处理PB级海量数据

Elasticsearch术语解释

  • 索引:相当于数据库中的database (改版后作为table)
  • 类型:相当于数据库中的table (不再使用)
  • 文档:相当于数据库中的一行数据,数据结构为JSON
  • 字段:相当于数据库中的一列
Elasticsearch6.0以后开始逐步废除类型的概念,索引的含义中也包括了类型。
  • 集群:分布式部署,提高性能
  • 节点:集群中的每一台服务器
  • 分片:对一个索引的进一步划分存储,提高并发处理能力
  • 副本:对分片的备份,提高可用性
  • Elasticsearch相关链接:https://www.elastic.co/

Elasticsearch选择下载6.4.3版本和SpringBoot兼容

Elasticsearch安装

(1)下载链接:https://www.elastic.co/cn/downloads/elasticsearch

也可以通过修改以下路径中版本号,直接get安装包

https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.3.0-linux-x86_64.tar.gz

(2)解压下载的压缩包,文件位置config/elasticsearch.yml,修改配置

(3)配置环境变量, 把bin路径放在Path里

(4)安装中文分词插件(分词插件要和ES版本相同)

下载链接:https://github.com/medcl/elasticsearch-analysis-ik/releases

将安装包解压到elasticsearch-8.11.3\plugins\ik下

 ik\config\IKAnalyzer.cfg.xml可以自己配置新词

 (5)Elasticsearch相关使用

启动

bin文件夹下双击elasticsearch.bat

出现问题:

warning: ignoring JAVA_HOME=D:\jdk\jdk1.8; using bundled JDK

百度了,好像是我这个ES版本(8.11.3)太高了。要注意ES和SpringBoot、jdk的版本对应关系

Elasticsearch 和 JVM支持一览表: https://www.elastic.co/cn/support/matrix#matrix_jvm

目前我的版本为SpringBoot 2.7.12 + ES 7.17.3 + jdk1.8

如果elasticsearch.bat运行成功,访问localhost:9200会出现以下结果:

安装Postman模拟网页访问

  • 增加索引

  •  查询索引

  • 删除索引

  •  提交数据
//test:索引  _doc:固定格式  1:id号 然后在请求body中写数据
PUT localhost:9200/test/_doc/1   

  •  搜索演示

先插入几条数据

2.Spring整合Elasticsearch

(1)引入依赖

  spring-boot-starter-data-elasticsearch

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

(2)配置Elasticsearch

  cluster-name、cluster-nodes

启动依赖冲突:

问题原因:Redis底层使用了Netty,Elasticsearch也用了Netty,当被注册两次就会报错

Elasticsearch中注册Netty前会判断有无一个参数,如果有就不注册

解决Netty冲突问题

@SpringBootApplication
public class CommunityApplication {
    @PostConstruct
    public void init() {
        // 解决netty启动冲突问题
        // see Netty4Utils.setAvailableProcessors()
        System.setProperty("es.set.netty.runtime.available.processors", "false");
    }
    public static void main(String[] args) {
        SpringApplication.run(CommunityApplication.class, args);
    }

}

写ES配置类(不需要写application.properties,注入config即可)

@Configuration
public class ElasticSearchClientConfig {
    //配置RestHighLevelClient依赖到spring容器中待用
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        //绑定本机,端口,协议,如果是ES集群,就配置多个
                        new HttpHost("192.168.223.129", 9200, "http")));
        return client;
    }
}

(3)Spring Data Elasticsearch

  ElasticsearchTemplate

  ElasticsearchRepository

对Discusspost类做处理

  • analyzer = "ik_max_word" 分析的时候尽量拆分出多的词

  • searchAnalyzer = "ik_smart" 查找的时候智能拆分出少点的词

 

@Data
@Document(indexName = "discusspost", shards = 6, replicas = 3)
public class DiscussPost {
    @Id
    private int id;
    @Field(type = FieldType.Integer)
    private int userId;
    @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")
    private String content;
    @Field(type = FieldType.Integer)
    private int type;
    @Field(type = FieldType.Integer)
    private int status;
    @Field(type = FieldType.Date,format = DateFormat.basic_date)
    private Date createTime;
    @Field(type = FieldType.Integer)
    private int commentCount;
    @Field(type = FieldType.Double)
    private double score;
}

3.开发社区搜索功能

搜索服务#
  • 将帖子保存至Elasticsearch服务器。
    • 对贴子实体类DiscussPost用注解进行相关配置
    • 从Mybatis取数据存入
    • 在dao层创建DiscussPostRepository类,继承ElasticsearchRepository接口即可,它集成了CRUD方法
  • 从Elasticsearch服务器删除帖子。
  • 从Elasticsearch服务器搜索帖子。
    • Es可以在搜索到的词加标签,达到高亮显示
    • 利用elasticTemplate.queryForPage()查询
发布事件#
  • 发布帖子时,将帖子异步提交到Elasticsearch服务器。
    • 新建ElasticsearchService类,定义CRUD和搜索方法。
    • 在DiscussPostController类发帖时,定义和触发发帖事件(Event、eventProducer.fireEvent(event))
  • 增加评论时,将帖子异步的提交到Elasticsearch服务器。
    • 在CommentController类发表评论时,定义和触发发帖事件
  • 在消费组件中增加一个方法,消费帖子发布事件。
    • 在EventConsumer类增加消费发帖事件的方法
    • 在事件中查询帖子,存到Es服务器
显示结果#
  • 在控制器中处理搜索请求,在HTML上显示搜索结果。
    • 新建SearchController类处理搜索请求
    • 此时为GET请求,keyword的传入(search?keyword=xxx)
    • 修改index.html,表单提交路径,文本框name="keyword"
    • 在search.html修改,遍历取到帖子。
 (1) 在discusspostmapper.xml中的insertDiscussPost 增加keyproperty=“id” 就是自增id
<insert id="insertDiscussPost" parameterType="DiscussPost" keyProperty="id">
     insert into discuss_post(<include refid="insertFields"></include>)
     values (#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>

(2) 编写elasticsearchService

RestHighLevelClient 是es的原生高客户端,ElasticsearchRestTemplate 是es基于Spring集成 用法和JPA类似,
如果是有用Spring或者SpringBoot推荐还是ElasticsearchRestTemplate

源码的ElasticsearchTemplate在ES7.x无法使用,现在更换成ElasticsearchRestTemplate

@Service
public class ElasticsearchService {
    @Autowired
    private DiscussPostRepository discussPostRepository;
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

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

    public void deleteDiscussPost(int id){
        discussPostRepository.deleteById(id);
    }
    //multiMatchQuery
     /* 从多个列中查询包含搜索关键字分词后的字段的数据:
        如中华华为,分词后的字段大概有:中华、华为、华,
        会从"title", "content"两个列搜索包含分词后的字段的数据*/
    public SearchResult searchDiscussPosts(String keyword, int current, int limit) {
        // 构建一个NativeSearchQuery并添加分页条件、排序条件以及高光区域
        NativeSearchQuery nativeSearchQuery=new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                .withPageable(PageRequest.of(current, limit))
                .withHighlightFields(//高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
                ).build();
        //queryForPage()过时了,可以用search先查询到结果,再自行包装成对象,然后返回
        // 使用SearchHits存储搜索结果
        SearchHits<DiscussPost> searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, DiscussPost.class);
        long total = searchHits.getTotalHits();

        // 遍历搜索结果设置帖子的各个参数
        List<DiscussPost> list = new ArrayList<>();
        if (searchHits.getTotalHits() != 0) {
            for (SearchHit<DiscussPost> searchHit : searchHits) {
                DiscussPost post = new DiscussPost();

                post.setId(searchHit.getContent().getId());
                post.setUserId(searchHit.getContent().getUserId());
                post.setTitle(searchHit.getContent().getTitle());
                post.setContent(searchHit.getContent().getContent());
                post.setStatus(searchHit.getContent().getStatus());
                post.setType(searchHit.getContent().getType());
                String createTime = searchHit.getContent().getCreateTime().toString();
                SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
                Date date = null;
                try {
                    date = (Date) sdf.parse(createTime);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
                post.setCreateTime(date);
                post.setCommentCount(searchHit.getContent().getCommentCount());

                // 获得刚刚构建的高光区域,填到帖子的内容和标题上
                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));
                }
                list.add(post);
            }
        }
        return new SearchResult(total,list);
    }
}

(3)处理DiscussPostController.addDiscussPost

//发布异步处理,在ES中增加帖子
//发布帖子-> 发送帖子id到队列-> 队列处理 放入es中
Event event=new Event();
event.setTopic(TOPIC_PUBLISH);//主题为”发布“
event.setUserId(user.getId());
event.setEntityId(discussPost.getId());
event.setEntityType(ENTITY_TYPE_POST);
eventProducer.fireEvent(event);

处理CommentController.addComment

//触发发帖事件 es
if(comment.getEntityType()==ENTITY_TYPE_POST){//给帖子评论
     event=new Event();
     event.setTopic(TOPIC_PUBLISH);
     event.setUserId(comment.getUserId());
     event.setEntityType(ENTITY_TYPE_POST);
     event.setEntityId(discussPostId);
     eventProducer.fireEvent(event);
}

(4)EventConsumer增加一个方法:消费发帖事件

    //发布帖子-> 发送帖子id到队列-> 队列处理 放入es中
    @KafkaListener(topics = {TOPIC_PUBLISH})
    public void handlePublishMessage(ConsumerRecord record){
        if(record==null||record.value()==null){
            log.error("消息的内容为空!");
            return;
        }
        //接收到的消息事件
        Event event= JSONObject.parseObject(record.value().toString(),Event.class);
        if(event==null){
            log.error("消息格式错误!");
            return;
        }
        //查询到该帖子
        DiscussPost discussPost=discussPostService.findDiscussPostById(event.getEntityId());
        //在es中存数据
        elasticsearchService.saveDiscussPost(discussPost);
    }

(5)SearchController

@Slf4j
@Controller
public class SearchController {
    @Autowired
    private ElasticsearchService elasticsearchService;
    @Autowired
    private UserService userService;
    @Autowired
    private LikeService likeService;
    @RequestMapping(path = "/search", method = RequestMethod.GET)
    public String search(String keyword, Model model, Page page) {

        page.setPath("/search?keyword=" + keyword);
        long rows = elasticsearchService.searchDiscussPosts(keyword, page.getCurrent() - 1, page.getLimit()).getRows();
        page.setRows((int) rows);

        List<DiscussPost> list = elasticsearchService.searchDiscussPosts(keyword, page.getCurrent() - 1, page.getLimit()).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";
    }
}

(6) 改写index.html

<!-- 搜索 -->
<form class="form-inline my-2 my-lg-0" method="get" th:action="@{/search}">
      <input class="form-control mr-sm-2" type="search" aria-label="Search" name="keyword" th:value="${keyword}"/>
      <button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button>
</form>

改写search.html

      <!-- 内容 -->
        <div class="main">
            <div class="container">
                <h6><b class="square"></b> 相关帖子</h6>
                <!-- 帖子列表 -->
                <ul class="list-unstyled mt-4">
                    <li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
                        <img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;">
                        <div class="media-body">
                            <h6 class="mt-0 mb-3">
                                <a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战<em>春招</em>,面试刷题跟他复习,一个月全搞定!</a>
                            </h6>
                            <div class="mb-3" th:utext="${map.post.content}">
                                金三银四的金三已经到了,你还沉浸在过年的喜悦中吗? 如果是,那我要让你清醒一下了:目前大部分公司已经开启了内推,正式网申也将在3月份陆续开始,金三银四,<em>春招</em>的求职黄金时期已经来啦!!! 再不准备,作为19应届生的你可能就找不到工作了。。。作为20届实习生的你可能就找不到实习了。。。 现阶段时间紧,任务重,能做到短时间内快速提升的也就只有算法了, 那么算法要怎么复习?重点在哪里?常见笔试面试算法题型和解题思路以及最优代码是怎样的? 跟左程云老师学算法,不仅能解决以上所有问题,还能在短时间内得到最大程度的提升!!!
                            </div>
                            <div class="text-muted font-size-12">
                                <u class="mr-3" th:utext="${map.user.username}">寒江雪</u>
                                发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
                                <ul class="d-inline float-right">
                                    <li class="d-inline ml-2"><i th:text="${map.likeCount}">11</i></li>
                                    <li class="d-inline ml-2">|</li>
                                    <li class="d-inline ml-2">回复 <i th:text="${map.post.commentCount}">7</i></li>
                                </ul>
                            </div>
                        </div>
                    </li>
                </ul>
                <!-- 分页 -->
                <nav class="mt-5" th:replace="index::pagination">
                    <ul class="pagination justify-content-center">
                        <li class="page-item"><a class="page-link" href="#">首页</a></li>
                        <li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
                        <li class="page-item active"><a class="page-link" href="#">1</a></li>
                        <li class="page-item"><a class="page-link" href="#">2</a></li>
                        <li class="page-item"><a class="page-link" href="#">3</a></li>
                        <li class="page-item"><a class="page-link" href="#">4</a></li>
                        <li class="page-item"><a class="page-link" href="#">5</a></li>
                        <li class="page-item"><a class="page-link" href="#">下一页</a></li>
                        <li class="page-item"><a class="page-link" href="#">末页</a></li>
                    </ul>
                </nav>
            </div>
        </div>

单一关键词:

 多关键词:

 这个搜索检索的就是ES中的discussPost,如果想要检索到数据库中的,需要先遍历数据库discussPost,然后插入ES

elasticsearchService.saveDiscussPost(discussPost);

 

posted @ 2024-01-03 18:19  壹索007  阅读(191)  评论(0编辑  收藏  举报