1. 前言
前一段时间我写过一篇文章,也是关于Elasticsearch的,那篇文章使用的方法确实邪门,用的是ElasticsearchRepository的自动依据方法名返回搜索结果,比如在ElasticsearchRepository接口中定义一个名为findPostByTitleOrContent(),Elasticsearch就会返回内容或结果相关的帖子,但是分词、高光、分段全部失效了,只能返回全词匹配的结果,算是一个半成品,所以我改进了一下,研究了源码之后,在chatGPT的辅助下,我终于完美实现了所有的功能,下面我会从安装开始,详细的阐述一下Elasticsearch的用法,话不多说,直接开始。
2. 配置准备
2.1 安装Elasticsearch
这里我使用的是7.15.2的版本,适配2.6.11版本的Springboot(因为我使用的是jdk1.8),如果需要更高版本则需要自己去官网找对应的版本号。
这里附上下载地址Past Releases of Elastic Stack Software | Elastic,如果速度比较慢可以挂个梯子。
下载完成以后就是按部就班的安装,安装完成以后可以顺便配置一下Elasticsearch的环境变量,方便后续的一些操做,可以用
curl -X GET "localhost:9200/_cat/health?v"
检验是否安装成功,如果成功就会像下面这样,可以看到服务器的健康程度:
确认成功安装就可以进入安装目录,打开elasticsearch.bat就可以启动Elasticsearch了,但是我想弄点骚操作,因为项目整合了Kafka以后每次想要运行项目都要先启动Kafka敲一长串命令,所以我想偷个懒,写个bat来自动执行,顺便也把执行elasticsearch.bat加进去一次性启动:
@echo off
cd /d D:\kafka\kafka_2.13-3.4.0
start /B bin\windows\zookeeper-server-start.bat config\zookeeper.properties
start /B bin\windows\kafka-server-start.bat config\server.properties
cd /d D:\elasticsearch\elasticsearch-7.15.2\bin
start /B elasticsearch.bat
将上面的内容复制到txt文件中,参照我上面的目录修改自己的安装目录,然后修改文件后缀为.bat即可食用,注意: 要先把Kafka和Zookeeper添加到环境变量,否则可能不能运行。
但是要像愉快的使用中文搜索,我们还需要安装ik分词:
直接上链接:Releases · medcl/elasticsearch-analysis-ik · GitHub
选择7.15.2
然后把下载好的ik放到elasticsearch的plugins目录下就可以了:
2.2 Springboot的配置
首先注入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
其次是配置application.properties:
#ElasticsearchProperties
spring.data.elasticsearch.repositories.enabled=true
3. 实体类的处理
下面以实体类帖子为例,演示一下实体类的处理:
package com.newcoder.community.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
@Document(indexName = "discusspost", shards = 6 ,replicas = 3)
public class DiscussPost {
// Elasticsearch的ID
@Id
private int id;
// Elasticsearch映射数据类型
@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")
// 分词器analyzer
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 int score;
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return "DiscussPost{" +
"id=" + id +
", userId=" + userId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", type=" + type +
", status=" + status +
", createTime=" + createTime +
", commentCount=" + commentCount +
", commentCount=" + score +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public int getCommentCount() {
return commentCount;
}
public void setCommentCount(int commentCount) {
this.commentCount = commentCount;
}
}
4. Dao层的处理
主要是定义一个接口,用来声明需要存取的实体类,这里是DiscussPost
package com.newcoder.community.dao.elasticsearch;
import com.newcoder.community.entity.DiscussPost;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
声明完这个接口以后方便后续调用Elasticsearch的save()方法和delete()方法还有update()方法来对Elasticsearch数据库中的内容进行增删改操作。
5. Service层的处理
5.1 前期准备
在这里我为了让方法可以同时返回帖子和帖子条数(因为调用.size()方法得到了错误的帖子条数,导致搜素结果大量缺失)新建了一个类来存储帖子和帖子条数:
package com.newcoder.community.entity;
import java.util.List;
public class SearchResult {
private long rows;
private List<DiscussPost> posts;
public SearchResult(long rows, List<DiscussPost> posts) {
this.rows = rows;
this.posts = posts;
}
public long getRows() {
return rows;
}
public void setRows(long rows) {
this.rows = rows;
}
public List<DiscussPost> getPosts() {
return posts;
}
public void setPosts(List<DiscussPost> posts) {
this.posts = posts;
}
}
5.2 使用 ElasticsearchRestTemplate获得并处理搜索结果
因为ElasticsearchTemplate被弃用了,所以我选择使用的是ElasticsearchRestTemplate来完成构建Service层。
package com.newcoder.community.services;
import com.newcoder.community.dao.DiscussPostMapper;
import com.newcoder.community.dao.elasticsearch.DiscussPostRepository;
import com.newcoder.community.entity.DiscussPost;
import com.newcoder.community.entity.SearchResult;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class SearchService {
@Autowired
private DiscussPostRepository discussPostRepository;
@Autowired
private DiscussPostMapper discussPostMapper;
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
/*
每次项目重新部署就把mysql里的帖子全部导入elasticsearch
*/
@PostConstruct
public void init() {
discussPostRepository.deleteAll();
System.out.println("自动删除所有帖子");
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(0,0,discussPostMapper.selectDiscussPostRows(0)));
System.out.println("自动注入所有帖子");
}
public SearchResult search(String keyword, Pageable pageable) {
List<DiscussPost> posts = new ArrayList<>();
// 构建一个NativeSearchQuery并添加分页条件、排序条件以及高光区域
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
.withPageable(pageable)
.withSorts(
SortBuilders.fieldSort("type").order(SortOrder.DESC),
SortBuilders.fieldSort("score").order(SortOrder.DESC),
SortBuilders.fieldSort("createTime").order(SortOrder.DESC)
)
.withHighlightFields(
new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
)
.build();
// 调用ElasticsearchRestTemplate的search()方法进行查询
// 使用SearchHits存储搜索结果
SearchHits<DiscussPost> searchHits = elasticsearchRestTemplate.search(query, DiscussPost.class);
long rows = searchHits.getTotalHits();
// 遍历搜索结果设置帖子的各个参数
if (searchHits.getTotalHits() != 0) {
for (SearchHit<DiscussPost> searchHit : searchHits) {
DiscussPost post = new DiscussPost();
int id = searchHit.getContent().getId();
post.setId(id);
int userId = searchHit.getContent().getUserId();
post.setUserId(userId);
String title = searchHit.getContent().getTitle();
post.setTitle(title);
String content = searchHit.getContent().getContent();
post.setContent(content);
int status = searchHit.getContent().getStatus();
post.setStatus(status);
int type = searchHit.getContent().getType();
post.setType(type);
Date createTime = searchHit.getContent().getCreateTime();
post.setCreateTime(createTime);
int commentCount = searchHit.getContent().getCommentCount();
post.setCommentCount(commentCount);
// 获得刚刚构建的高光区域,填到帖子的内容和标题上
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));
}
posts.add(post);
}
}
return new SearchResult(rows, posts);
}
public void saveDiscussPost(DiscussPost discussPost) {
discussPostRepository.save(discussPost);
}
public void deleteDiscussPost(DiscussPost discussPost) {
discussPostRepository.delete(discussPost);
}
}
值得一提的是,貌似在7.x之后Elasticsearch就把getHighlightFields().get()方法返回类型设置为了List,但是很智能的将分段和高光全部集成了,所以还是很简单的。
6. Controller层
这一层比较简单,没啥好说的
package com.newcoder.community.controller;
import com.newcoder.community.entity.DiscussPost;
import com.newcoder.community.entity.Page;
import com.newcoder.community.entity.User;
import com.newcoder.community.services.LikeService;
import com.newcoder.community.services.SearchService;
import com.newcoder.community.services.UserService;
import com.newcoder.community.util.CommunityConstant;
import com.newcoder.community.util.CommunityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class SearchController implements CommunityConstant {
@Autowired
private SearchService searchService;
@Autowired
private UserService userService;
@Autowired
private LikeService likeService;
@RequestMapping(path = "/search", method = RequestMethod.GET)
public String search(String keyword, Model model, Page page) {
Sort sort = CommunityUtil.getSearchSort();
Pageable pageable = PageRequest.of(page.getCurrent() - 1, page.getLimit(),sort);
page.setPath("/search?keyword=" + keyword);
long rows = searchService.search(keyword, pageable).getRows();
page.setRows((int) rows);
List<DiscussPost> list = searchService.search(keyword, pageable).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";
}
}
7. 最终效果
7.1 单一关键词
7.2 多关键词
8. 后记
这篇文章是小白学习路上的一些记录,如果能帮助到你的话,请帮我点个赞!