互助交流论坛系统 Elasticsearch分布式搜索引擎

8 篇文章 0 订阅
5 篇文章 0 订阅

Elasticsearch入门

1. Elasticsearch简介

性能最好的搜索引擎

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

2. Elasticsearch术语

  • 索引(对应数据库,最新版本对应一张表)、类型(对应表,最新版本被废弃)、文档(表里一行,通常采用json)、字段(一列)
  • 集群(服务器组合在一起)、节点(集群中每台服务器)、分片(对索引的划分,提高并发能力)、副本(分片的备份,提高可用性)。
    通过ES搜索的数据必须要在ES中转存一份,某种角度来说它是一个数据库。

3. Elasticsearch使用

下载版本6.4.3

  • 安装、修改配置文件
    • elasticsearch.yml文件,修改cluster.name,path.data,path.logs
    • 配置环境变量
  • 安装中文分词插件(ES仅支持中文分词):https://github.com/medcl/elasticsearch-analysis-ik
    • ik插件安装到plugins文件夹下
  • 安装postman(提交html数据给ES)模拟web客户端
  • 启动ES:打开bin/elasticsearch.bat
    • 查看集群健康状态:curl -X GET “localhost:9200/_cat/health?v”
    • 查看节点:curl -X GET “localhost:9200/_cat/nodes?v”
    • 查看索引:curl -X GET “localhost:9200/_cat/indices?v”
    • 创建索引:curl -X PUT “localhost:9200/test”
    • 删除索引:curl -X DELETE “localhost:9200/test”
  • 使用postman查询
    在这里插入图片描述
    • 提交数据,PUT localhost:9200/test/_doc/1选择Body,raw,JSON
      • Test: 索引 _doc:占位 1:id
    • 搜索,GET localhost:9200/test/_search?q=title(/content):xxx(搜索title/content中包含xxx的)
    • 搜索时ES对关键词进行了分词
    • 通过请求体构造复杂搜索条件
      在这里插入图片描述

Spring整合Elasticsearch

1. 引入依赖

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

2. 配置Elasticsearch

  • cluster-name、cluster-nodes(集群的名字,节点)
  • Redis和Es底层都用到了Netty,有启动冲突。解决:在CommunityApplication类加入初始化方法进行配置。
# ElasticsearchProperties
# 配置集群名字
spring.data.elasticsearch.cluster-name=nowcoder
# 9200是http默认访问的端口 9300是tcp访问的端口
# 配置节点
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300

还需解决冲突
es底层和redis都基于Netty,两者在启用Netty时有冲突
在这里插入图片描述

// 管理bean的生命周期,会在构造器被调用之后执行,通常是初始化方法
@PostConstruct
public void init() {
    // 解决netty启动冲突问题
    // see Netty4Utils.setAvailableProcessors()
    System.setProperty("es.set.netty.runtime.available.processors", "false");
}
  • Spring Data Elasticsearch(调用API)
    • ElasticsearchTemplate(集成了Es的CRUD方法)
    • ElasticsearchRepository(接口,底层为ElasticsearchTemplate,用起来更方便)

3. 需求

把数据库存的帖子传入服务器里,用服务器搜索帖子

4. 代码实现

  • 把实体类和es之间建立关联
// 自动将实体数据与es映射
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
public class DiscussPost {

    @Id
    private int id;
    
    @Field(type = FieldType.Integer)
    private int userId;
    
    // analyze和searchAnalyzer是两个不同的分词器
    // 存储数据时采用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")
    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 double score;
   }
  • 定义repository接口
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
   
}
  • 创建测试类
    • 增删改查
@RunWith(SpringRunner.class)
@SpringBootTest
// 在测试代码中启用CommunityApplication作为测试类
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTests {

    @Autowired
    private DiscussPostMapper discussMapper;

    @Autowired
    private DiscussPostRepository discussRepository;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    // 插入一条数据
    @Test
    public void testInsert() {
        discussRepository.save(discussMapper.selectDiscussPostById(241));
        discussRepository.save(discussMapper.selectDiscussPostById(242));
        discussRepository.save(discussMapper.selectDiscussPostById(243));
    }

    // 插入多条数据
    @Test
    public void testInsertList() {
        discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100));
    }

    @Test
    public void testUpdate() {
        DiscussPost post = discussMapper.selectDiscussPostById(231);
        post.setContent("我是新人,使劲灌水.");
        discussRepository.save(post);
    }

    @Test
    public void testDelete() {
        // discussRepository.deleteById(231); 删除所有数据
        discussRepository.deleteAll();
    }
}
  • 搜索
@Test
public void testSearchByRepository() {
    SearchQuery searchQuery = new NativeSearchQueryBuilder()
            // 构造搜索条件:字段同时匹配
            .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬","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(0, 10))
            .withHighlightFields(
                    new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                    new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
            ).build();

    // elasticTemplate.queryForPage(searchQuery, class,SearchResultMapper) 
    // 底层获取得到了高亮显示的值, 但是没有返回.   
    Page<DiscussPost> page = discussRepository.search(searchQuery);
    System.out.println(page.getTotalElements());
    System.out.println(page.getTotalPages());
    System.out.println(page.getNumber());
    System.out.println(page.getSize());
    for (DiscussPost post : page) {
        System.out.println(post);
    }
}
  • 使用template方法进行搜索 进行高亮处理
@Test
public void testSearchByRepository() {
   SearchQuery searchQuery = new NativeSearchQueryBuilder()
           // 构造搜索条件:字段同时匹配
           .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬","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(0, 10))
           .withHighlightFields(
                   new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                   new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
           ).build();

   // elasticTemplate.queryForPage(searchQuery, class,SearchResultMapper)
   // 底层获取得到了高亮显示的值, 但是没有返回.
   Page<DiscussPost> page = discussRepository.search(searchQuery);
   System.out.println(page.getTotalElements());
   System.out.println(page.getTotalPages());
   System.out.println(page.getNumber());
   System.out.println(page.getSize());
   for (DiscussPost post : page) {
       System.out.println(post);
   }
}

@Test
public void testSearchByTemplate() {
   SearchQuery searchQuery = new NativeSearchQueryBuilder()
           .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "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(0, 10))
           .withHighlightFields(
                   new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                   new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
           ).build();
   Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
       @Override
       public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
           SearchHits hits = response.getHits();
           if (hits.getTotalHits() <= 0) {
               return null;
           }

           List<DiscussPost> list = new ArrayList<>();
           for (SearchHit hit : hits) {
               DiscussPost post = new DiscussPost();

               String id = hit.getSourceAsMap().get("id").toString();
               post.setId(Integer.valueOf(id));

               String userId = hit.getSourceAsMap().get("userId").toString();
               post.setUserId(Integer.valueOf(userId));

               String title = hit.getSourceAsMap().get("title").toString();
               post.setTitle(title);

               String content = hit.getSourceAsMap().get("content").toString();
               post.setContent(content);

               String status = hit.getSourceAsMap().get("status").toString();
               post.setStatus(Integer.valueOf(status));

               String createTime = hit.getSourceAsMap().get("createTime").toString();
               post.setCreateTime(new Date(Long.valueOf(createTime)));

               String commentCount = hit.getSourceAsMap().get("commentCount").toString();
               post.setCommentCount(Integer.valueOf(commentCount));

               // 处理高亮显示的结果
               HighlightField titleField = hit.getHighlightFields().get("title");
               if (titleField != null) {
                   post.setTitle(titleField.getFragments()[0].toString());
               }

               HighlightField contentField = hit.getHighlightFields().get("content");
               if (contentField != null) {
                   post.setContent(contentField.getFragments()[0].toString());
               }

               list.add(post);
           }

           return new AggregatedPageImpl(list, pageable,
                   hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
       }
   });

   System.out.println(page.getTotalElements());
   System.out.println(page.getTotalPages());
   System.out.println(page.getNumber());
   System.out.println(page.getSize());
   for (DiscussPost post : page) {
       System.out.println(post);
   }

}

开发社区搜索功能

1. 搜索服务

  • 将帖子保存至Elasticsearch服务器。
    • 对贴子实体类DiscussPost用注解进行相关配置
// 自动将实体数据与es映射
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
public class DiscussPost {

    @Id
    private int id;

    @Field(type = FieldType.Integer)
    private int userId;

    // analyze和searchAnalyzer是两个不同的分词器
    // 存储数据时采用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")
    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 double score;
}
  • 从Mybatis取数据存入
  • 在dao层创建DiscussPostRepository类,继承ElasticsearchRepository接口即可,它集成了CRUD方法
  • 从Elasticsearch服务器删除帖子。
  • 从Elasticsearch服务器搜索帖子。
    • Es可以在搜索到的词加标签,达到高亮显示
    • 利用elasticTemplate.queryForPage()查询

同步:有多个请求需要被处理,需要一个请求接着一个请求被处理,当前请求不处理完成,下一个请求不能开始。
异步:有多个请求需要被处理,处理请求a,就接收这个请求a并返回一个响应,并开始执行下一个请求b,当前请求a只返回了一个响应,因为结果还需要处理,等结果处理完成后就可以返回结果给对应的请求a。

2. 发布事件

  • 发布帖子时,将帖子异步的提交到Elasticsearch服务器。
    • 新建ElasticsearchService类,定义CRUD和搜索方法。
@Service
public class ElasticsearchService {

   @Autowired
   private DiscussPostRepository discussRepository;

   @Autowired
   private ElasticsearchTemplate elasticTemplate;

   /**
    * 向es服务器提交新的帖子
    */
   public void saveDiscussPost(DiscussPost post) {
       discussRepository.save(post);
   }

   /**
    * 删除帖子
    */
   public void deleteDiscussPost(int id) {
       discussRepository.deleteById(id);
   }

   /**
    * 分页:current是当前页
    */
   public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {
       // 构造查询条件
       SearchQuery searchQuery = 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(
                       // global标签中对<em>做了定义,<em>显示红色
                       new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                       new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
               ).build();
      return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
           @Override
           public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
               // 获取命中数据,做判断
               SearchHits hits = response.getHits();
               if (hits.getTotalHits() <= 0) {
                   return null;
               }
               
               // 实例化了一个集合,根据命中的实体将其返回
               List<DiscussPost> list = new ArrayList<>();
               for (SearchHit hit : hits) {
                   DiscussPost post = new DiscussPost();

                   String id = hit.getSourceAsMap().get("id").toString();
                   post.setId(Integer.valueOf(id));

                   String userId = hit.getSourceAsMap().get("userId").toString();
                   post.setUserId(Integer.valueOf(userId));

                   String title = hit.getSourceAsMap().get("title").toString();
                   post.setTitle(title);

                   String content = hit.getSourceAsMap().get("content").toString();
                   post.setContent(content);

                   String status = hit.getSourceAsMap().get("status").toString();
                   post.setStatus(Integer.valueOf(status));

                   String createTime = hit.getSourceAsMap().get("createTime").toString();
                   post.setCreateTime(new Date(Long.valueOf(createTime)));

                   String commentCount = hit.getSourceAsMap().get("commentCount").toString();
                   post.setCommentCount(Integer.valueOf(commentCount));

                   // 处理高亮显示的结果
                   HighlightField titleField = hit.getHighlightFields().get("title");
                   if (titleField != null) {
                       post.setTitle(titleField.getFragments()[0].toString());
                   }

                   HighlightField contentField = hit.getHighlightFields().get("content");
                   if (contentField != null) {
                       post.setContent(contentField.getFragments()[0].toString());
                   }

                   list.add(post);
               }

               return new AggregatedPageImpl(list, pageable,
                       hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
           }
       });
   }
}
  • 在DiscussPostController类发帖时,定义和触发发帖事件(Event、eventProducer.fireEvent(event))
  • 增加评论时,将帖子异步的提交到Elasticsearch服务器。
    • 在CommentController类发表评论时,定义和触发发帖事件(需要先定义发帖事件常量)
      // 触发发帖事件,把新发布的帖子存到es服务器
      Event event = new Event()
      .setTopic(TOPIC_PUBLISH)
      .setUserId(user.getId())
      .setEntityType(ENTITY_TYPE_POST)
      .setEntityId(post.getId());
      // 触发发帖事件
      eventProducer.fireEvent(event);
      - 在消费组件中增加一个方法,消费帖子发布事件。
    • 在EventConsumer类增加消费发帖事件的方法
    • 在事件中查询帖子,存到Es服务器
/**
 * 消费发帖事件
 */
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(ConsumerRecord record) {
    if (record == null || record.value() == null) {
        logger.error("消息的内容为空!");
    }

    Event event = JSONObject.parseObject(record.value().toString(), Event.class);
    if (event == null) {
        logger.error("消息格式错误!");
        return;
    }

    // 查询帖子,存进elasticsearch里
    DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
    elasticsearchService.saveDiscussPost(post);
}

3. 显示结果

  • 在控制器中处理搜索请求,在HTML上显示搜索结果。
    • 新建SearchController类处理搜索请求
    • 此时为GET请求,keyword的传入(search?keyword=xxx)
    • 修改index.html,表单提交路径,文本框name=“keyword”
    • 在search.html修改,遍历取到帖子。
// /search?keyword=xxx
@RequestMapping(path = "/search", method = RequestMethod.GET)
public String search(String keyword, Page page, Model model) {
    // 搜索帖子
    org.springframework.data.domain.Page<DiscussPost> searchResult =
            elasticsearchService.searchDiscussPost(keyword, page.getCurrent() - 1, page.getLimit());
    // 聚合数据
    List<Map<String, Object>> discussPosts = new ArrayList<>();
    if (searchResult != null) {
        for (DiscussPost post : searchResult) {
            Map<String, Object> map = new HashMap<>();
            // 帖子
            map.put("post", post);
            // 作者
            map.put("user", userService.findUserById(post.getUserId()));
            // 点赞数量
            map.put("likeCount", likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()));
            
            discussPosts.add(map);
        }
    }
    model.addAttribute("discussPosts", discussPosts);
    model.addAttribute("keyword", keyword);
    
    // 分页信息
    page.setPath("/search?keyword=" + keyword);
    page.setRows(searchResult == null ? 0 : (int) searchResult.getTotalElements());
    
    return "/site/search";
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值