一、Elasticsearch入門
-
Elasticsearch簡介
-
- 一個分布式的、Restful風格的搜索引擎。
- 支持對各種類型的數據的檢索。
- 搜索速度快,可以提供實時的搜索服務。
- 便於水平擴展,每秒可以處理PB級海量數據。
-
Elasticsearch術語
-
- 索引、類型、文檔、字段。
- 集群、節點、分片、副本。
術語的解釋
- 索引:相當於數據庫中的database 改版后作為table
- 類型:相當於數據庫中的table 不再使用
- 文檔:相當於數據庫中的一行數據,數據結構為JSON
- 字段:相當於數據庫中的一列
Elasticsearch6.0以后開始逐步廢除類型的概念,索引的含義中也包括了類型。
- 集群:分布式部署,提高性能
- 節點:集群中的每一台服務器
- 分片:對一個索引的進一步划分存儲,提高並發處理能力
- 副本:對分片的備份,提高可用性
Elasticsearch相關鏈接:官網
Elasticsearch選擇下載6.4.3版本和SpringBoot兼容
Elasticsearch配置
文件位置config/elasticsearch.yml
配置環境變量
安裝中文分詞插件
github上找
解壓到指定目錄下
ik插件配置文件說明
IKAnalyzer.cfg 可以自己配置新詞
安裝Postman模擬網頁訪問
Postman相關鏈接:官網
使用命令行操作Elasticsearch
1.啟動ES—./bin/elasticsearch.bat
2.常用命令介紹
- 查看節點健康狀態
curl -X GET "localhost:9200/_cat/health?v"
這個命令需要下載curl相關包:參考鏈接
- 查看節點具體信息
curl -X GET "localhost:9200/_cat/nodes?v"
- 查看索引相關信息
curl -X GET "localhost:9200/_cat/indices?v"
新裝的沒有索引:
- 創建索引
curl -X PUT "localhost:9200/test" //test就是索引名字
查看狀態顯示yellow是因為沒有分片(備份)。
- 刪除索引
curl -X DELETE "localhost:9200/test" //test就是索引名字
使用Postman/RESTer訪問ES
RESTer是火狐插件也很好用。
- 查索引
- 建索引
- 刪除索引
- 提交數據
//test:索引 _doc:固定格式 1:id號 然后在請求body中寫數據
PUT localhost:9200/test/_doc/1
- 查數據
GET localhost:9200/test/_doc/1
- 改數據
和添加數據一樣,底層會先刪除再添加
- 刪除數據
DELETE localhost:9200/test/_doc/1
-
搜索功能的演示
-
- 先插入一些數據
- 先插入一些數據
-
開始搜索演示
搜索時會先分詞然后搜索,並不一定完全匹配
二、Spring整合Elasticsearch
-
引入依賴
-
- spring-boot-starter-data-elasticsearch
-
配置Elasticsearch
-
- cluster-name、cluster-nodes
-
Spring Data Elasticsearch
-
- ElasticsearchTemplate
- ElasticsearchRepository
導包
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
配置
1.application.properties
spring:
data:
elasticsearch:
cluster-name: nowcoder
cluster-nodes: 127.0.0.1:9300
2.解決Netty沖突問題
問題原因:Redis底層使用了Netty,Elasticsearch也用了Netty,當被注冊兩次就會報錯
解決思路:Elasticsearch中注冊Netty前會判斷有無一個參數,如果有就不注冊
這么解決:
3.給discussPost加注解
//indexName:索引名,type:固定_doc,shards:分片,replicas:備份
@Document(indexName = "discusspost",type = "_doc",shards =6 ,replicas = 2)
public class DiscussPost {
@Id
private 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)
//0-普通; 1-置頂;
private int type;
@Field(type = FieldType.Integer)
//0-正常; 1-精華; 2-拉黑;
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,並創建DiscussPostRepository接口
測試一波
Elasticsearch中配置再加一個這個:
elasticsearch包不要放在dao包下邊,會導致bean注入失敗。
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTest {
@Autowired
private DiscussPostMapper discussMapper;
@Autowired
private DiscussPostRepository discussRepository;
//有些功能上邊的解決不了,所以引入下邊的
@Autowired
private ElasticsearchTemplate elasticTemplate;
@Test
public void testInsert() {
discussRepository.save(discussMapper.selectDiscussPostById(271));
discussRepository.save(discussMapper.selectDiscussPostById(272));
discussRepository.save(discussMapper.selectDiscussPostById(273));
}
@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));
}
//localhost:9200/discusspost/_doc/231
@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)
// 底層獲取得到了高亮顯示的值, 但是沒有返回.所以為了得到高亮顯示直接用elasticTemplate.queryForPage見下面
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(); //long類型的字符串
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());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
return null;
}
});
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);
}
}
}
三、開發社區搜索功能
-
搜索服務
-
- 將帖子保存至Elasticsearch服務器。
- 從Elasticsearch服務器刪除帖子。
- 從Elasticsearch服務器搜索帖子。
-
發布事件
-
- 發布帖子時,將帖子異步的提交到Elasticsearch服務器。
- 增加評論時,將帖子異步的提交到Elasticsearch服務器。
- 在消費組件中增加一個方法,消費帖子發布事件。
-
顯示結果
-
- 在控制器中處理搜索請求,在HTML上顯示搜索結果。
一個小問題的解決
ElasticsearchService
@Service
public class ElasticsearchService {
@Autowired
private DiscussPostRepository discussPostRepository;
@Autowired
private ElasticsearchTemplate elasticTemplate;//高亮顯示
//添加、修改
public void saveDiscussPost(DiscussPost discussPost){
discussPostRepository.save(discussPost);
}
//刪除
public void deleteDiscussPost(int id){
discussPostRepository.deleteById(id);
}
//查詢
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(
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(); //long類型的字符串
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());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
return null;
}
});
}
}
處理DiscussPostController.addDiscussPost
處理CommentController.addComment
EventConsumer寫一個消費發帖事件的方法
@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());
//往es中存數據
elasticsearchService.saveDiscussPost(post);
}
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17
新寫一個SearchController
@Controller
public class SearchController implements CommunityContant {
@Autowired
private ElasticsearchService elasticsearchService;
@Autowired
private UserService userService;
@Autowired
private LikeService likeService;
@RequestMapping(path="/search",method = RequestMethod.GET)
//路徑 search?keyword=xxx
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處理搜索框
處理頁面search
突然發現之前的一個bug
MessageController.getNoticeList
先把message加入messageVo,不然模板判斷顯示不顯示的時候會報null