让你的博客支持仿百度全文搜索!

前言:都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">&#xe612;</i>
                     ${search.username}
                </span>
                <span>
                    <i class="layui-icon">&#xe637;</i>
                     ${search.createDate}
                </span>
            </p>
        </li>
        @}
    </ul>
    @}
</div>
@include("/blog/_common_nav.html"){}
@include("/blog/_footer.html"){}

三、效果截图

效果图一

搜索关键字:虚拟机。

效果图二

搜索关键字:数据库。

点到为止,打完收工!

相关推荐
开心就好20255 分钟前
不依赖 Mac 也能做 iOS 开发?跨设备开发流程
后端·ios
一直都在5726 分钟前
线程间的通信
java·jvm
一只叫煤球的猫7 分钟前
RAG 如何落地?从原理解释到工程实现
人工智能·后端·ai编程
卷心菜投手ovo17 分钟前
一个页面支持自定义字段,后端该怎么设计数据库?
后端
隔壁家滴怪蜀黍22 分钟前
AgentScope MsgHub 多智能体通信机制详解
后端
孟陬22 分钟前
国外技术周刊 #3:“最差程序员”带动高效团队、不写代码的创业导师如何毁掉创新…
前端·后端·设计模式
GIOTTO情25 分钟前
Infoseek危机公关全链路技术解析:基于近期热点舆情的落地实践
java
Cosolar27 分钟前
Transformer训练与生成背后的数学基础
人工智能·后端·开源
我是人✓1 小时前
从零入门 Servlet:JavaWeb 核心组件的实操与理解
java·servlet
lay_liu1 小时前
Spring Boot 自动配置
java·spring boot·后端