前言:都2024年了,不会还有技术人没有个人博客吧?不会个人博客还不支持仿百度全文搜索吧?哥哥~
跟着我从零开始,让你的个人博客支持 ElasticSearch 全文搜索!
PS:基于现有的博客系统引入新功能,只列举相关核心代码,供读者参考。
一、部署ES
1、下载安装
为了简化,我们采用docker部署es,相关脚本如下
bash
# 现在docker拉取镜像可能会普遍存在超时的问题,这个问题需要重视&解决
# 可以先不挂载文件,把初始配置复制出来后再进行文件目录挂载,这样下次容器删除后数据还在
# 相关脚本如下
# docker exec -it es sh
# docker cp es:/usr/share/elasticsearch /Users/docker/es
docker pull elasticsearch:7.17.3
docker stop es && docker rm es
docker run -d --name es \
-p 9200:9200 \
-p 9300:9300 \
--network=common_container_network \
--add-host=host.docker.internal:host-gateway \
-e ES_JAVA_OPTS="-Xms256m -Xmx512m" \
-e "discovery.type=single-node" \
-v /home/data/docker/es/data:/usr/share/elasticsearch/data \
-v /home/data/docker/es/plugins:/usr/share/elasticsearch/plugins \
-v /home/data/docker/es/config:/usr/share/elasticsearch/config \
-v /home/data/docker/es/logs:/usr/share/elasticsearch/logs \
-v /etc/localtime:/etc/localtime:ro \
elasticsearch:7.17.3
2、设置密码
bash
docker exec -it es /bin/bash
./bin/elasticsearch-setup-passwords interactive
#输入密码
yourpassword
3、安装ik分词器
bash
# 安装ik分词器,支持中文分词
docker exec -it es bash
# 在线下载并安装,版本需要与es相对应
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.3/elasticsearch-analysis-ik-7.17.3.zip
# 重启es
docker restart es
4、安装谷歌插件
谷歌浏览器搜索并安装 ElasticSearch Head 插件,这样可以方面的查看和管理es集群。
json
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
安装完成后,可以在界面执行上述请求参数,查看ik分词器的不同策略。
二、Spring服务端
1、相关依赖
这里需要注意Spring Boot版本需要与 ES 版本相对应,不然会有各种各样的兼容问题,详见 这里。
pom相关依赖如下
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.8</version>
<relativePath/>
</parent>
<elasticsearch.version>7.17.3</elasticsearch.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2、相关配置
ini
# RestClientProperties
spring.elasticsearch.rest.uris = ${ES_HOST:http://localhost:9200}
spring.elasticsearch.rest.username = ${ES_USER:}
spring.elasticsearch.rest.password = ${ES_PWD:}
spring.elasticsearch.rest.connectionTimeout = 1s
spring.elasticsearch.rest.readTimeout = 10s
3、相关代码
1、Controller
java
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/blog/es")
public class EsController {
private final EsService esService;
@GetMapping("/search")
public ResultEntity search(String keyword, String username) {
return ResultEntity.ok(esService.search(keyword, username));
}
@GetMapping("/freshAll")
public ResultEntity freshAllData(@RequestParam(defaultValue = "false") boolean force) {
long count = esService.freshAllData(force);
return ResultEntity.ok("重刷数据成功 条数=" + count);
}
@GetMapping("/all")
public ResultEntity getAllData() {
return ResultEntity.ok(esService.getAllData());
}
@GetMapping("/deleteSingle")
public ResultEntity deleteSingle(Long id) {
esService.deleteSingle(id);
return ResultEntity.ok();
}
}
2、Service
java
@Slf4j
@Service
@RequiredArgsConstructor
public class EsService {
private final EsBlogRepository esBlogRepository;
private final ElasticsearchRestTemplate restTemplate;
private final SysBlogServiceImpl blogService;
private final UserService userService;
public SearchHits<EsBlog> search(String keyword, String username) {
// api参考 https://blog.csdn.net/weixin_52438357/article/details/137050151
// boolQuery 组合多个查询条件,支持must(必须匹配)、should(至少匹配一个)、must_not(不能匹配)和filter(过滤)
BoolQueryBuilder builder = QueryBuilders.boolQuery();
// matchPhraseQuery 搜索与指定短语匹配的文档,考虑短语的完整性和顺序
// multiMatchQuery 允许你在多个字段上执行匹配查询
// termQuery 对指定字段执行精确匹配查询 不会对字段值进行分词
builder.must(QueryBuilders.termQuery("username", username));
builder.must(QueryBuilders.multiMatchQuery(keyword, "title", "summary"));
// 高亮
String preTag = "<font color='red'>";
String postTag = "</font>";
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(builder)
.withHighlightFields(
new HighlightBuilder.Field("title"),
new HighlightBuilder.Field("summary")
.preTags(preTag).postTags(postTag))
.build();
log.info("查询es build={} query={}", builder, query);
return restTemplate.search(query, EsBlog.class);
}
public long freshAllData(@RequestParam(defaultValue = "false") boolean force) {
log.info("开始重刷博客数据 force={}", force);
// 删除索引,并且显示创建映射
restTemplate.indexOps(EsBlog.class).delete();
restTemplate.indexOps(EsBlog.class).createWithMapping();
Query<SysBlog> query = blogService.createQuery();
// 只允许搜索公开类型博客
query.andEq("blog_type", BlogTypeEnum.PUBLIC.getCode());
// 根据用户id分组
List<SysBlog> blogs = blogService.query(query);
Map<Long, List<SysBlog>> userMap = blogs
.stream()
.collect(Collectors.groupingBy(SysBlog::getUserId));
userMap.forEach((userId, list) -> {
UserDto userDto = userService.findById(userId);
if (userDto == null) {
return;
}
String username = userDto.getNickName();
List<EsBlog> esBlogs = list.stream()
.map(parseEsBlog(force, username))
.collect(Collectors.toList());
// 保存数据到es
esBlogRepository.saveAll(esBlogs);
log.info("刷新博客数据成功 userId={} username={} count={}", userId, username, esBlogs.size());
});
log.info("结束重刷博客数据,条数={}", blogs.size());
return blogs.size();
}
public Page<EsBlog> getAllData() {
// 从0开始
Pageable pageable = PageRequest.of(0, 200);
return esBlogRepository.findAll(pageable);
}
public void deleteSingle(Long id) {
esBlogRepository.deleteById(id);
}
private Function<SysBlog, EsBlog> parseEsBlog(boolean force, String username) {
return item -> {
if (force) {
String summary = BlogUtil.exactSummary(item.getUserId(), item.getDir(), item.getFileName());
item.setSummary(summary);
SysBlog tmp = new SysBlog();
tmp.setId(item.getId());
tmp.setSummary(summary);
blogService.updateByIdIgnoreNull(tmp);
} else if (StrUtil.isEmpty(item.getSummary())) {
String summary = BlogUtil.exactSummary(item.getUserId(), item.getDir(), item.getFileName());
item.setSummary(summary);
}
String createDate = DateUtil.parseIntDate(item.getCreateDate());
return EsBlog
.builder()
.id(item.getId())
.username(username)
.title(createDate + " " + item.getDir() + ":" + item.getTitle())
.summary(item.getSummary())
.createDate(createDate)
.updateAt(new Date())
.build();
};
}
}
3、Entity
java
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "es_xiaokui_blog", createIndex = true)
public class EsBlog {
@Id
private Long id;
/**
* 所属用户
* FieldType.Keyword存储字符串数据时,不会建立索引,精准匹配
*/
@Field(type = FieldType.Keyword)
private String username;
/**
* FieldType.Text在存储字符串数据的时候,会自动建立索引,也会占用部分空间资源,分词搜索
*/
@Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_smart")
private String title;
/**
* 博客总结,默认提取前150字,去掉换行符
* ik_smart 最小分词法
* 我是程序员 -> 我、是、程序员
* ik_max_word 最细分词法
* 我是程序员 -> 我、是、程序员、程序、员
*/
@Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_smart")
private String summary;
/**
* 博客创建日期
*/
@Field(value = "create_date", type = FieldType.Keyword)
private String createDate;
/**
* 数据更新时间
*/
@Field(value = "updated_at", type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateAt;
}
4、前端
html
@include("/blog/_header.html"){}
<div class="layui-main main-container" id="main-blog">
@if (isNotEmpty(lists)) {
<h4>共找到 ${lists.~size} 篇相关博客</h4>
<ul>
@for (search in lists) {
<li style="width:100%;margin:5px 0;float:left;padding-right:5px;" class="es-blog">
<a target="_blank" href="${ctxPath + search.blogPath}">${search.title}</a>
<p style="margin: 0 0 2px 0">
${search.summary}
</p>
<p style="color: #808080; margin: 0 0 5px 0">
<span style="margin:0 10px 0 0">
<i class="layui-icon"></i>
${search.username}
</span>
<span>
<i class="layui-icon"></i>
${search.createDate}
</span>
</p>
</li>
@}
</ul>
@}
</div>
@include("/blog/_common_nav.html"){}
@include("/blog/_footer.html"){}
三、效果截图
效果图一
搜索关键字:虚拟机。
效果图二
搜索关键字:数据库。
点到为止,打完收工!