基于SpringBoot+Elasticsearch实现一套完整的搜索服务系统

 更新时间:2026年05月29日 09:11:06   作者:fengxin_rou  
在内容平台场景中,高性能、高相关性、实时可搜是搜索模块的核心诉求,本文基于 SpringBoot 与 Elasticsearch(ES),从零实现一套包含索引初始化、数据同步、增量更新、关键词检索、游标分页的完整搜索服务,需要的朋友可以参考下

前言

在内容平台场景中,高性能、高相关性、实时可搜是搜索模块的核心诉求。本文基于 SpringBoot 与 Elasticsearch(ES),从零实现一套包含索引初始化、数据同步、增量更新、关键词检索、游标分页的完整搜索服务,解决传统数据库搜索性能差、分词不精准、实时性不足等痛点,可直接应用于文章、资讯、社区类内容平台。

一、Elasticsearch 索引设计与初始化

1.1 核心概念类比

ES 是分布式搜索引擎,核心是倒排索引,其结构可与 MySQL 直接类比,降低理解成本:

  • Index(索引)≈ 数据库表
  • Document(文档)≈ 表行数据
  • Mapping(映射)≈ 表结构 Schema
  • Field(字段)≈ 表列

1.2 索引初始化实现

项目启动时自动创建索引与 Mapping,title/body 字段启用 IK 分词,需提前安装 ES 分析 - ik 插件。标题使用 ik_max_word 分词、ik_smart 检索,兼顾召回率与精准度。

/**
 * 搜索索引初始化:应用启动时创建索引与映射
 */
@Service
@RequiredArgsConstructor
public class SearchIndexInitializer {
    private final ElasticsearchClient es;
    private static final String INDEX = "zhiguang_content_index";

    @PostConstruct
    public void ensureIndex() {
        try {
            // 检查索引是否存在
            boolean exists = es.indices().exists(e -> e.index(INDEX)).value();
            if (exists) return;
            // 创建索引并定义映射
            es.indices().create(c -> c.index(INDEX).mappings(m -> m
                .properties("content_id", p -> p.long_(LongNumberProperty.of(b -> b)))
                .properties("title", p -> p.text(t -> t.analyzer("ik_max_word").searchAnalyzer("ik_smart")))
                .properties("body", p -> p.text(t -> t.analyzer("ik_max_word")))
                .properties("status", p -> p.keyword(KeywordProperty.of(b -> b)))
                .properties("title_suggest", p -> p.completion(CompletionProperty.of(b -> b)))
                // 其他字段省略...
            ));
        } catch (Exception ignored) {}
    }
}

1.3 字段设计要点

  • keyword 类型:用于标签、状态、作者信息等精确匹配与过滤,不分词。
  • text 类型:用于标题、正文等全文检索,绑定 IK 分词器。
  • completion 类型:专门用于搜索建议,提升输入 联想体验。

二、搜索索引数据写入与同步机制

2.1 全量数据回灌

应用启动时若索引为空,自动从数据库分页读取历史数据,批量写入 ES,保证索引数据完整。

@PostConstruct
public void ensureBackfill() {
    long cnt = es.count(c -> c.index(INDEX)).count();
    if (cnt > 0) return;
    int limit = 500;
    int offset = 0;
    while (true) {
        List<KnowPostFeedRow> rows = knowPostMapper.listFeedPublic(limit, offset);
        if (rows == null || rows.isEmpty()) break;
        for (KnowPostFeedRow r : rows) {
            upsertKnowPost(r.getId());
        }
        offset += rows.size();
    }
}

2.2 单篇文档写入逻辑

核心方法 upsertKnowPost 实现数据新增 / 更新,流程标准化:

  1. 从数据库查询文章详情;
  2. 远程拉取正文,失败则使用描述兜底,截断至 4000 字符;
  3. 补充点赞、收藏等计数数据;
  4. 写入 ES 并设置 refresh=WaitFor保证写入后立即可搜

2.3 软删除实现

不物理删除文档,仅更新 status=deleted,搜索时过滤该状态,避免数据丢失与索引波动。

public void softDeleteKnowPost(long id) {
    Map<String, Object> doc = new HashMap<>();
    doc.put("content_id", id);
    doc.put("status", "deleted");
    es.index(i -> i.index(INDEX).id(String.valueOf(id))
        .document(doc).refresh(Refresh.WaitFor));
}

三、基于 Kafka+Canal 的增量数据同步

3.1 同步架构

使用 Canal 监听 MySQL binlog,将数据变更发送至 Kafka 的 canal-outbox 主题,搜索模块作为消费者,实现数据库与 ES 数据准实时一致

3.2 消息消费逻辑

与用户关系模块共用 Topic,通过不同消费者组隔离业务,仅处理 entity=knowpost 的变更消息,保证幂等性。

/**
 * 搜索索引 Outbox 消费者
 */
@Service
@RequiredArgsConstructor
public class CanalOutboxConsumerSearch {
    private final SearchIndexService indexService;

    @KafkaListener(topics = OutboxTopics.CANAL_OUTBOX, groupId = "search-index-consumer")
    public void onMessage(String message, Acknowledgment ack) {
        try {
            List<JsonNode> rows = OutboxMessageUtil.extractRows(objectMapper, message);
            for (JsonNode row : rows) {
                JsonNode payload = objectMapper.readTree(row.get("payload").asText());
                String entity = payload.get("entity").asText();
                String op = payload.get("op").asText();
                Long id = payload.get("id").asLong();
                if (!"knowpost".equals(entity) || id == null) continue;
                // 执行更新或软删除
                if ("delete".equalsIgnoreCase(op)) {
                    indexService.softDeleteKnowPost(id);
                } else {
                    indexService.upsertKnowPost(id);
                }
            }
            ack.acknowledge();
        } catch (Exception ignored) {}
    }
}

3.3 优势说明

  • 解耦:数据库变更与搜索同步分离,互不影响;
  • 高可用:消息队列缓冲流量,避免直接写入 ES 导致雪崩;
  • 易扩展:新增下游模块只需新增消费者组,无侵入改造。

四、搜索服务核心实现:检索、加权与分页

4.1 完整搜索流程

前端传入关键词、标签、分页参数,后端构建 ES 查询,流程分为:参数解析→召回过滤→业务加权→排序高亮→游标分页→结果封装

4.2 多字段匹配与权重加权

使用 multi_match 实现多字段检索,标题权重设为 3,正文权重为 1,提升标题匹配优先级。通过 function_score 对点赞、浏览量做对数加权,让优质内容排名更靠前。

// 构建查询核心逻辑
.query(qb -> qb.functionScore(fs -> fs
    .query(qb2 -> qb2.bool(bq -> {
        // 多字段匹配,标题权重3倍
        bq.must(m -> m.multiMatch(mm -> mm.query(q).fields("title^3", "body")));
        // 过滤已发布内容
        bq.filter(f -> f.term(t -> t.field("status").value("published")));
        // 标签过滤
        if (!tags.isEmpty()) {
            bq.filter(f -> f.terms(t -> t.field("tags").terms(tv -> tv.value(tags))));
        }
        return bq;
    }))
    // 点赞数加权:log(1+like)×2
    .functions(fn -> fn.fieldValueFactor(f -> f.field("like_count").modifier(Log1p)).weight(2.0))
    // 浏览数加权:log(1+view)×1
    .functions(fn -> fn.fieldValueFactor(f -> f.field("view_count").modifier(Log1p)).weight(1.0))
    .boostMode(Sum)
))

4.3 游标分页实现

替代传统 offset+limit,使用 search_after 实现深分页高性能,将最后一条数据的排序值(评分、时间、点赞、ID)Base64 编码为游标,下一页从该位置继续查询。

4.4 高亮与摘要生成

对标题、正文关键词添加 <em> 高亮标签,合并为搜索摘要(Snippet),提升用户阅读体验。

五、搜索建议功能实现

基于 ES completion 类型实现输入 联想,用户输入前缀时快速返回标题候选,响应时间毫秒级。

public SuggestResponse suggest(String prefix, int size) {
    var resp = es.search(s -> s.index(INDEX)
        .suggest(sug -> sug.suggesters("title_suggest",
            sc -> sc.prefix(prefix).completion(c -> c.field("title_suggest").size(size))))
        , Map.class);
    // 解析建议结果并返回
    List<String> items = new ArrayList<>();
    resp.suggest().get("title_suggest").forEach(s -> {
        s.completion().options().forEach(opt -> items.add(opt.text()));
    });
    return new SuggestResponse(items);
}

结语

本文完整实现了 SpringBoot 整合 Elasticsearch 的企业级内容搜索系统,覆盖索引设计、数据全量 / 增量同步、关键词检索、游标分页、搜索建议全流程。方案具备实时性高、检索精准、扩展性强、性能稳定等特点,适配文章、社区、电商等内容搜索场景。

实际落地需注意:IK 分词器自定义词库优化、ES 集群分片规划、异步同步重试机制、查询性能监控。后续可扩展语义搜索、个性化排序、搜索热词统计等能力,进一步提升搜索体验。

以上就是基于SpringBoot+Elasticsearch实现一套完整的搜索服务系统的详细内容,更多关于SpringBoot Elasticsearch搜索服务系统的资料请关注脚本之家其它相关文章!

相关文章

  • JavaSE多线程阻塞队列实现代码

    JavaSE多线程阻塞队列实现代码

    阻塞队列是一种线程安全的队列,可以用于多线程之间的数据传递和同步,下面这篇文章主要介绍了JavaSE多线程阻塞队列的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-12-12
  • SpringBoot实现接口限流的常用方案

    SpringBoot实现接口限流的常用方案

    本文主要介绍SpringBoot项目的接口限流方案,市面上常用有2种限流方案漏桶和令牌桶,大家常用的都是令牌桶,本文也只实现了令牌桶的方案,需要的朋友可以参考下
    2025-08-08
  • Java实现一键将Word文档转为PDF的两种方法

    Java实现一键将Word文档转为PDF的两种方法

    本文主要介绍了Java实现一键将Word文档转为PDF的两种方法,分别使用Apache POI和Docx4J结合iText库来实现Word转PDF,具有一定的参考价值,感兴趣的可以了解一下
    2025-04-04
  • Java结束线程的三种方法及该如何选择

    Java结束线程的三种方法及该如何选择

    这篇文章主要介绍了Java结束线程的三种方法及该如何选择,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下
    2021-03-03
  • springboot使用logback自定义日志的详细过程

    springboot使用logback自定义日志的详细过程

    这篇文章主要介绍了springboot使用logback自定义日志的详细过程,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧
    2024-12-12
  • IntelliJ安装并使用Rust IDE插件

    IntelliJ安装并使用Rust IDE插件

    这篇文章主要介绍了IntelliJ安装并使用Rust IDE插件,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • JavaWeb基于Session实现的用户登陆注销方法示例

    JavaWeb基于Session实现的用户登陆注销方法示例

    为了安全起见,session常常用来保存用户的登录信息。那么服务器是怎么来实现的呢?下面这篇文章就来给大家介绍了关于JavaWeb基于Session实现的用户登陆注销的相关资料,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧。
    2017-12-12
  • java导出数据库中Excel表格数据的方法

    java导出数据库中Excel表格数据的方法

    这篇文章主要为大家详细介绍了java导出数据库中Excel表格数据的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • Java如何替换jar中的class文件

    Java如何替换jar中的class文件

    在调整java代码过程中会遇到需要改jar包中的class文件的情况,改了如何替换呢?下面小编给大家分享java替换jar中的class文件的操作方法,感兴趣的朋友跟随小编一起看看吧
    2024-02-02
  • Java中Spring获取bean方法小结

    Java中Spring获取bean方法小结

    Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,如何在程序中获取Spring配置的bean呢?下面通过本文给大家介绍Java中Spring获取bean方法小结,对spring获取bean方法相关知识感兴趣的朋友一起学习吧
    2016-01-01

最新评论