8.Elasticsearch,分布式搜索引擎
1.Elasticsearch入门
Elasticsearch简介
- 一个分布式的、Restful风格的搜索引擎。
- 支持对各种类型的数据的检索。
- 搜索速度快,可以提供实时的搜索服务。
- 便于水平扩展,每秒可以处理PB级海量数据
Elasticsearch术语解释
- 索引:相当于数据库中的database (改版后作为table)
- 类型:相当于数据库中的table (不再使用)
- 文档:相当于数据库中的一行数据,数据结构为JSON
- 字段:相当于数据库中的一列
- 集群:分布式部署,提高性能
- 节点:集群中的每一台服务器
- 分片:对一个索引的进一步划分存储,提高并发处理能力
- 副本:对分片的备份,提高可用性
- 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修改,遍历取到帖子。
<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);