【牛客讨论区】第六章:Elasticsearch
目录
- 1. Elasticsearch入门
- 2. Spring整合Elasticsearch
- 2.1 springboot 版本问题
- 2.2 整合Elasticsearch
- 3. 开发社区搜索功能
1. Elasticsearch入门
Elasticsearch简介
- 一个分布式的、Restful风格的搜索引擎。
- 支持对各种类型的数据的检索。
- 搜索速度快,可以提供实时的搜索服务。
- 便于水平扩展,每秒可以处理PB级海量数据。
Elasticsearch术语
- 索引、类型、文档、字段。
- 集群、节点、分片、副本。
https://www.elastic.co
https://www.getpostman.com
【安装】
解压 elasticsearch-6.4.3.zip 即可
【修改配置文件 】
E:\elasticsearch-6.4.3\config 下的 elasticsearch.yml
17行
cluster.name: nowcoder33行
path.data: d:\work\data\elasticsearch-6.4.3\data37行
path.logs: d:\work\data\elasticsearch-6.4.3\logs【配置环境变量】
将 bin 目录 E:\elasticsearch-6.4.3\bin 配到 path 中
【安装中文分词插件】
注意:必须将elasticsearch-analysis-ik-6.4.3.zip 解压到固定的目录下
E:\elasticsearch-6.4.3\plugins 下新建 ik 目录,解压到 ik 目录下即可
【安装postman】
【启动 elasticsearch】
Windows下,双击 E:\elasticsearch-6.4.3\bin 下的 elasticsearch.bat
打开一个 cmd,访问 elasticsearch 健康状况
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 访问 es】
发送 GET 请求:查看索引
localhost:9200/_cat/indices?v
发送 PUT 请求:建立索引
localhost:9200/test
发送 DELETE 请求:删除索引
localhost:9200/test
【向 es 提交数据】
test 是索引,也是表名,不存在的话会自动创建,_doc是占位的,1是 id,在 body 中写数据,以 JSON 方式传输
点击 send,返回结果:
{"_index": "test","_type": "_doc","_id": "1","_version": 1,"result": "created","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 0,"_primary_term": 1 }【查看该条数据】
发送 GET 请求:localhost:9200/test/_doc/1
【删除该数据】
发送 DELETE 请求:localhost:9200/test/_doc/1
【实现搜索】
先提交几条数据
PUT : localhost:9200/test/_doc/1
{"title":"互联网求职","content":"寻求一份运维的岗位" }PUT : localhost:9200/test/_doc/2
{"title":"互联网招聘","content":"招一名资深程序员" }PUT : localhost:9200/test/_doc/3
{"title":"实习生推荐","content":"本人在一家互联网公司任职,可推荐实习开发岗位!" }搜索:
不加条件的搜索
GET:localhost:9200/test/_search
加条件的搜索
GET:localhost:9200/test/_search?q=title:互联网
localhost:9200/test/_search?q=content:运维实习
搜索引擎会将 “运维实习” 分词成 “运维”和 “实习”,分别去搜索。
【更加复杂的搜索】
地址栏写不下,可以在 body 中写,fields 表示要在哪些字段中进行搜索
2. Spring整合Elasticsearch
2.1 springboot 版本问题
【问题】
我的 springboot 版本是 2.3.7.RELEASE,需要降到 2.1.5.RELEASE,不然和 Elasticsearch 版本不兼容,会产生一系列的问题
【解决】
将 pom.xml 中所有的 2.3.7.RELEASE 都改成 2.1.5.RELEASE,然后点击IDEA 的 “File”–“Invalidate Caches” ,重新启动即可
2.2 整合Elasticsearch
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>application.properties
# ElasticsearchProperties spring.data.elasticsearch.cluster-name=nowcoder spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300【解决冲突】
Redis 和 elasticsearch 底层都是基于 Netty,所以二者启动时会有冲突,解决如下:
CommunityApplication 类的 main 方法之前写个方法
@PostConstruct public void init() {//解决Netty启动冲突//详见 Netty4Utils 类,设置为falseSystem.setProperty("es.set.netty.runtime.available.processors", "false"); }因为我们是将帖子存到 es 中实现搜索功能,所以需要通过注解配置一下 DiscussPost 类
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3) public class DiscussPost {@Idprivate int id;@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")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;dao 包下新建子包 elasticsearch
新建接口
新建测试类
最终版
package com.nowcoder.community;import com.nowcoder.community.dao.DiscussPostMapper; import com.nowcoder.community.dao.elasticsearch.DiscussPostRepository; import com.nowcoder.community.entity.DiscussPost; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; import org.springframework.data.elasticsearch.core.SearchResultMapper; import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage; import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.SearchQuery; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner;import javax.annotation.Resource; import java.util.ArrayList; import java.util.Date; import java.util.List;@RunWith(SpringRunner.class) @SpringBootTest @ContextConfiguration(classes = CommunityApplication.class) public class ElasticsearchTests {@Resourceprivate DiscussPostMapper discussPostMapper;@Resourceprivate DiscussPostRepository discussPostRepository;//DiscussPostRepository 功能有限,因此还需要 ElasticsearchTemplate@Resourceprivate ElasticsearchTemplate elasticTemplate;@Testpublic void testInsert() { //插入一条数据discussPostRepository.save(discussPostMapper.selectDiscussById(241));discussPostRepository.save(discussPostMapper.selectDiscussById(242));discussPostRepository.save(discussPostMapper.selectDiscussById(243));}@Testpublic void testInsertList() { //插入多条数据。为后面的搜索功能做准备discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(101, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(102, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(103, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(112, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(131, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(132, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(133, 0, 100));discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(134, 0, 100));}@Testpublic void testUpdate() { //所谓修改就是save一下,覆盖原来的DiscussPost post = discussPostMapper.selectDiscussById(231);post.setContent("我是新人,使劲灌水...");discussPostRepository.save(post);}@Testpublic void testDelete() {discussPostRepository.deleteById(231); //删一条数据//discussPostRepository.deleteAll();//删除所有数据}@Testpublic void testSearchByRepository() {SearchQuery searchQuery = new NativeSearchQueryBuilder()//搜索条件 从title和content中搜 互联网寒冬.withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content"))//排序条件.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) //优先按照type降序.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 = discussPostRepository.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);}}@Testpublic void testSearchByTemplate() {SearchQuery searchQuery = new NativeSearchQueryBuilder()//搜索条件 从title和content中搜 互联网寒冬.withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content"))//排序条件.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) //优先按照type降序.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() {@Overridepublic <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);}} }启动 zookeeper、kafka、Elasticsearch,执行测试方法 testInsert()
【踩坑】
这里执行了好多次,每次都出现 java: 程序包org.junit.jupiter.api不存在
原因是我们降低 springboot 版本之后,Junit 的版本也由 5 降低为 4,org.junit.jupiter.api 是 Junit5 的,自然就找不到了,所以我们需要按照 Junit4 的语法进行测试,org.junit.Test 是Junit4 的包
方法:
将所有测试类中的 import org.junit.jupiter.api.Test; 改成 import org.junit.Test;在所有测试类的类名前写这三个注解(BlockingQueueTests不用管,因为他是用main方法测试的)
如果还有错,重启一下 elasticsearch
使用 postman 查看是否插入成功:
3. 开发社区搜索功能
搜索服务
- 将帖子保存至Elasticsearch服务器。
- 从Elasticsearch服务器删除帖子。
- 从Elasticsearch服务器搜索帖子。
发布事件
- 发布帖子时,将帖子异步的提交到Elasticsearch服务器。
- 增加评论时,将帖子异步的提交到Elasticsearch服务器。
- 在消费组件中增加一个方法,消费帖子发布事件。
显示结果
- 在控制器中处理搜索请求,在HTML上显示搜索结果。
解决一个遗漏的小问题:
discusspost-mapper.xml,加上 keyProperty
service层
package com.nowcoder.community.service;import com.nowcoder.community.dao.elasticsearch.DiscussPostRepository; import com.nowcoder.community.entity.DiscussPost; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; import org.springframework.data.elasticsearch.core.SearchResultMapper; import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage; import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.SearchQuery; import org.springframework.stereotype.Service;import javax.annotation.Resource; import java.util.ArrayList; import java.util.Date; import java.util.List;@Service public class ElasticsearchService {@Resourceprivate DiscussPostRepository discussRepository;@Resourceprivate ElasticsearchTemplate elasticTemplate;public void saveDiscussPost(DiscussPost post) {discussRepository.save(post);}public void deleteDiscussPost(int id) {discussRepository.deleteById(id);}public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {SearchQuery searchQuery = new NativeSearchQueryBuilder()//搜索条件 从title和content中搜 互联网寒冬.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))//排序条件.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)) //优先按照type降序.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();return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {@Overridepublic <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());}});}}controller 层
CommunityConstant
新增常量
DiscussPostController 完善 addDiscussPost() 方法
@Resource private EventProducer eventProducer;@PostMapping("/add") @ResponseBody public String addDiscussPost(String title, String content) {User user = hostHolder.getUser();if (user == null) {return CommunityUtil.getJSONString(403, "你还没有登录哦!");}DiscussPost discussPost = new DiscussPost();discussPost.setUserId(user.getId());discussPost.setTitle(title);discussPost.setContent(content);discussPost.setCreateTime(new Date());discussPostService.addDiscussPost(discussPost);//触发发帖事件Event event = new Event().setTopic(TOPIC_PUBLISH).setUserId(user.getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(discussPost.getId());eventProducer.fireEvent(event);//报错的情况,将来统一处理return CommunityUtil.getJSONString(0, "发布成功!"); }CommentController
addComment 方法,在 return 之前,加一段逻辑:
某个帖子新增评论之后,需要重新上传 es 服务器,覆盖掉旧的帖子
EventConsumer
增加方法
@Resource private DiscussPostService discussPostService;@Resource private ElasticsearchService elasticsearchService;//消费发帖事件 @KafkaListener(topics = {TOPIC_PUBLISH}) public void handlePublishMessage(ConsumerRecord record) {if (record == null || record.value() == null) {logger.error("消息的内容为空!");return;}Event event = JSONObject.parseObject(record.value().toString(), Event.class);if (event == null) {logger.error("消息格式错误!");return;}DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());elasticsearchService.saveDiscussPost(post); }新建controller
package com.nowcoder.community.controller;import com.nowcoder.community.entity.DiscussPost; import com.nowcoder.community.entity.Page; import com.nowcoder.community.service.ElasticsearchService; import com.nowcoder.community.service.LikeService; import com.nowcoder.community.service.UserService; import com.nowcoder.community.util.CommunityConstant; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping;import javax.annotation.Resource; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;@Controller public class SearchController implements CommunityConstant {@Resourceprivate ElasticsearchService elasticsearchService;@Resourceprivate UserService userService;@Resourceprivate LikeService likeService;//格式 search?keyword=xxx@GetMapping("/search")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";} }index.html
52行 form 表单
<!-- 搜索 --> <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
2行
8行
<link rel="stylesheet" th:href="@{/css/global.css}" />14行
<header class="bg-dark sticky-top" th:replace="index::header">删除 86-161 的 li 标签,留一个 li 即可
170行
<script th:src="@{/js/global.js}"></script>67行
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">68行
<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width: 50px;height: 50px;">71行
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战<em>春招</em>,面试刷题跟他复习,一个月全搞定!</a>73行
<div class="mb-3" th:utext="${map.post.content}">77行
<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>80行
<li class="d-inline ml-2">赞 <i th:text="${map.likeCount}">11</i></li>82行
<li class="d-inline ml-2">回复 <i th:text="${map.post.commentCount}">7</i></li>89行
<!-- 分页 --> <nav class="mt-5" th:replace="index::pagination">测试:
登录账号,新发一个帖子,看看能不能搜到。
总结
以上是生活随笔为你收集整理的【牛客讨论区】第六章:Elasticsearch的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 一辆车到底需要多少芯片?
- 下一篇: 建立一个GTalk连接和启动一个IM会话