上一篇:从零搭建ELK在线日志系统 让你的博客支持仿百度全文搜索! 下一篇:本系列完

1496 2024-07-14 2024-07-14

前言:都2024年了,不会还有技术人没有个人博客吧?不会个人博客还不支持仿百度全文搜索吧?哥哥~

跟着我从零开始,让你的个人博客支持elasticsearch全文搜索!

PS:基于现有的博客系统引入新功能,只列举相关核心代码,供读者参考。

一、部署ES

1、下载安装

为了简化,我们采用docker部署es,相关脚本如下

# 现在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、设置密码

docker exec -it es /bin/bash
./bin/elasticsearch-setup-passwords interactive
#输入密码
yourpassword

3、安装ik分词器

# 安装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集群。

POST _analyze
{
  "analyzer": "ik_smart",
  "text": "我是中国人"
}

POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}

安装完成后,可以在界面执行上述请求参数,查看ik分词器的不同策略。

二、Spring服务端

1、相关依赖

这里需要注意Spring Boot版本需要与Es版本相对应,不然会有各种各样的兼容问题,详见 https://docs.spring.io/spring-data/elasticsearch/reference/elasticsearch/versions.html。

pom相关依赖如下

<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、相关配置

# 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

@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

@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

@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、前端

@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"){}

三、效果截图

效果图一

搜索关键字:虚拟机。

效果图1

效果图二

搜索关键字:数据库。

效果图2

未完待续....

总访问次数: 13次, 一般般帅 创建于 2024-07-14, 最后更新于 2024-07-14

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!