"热点新闻搜索系统"这类题目,近期在我这儿出现的频率确实不低。准备做 java 毕业设计的朋友,一看到"SpringBoot + 实时热点 + 新闻搜索"就发怵,其实它没有想象中那么难——真正难的是把"实时热点"和"智能搜索"两件事拆明白,再用 SpringBoot 生态里的组件把它们串起来。这篇内容就按我完整落地这个项目的经验来写,从题目解读到核心代码实现,再到答辩前必须填完的坑,一步步拆清楚。无论你是想直接拿这个题交作业,还是想包装成简历上的实战项目,思路都一样适用。
这个系统本质上要解决三件事:第一,把分散在各处的新闻资讯统一采集、清洗、入库,形成自己的数据底座;第二,让用户能通过关键词快速检索到相关新闻,并具备一定的智能排序能力;第三,把实时发生的热点新闻主动推送给用户,而不是让用户自己一遍遍地刷新。落到 SpringBoot 技术上,对应的就是定时任务、全文检索、缓存和 WebSocket 推送这几个核心组件。适合的人群很明确——有 Java 基础、正在找毕业设计方向的学生,以及想快速搭建一个资讯类 demo 的初级工程师。
1. 项目定位与整体设计拆解
1.1 先把题目拆成三件事
很多同学拿到题目就开始建表、写接口,这是最容易跑偏的地方。我习惯先做名词拆解:热点、新闻、搜索、推送、聚合、智能。逐个对应到系统模块,你会发现架构自然就出来了。
热点,对应的是数据采集和热度计算模块。新闻不会凭空长在你数据库里,你需要抓取源站数据,或者至少准备一批合理的模拟数据。热度也不是随机的,得有一个可解释的计算逻辑,比如点击量、搜索次数、时间衰减的加权公式。新闻搜索,对应的是全文检索模块。这里的搜索不是 SQL 里的where title like '%xxx%'那么简单,而是要对正文做分词、索引、相关性打分,否则用户搜"疫苗接种"却搜不到"打疫苗"的文章,体验就很差。推送,对应的是实时通知模块。用户打开页面后,后端一旦发现新的热点新闻,要主动把数据推过去。聚合,则是所有数据的汇聚加工,包括去重、分类、标签提取。智能,在这个体量的毕设里,不一定要上机器学习,规则化的热度排序加上用户行为分析,已经足够撑起整个题目的亮点。
这样拆完,系统的功能边界就清楚了:一个数据端(采集清洗)、一个存储端(MySQL + Redis + 可选 Elasticsearch)、一个检索端(索引 + 关键词)、一个推送端(WebSocket/SSE)。前端页面反而是最不重要的部分,答辩时能展示搜索框和热点列表就够了。
1.2 技术选型:别一上来就上微服务
做毕设最忌讳的就是技术堆砌。我见过有同学在选题答辩 PPT 里写了 Spring Cloud、Kafka、Flink,结果代码连单机并发都跑不顺,这种项目一看就是抄的,根本没有沉淀。这个题目,SpringBoot 单机架构完全够用,而且能把每个技术点的深度做出来。
我最终选定的组合是这样的:SpringBoot 3.x(如果学校环境是 JDK8,就用 2.7.x,避免环境折腾)、MyBatis-Plus 做持久层、MySQL 8 存业务数据、Redis 做热搜榜和缓存、Elasticsearch 做全文检索(这一步可以作为扩展点,先讲清楚再决定要不要引入)、WebSocket 做实时推送。前端用 Vue3 + Element Plus,走前后端分离,分离的好处是推送逻辑清晰,后端只管推,前端只管渲染。如果你前端基础薄弱,用 Thymeleaf 套个 Bootstrap 也一样能交差,但效果和体验会差一些,好在不影响核心功能。
选 Elasticsearch 而不是纯用 MySQL 的 LIKE 查询,核心原因是中文检索效果不同。MySQL 的like '%关键词%'无法处理分词、同义词、相关性排序,而 ES 配合 IK 分词器,可以把一句话拆成有意义的词元,检索质量是质的提升。代价是 ES 本身吃内存,如果你的电脑只有 8G 内存,建议用 Docker 跑 ES 单节点,堆内存限制在 1G 左右,只用来支撑搜索服务,别让它干别的。
2. 核心模块拆分与检索方案选型
2.1 新闻采集与数据入库
新闻数据从哪来,是整个项目第一个现实问题。正规做法是写爬虫抓公开新闻源,但开班前我必须提醒:爬虫模块在毕业设计里是双刃剑。一方面它能展示你的网络编程能力,另一方面,如果目标网站有反爬限制、robots 协议不友好,你抓取时很容易触发对方封 IP,答辩现场演示翻车是常有的事。
我的建议是采用混合数据源方案:内置一批结构化的模拟新闻数据(可以手动造 500 条左右,覆盖科技、体育、财经、娱乐几个大类),同时提供一个可插拔的爬虫接口,用 HttpClient + Jsoup 抓取 RSS 源或公开 API。RSS 是最省事的选择,很多新闻站点还保留着 RSS 输出,结构统一、字段完整,反爬压力也小。抓取后经过 HTML 清洗(去掉标签、提取正文摘要)、去重(按标题 MD5 判断)、分类(规则匹配关键词,比如标题含"股价""基金"就归为财经),落库到 news 表。
news 表的核心字段,我建议这样设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(255) | 新闻标题 |
| summary | varchar(512) | 摘要,列表页展示用 |
| content | text | 正文全文 |
| category | varchar(32) | 分类,如 tech/sports |
| source | varchar(64) | 来源站点 |
| url | varchar(512) | 原文链接,可空 |
| publish_time | datetime | 发布时间 |
| heat_score | double | 热度分数,定时重算 |
热度计算我采用的是时间衰减模型,避免旧新闻霸榜。基础公式是heat = baseWeight * (1 / (1 + ageHours)),其中 baseWeight 由点击量、收藏量、搜索次数加权得出。举个例子:一条新闻发布 1 小时、被搜索 100 次,基础权重按搜索次数乘以 0.5 计算(50),那么当前热度就是50 / (1 + 1) = 25;10 小时后热度降到50 / 11 ≈ 4.5。这样热点榜上永远是最近几个小时的新鲜内容。定时任务每 5 分钟跑一次全量重算,更新到 Redis 有序集合里。
2.2 搜索链路设计与热搜榜实现
搜索功能是项目的核心,也是答辩时最能展示深度的地方。我选用 Elasticsearch 做检索层,配合 IK 中文分词插件。数据同步方案不需要上 logstash 那一套,直接在新闻落库的方法里同步调用 ES 的保存接口,单机数据量下毫无压力。ES 索引的 mapping 里,title 字段用 ik_max_word 分词(尽可能细粒度拆分,利于召回),summary 字段用 ik_smart 分词(粗粒度拆分,利于匹配精度),查询时用 bool 查询,title 匹配的权重设置为 3,summary 权重 1,content 权重 0.5,这样标题命中的结果会排在前面。
检索接口返回的数据结构要统一:总条数、分页参数、命中列表。列表里每条新闻带上高亮片段,也就是把匹配词用<em>标签包起来,前端渲染时高亮显示。这一步虽然不起眼,但用户体验差别很大,很多同学做完搜索发现"搜出来了但看不出哪里命中了",就是这个细节没做。
热搜榜则是另一套逻辑。用户每次成功搜索一个关键词,后端就往 Redis 的有序集合里对该词做增量加一:
stringRedisTemplate.opsForZSet().incrementScore(RedisKeys.HOT_KEYWORDS, keyword, 1);排行榜查询则是取前 N 个:
Set<String> topKeywords = stringRedisTemplate.opsForZSet() .reverseRange(RedisKeys.HOT_KEYWORDS, 0, 9);这里有个坑:用户输入的搜索词千奇百怪,直接统计会出来一堆无意义词语。解决方法是先对搜索词做停用词过滤和文本归一化(全角转半角、去空格、统一大小写),再把长度小于 2 的词语丢弃。如果想让热搜榜更"智能",还可以在返回结果前用 HanLP 对每个搜索词做词性标注,只保留名词性质的词。HanLP 在 SpringBoot 里接入很简单,引入hanlp依赖后,用HanLP.segment(text)就能拿到分词结果,但要注意它首次加载模型会耗时几百毫秒,建议在启动时预热,别让第一个请求卡在那。
3. 实操落地:从搭建工程到核心功能实现
3.1 工程骨架与依赖配置
我直接说一个可以照抄的配置方案。创建 SpringBoot 工程时,引入这几个核心依赖:spring-boot-starter-web(基础 Web)、mybatis-plus-boot-starter(数据库操作)、spring-boot-starter-data-redis(缓存和热搜榜)、spring-boot-starter-websocket(实时推送)、elasticsearch-rest-high-level-client(如果你用 ES 7.x 的话)。
有些同学喜欢用 spring-boot-starter-data-elasticsearch,但它的 Repository 风格对复杂查询不友好,我推荐直接用 RestHighLevelClient 原生 API,虽然代码量多一点,但查询语法透明,出了问题好排查。ES 8.x 的官方客户端换了新的 API,如果学校环境提供的版本较旧,建议统一固定 ES 7.17 和对应客户端版本,省得被版本差异折磨。
application.yml 里的几个关键配置:
spring: datasource: url: jdbc:mysql://localhost:3306/news_platform?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 2 elasticsearch: uris: http://localhost:9200 index: news_index这里建议给 Redis 指定单独的 database,比如 database 2,不要跟其他项目混用一个库,否则 key 冲突排查起来很痛苦。配置完骨架后,第一件事不要急着写业务,先把 HealthController 写好并启动一次,确认 SpringBoot、MySQL、Redis、ES 四个节点全部连通,再开始写业务代码。基础设施不稳,后面全是连锁问题。
3.2 检索接口与热搜榜核心代码实现
检索接口是最能体现代码质量的地方。我给出一个可以直接借鉴的实现思路,核心是构造 ES 查询 DSL。用 RestHighLevelClient 构造 bool 查询时,需要注意 should 子句里的权重设置:
public SearchResult searchNews(String keyword, int page, int size) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 标题命中的权重最高 boolQuery.should(QueryBuilders.matchQuery("title", keyword).boost(3.0f)); // 摘要匹配次之 boolQuery.should(QueryBuilders.matchQuery("summary", keyword).boost(1.5f)); // 正文匹配兜底,保证召回率 boolQuery.should(QueryBuilders.matchQuery("content", keyword).boost(0.5f)); boolQuery.minimumShouldMatch(1); // 至少一个条件命中 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(boolQuery); sourceBuilder.from((page - 1) * size); sourceBuilder.size(size); // 高亮:让前端可以显著展示命中位置 HighlightBuilder highlight = new HighlightBuilder(); highlight.field("title"); highlight.field("summary"); highlight.preTags("<em class='keyword'>"); highlight.postTags("</em>"); sourceBuilder.highlighter(highlight); ... }写检索代码的时候,我强烈建议把一个容易忽略的问题考虑进去——空搜索处理。用户打开搜索页不输入任何词直接点搜索,这时应该改为返回热点新闻列表,而不是查空关键词导致 ES 报错。这个逻辑虽然简单,但在验收演示时非常常见,没处理的系统直接白屏,非常尴尬。
数据库相关的操作,MyBatis-Plus 用起来最省心。LambdaQueryWrapper做条件查询、Page做分页,代码量很少。新闻落库时,我习惯手动控制主键生成策略为ASSIGN_ID,也就是雪花 ID,避免数据库自增 ID 在数据迁移、多实例部署时产生冲突。虽然单机部署用自增 ID 也没问题,但既然要写进简历,代码规范就按生产标准来。
3.3 实时推送:WebSocket 让热点"主动找人"
实时推送是这道题的另一大亮点,也是区别于普通 CRUD 项目的加分项。实现方案我选了 WebSocket,相比 SSE(服务端单向事件流),WebSocket 双向通信能力更强,也支持前端向后端发送订阅主题。SpringBoot 里启用 WebSocket 非常简单,三步走:
第一步,写一个配置类实现WebSocketConfigurer,注册 handler 和拦截器。第二步,实现TextWebSocketHandler,处理连接建立、消息收发、异常断开三个核心方法。第三步,在前端 WebSocket 挂载连接,监听后端推送。
后端推送热点新闻的核心代码大致这样:
public void broadcastHotNews(NewsVO news) { String message = JSON.toJSONString( Result.success("hot_news", news) ); synchronized (sessions) { for (WebSocketSession session : sessions) { if (session.isOpen()) { session.sendMessage(new TextMessage(message)); } } } }定时任务中检测到新增热点新闻后调用 broadcastHotNews,前端收到后弹一条通知并刷新热点榜。这里有两个生产级细节:一个是连接管理需要线程安全,用ConcurrentHashMap<String, WebSocketSession>存 session,连接建立时加入、断开时移除,否则并发下会出脏数据;另一个是推送消息要带类型标记(比如hot_news),前端根据类型做不同的 UI 动作,而不是所有消息统一弹窗,否则后期加新功能时前端代码到处是分支判断,难以维护。
如果不方便用 WebSocket,SSE 是另一个更轻的选择。SpringBoot 的SseEmitter实现起来比 WebSocket 还简单,但只支持服务端到客户端单向推送,对于"热点新闻通知"这个场景其实完全够用。我两个方案都试过,最终选 WebSocket 是因为答辩时更容易讲出"双向通信、协议原理"这类扩展知识点,但如果你时间紧,SSE 更稳妥。
4. 常见问题与排障实录
4.1 中文分词搜不准,问题出在哪
中文搜索和英文搜索的差异在第一版就给我上了一课。第一次测试时,搜索"苹果发布新手机",结果里居然混进了"苹果是一种水果"的文章,原因是 IK 分词把"苹果"和"发布"分开后,水果文章里的"苹果"词项也命中了。这个问题不是 ES 的锅,而是相关性调优没做好。
解决思路分两层。第一层,在查询构造时,对 title 字段使用match_phrase短语匹配提高精确度,短语匹配要求词项按顺序连续出现,能过滤掉大量无关结果。第二层,引入业务层面的词权重白名单,比如在代码里维护一个热点实体词表(手机品牌、体育队伍名等),命中词表的词在查询时 boost 额外乘 2。说白了就是让系统知道"苹果"在新闻场景里更可能是一个品牌词。这两个调整上线后,搜索质量明显改善。
另一个高频坑是 IK 分词器的词典问题。IK 默认词典对互联网新词很不友好,比如"碳中和""元宇宙"这类词汇,默认分词会拆得七零八落,搜索时召回率很差。解决办法是自定义扩展词典,在 IK 配置目录下新建一个 ext.dic 文件,把领域词汇放进去,重启 ES 生效。我在项目里维护了包括技术名词、财经术语在内的 200 多个扩展词,效果立竿见影。
4.2 热点数据不一致与缓存穿透
热搜榜用了 Redis 之后,最先出现的问题就是缓存和数据库不一致。定时任务重算热度分数时,如果直接更新 Redis,而数据库里的 heat_score 还没同步,就会出现"前端看的榜和后台数据对不上"的情况。我的做法是Redis 只作为读缓存,数据库是唯一事实源。定时任务先更新数据库,再把 Top 100 的热点新闻 ID 列表写入 Redis。用户查询热搜榜时,只从 Redis 取 ID 列表,再批量查数据库补全新闻信息。热度分数的变化频率很高,但热度榜的排名变化是分钟级的,这样的设计既保证了数据一致,又把 Redis 的压力降到最低。
缓存穿透也是必踩的坑。热点新闻榜被大量用户同时刷新时,如果 Redis 里没有数据,所有请求直接打到数据库,数据库很容易扛不住。解决方式是布隆过滤器或者缓存空值。毕设层面,我推荐更简单的空值缓存方案:查询结果不存在时,在 Redis 里缓存一个空对象并设置 60 秒过期,同时加一个互斥锁,保证同一时间只有一个请求去加载数据库,其余请求等待后从缓存读取。代码量不大,但能体现你对高并发场景的理解,答辩时这就是加分点。
4.3 定时任务重复执行与抓取稳定性
定时任务实现新闻抓取时,最容易忽略的是任务的幂等性。SpringBoot 的@Scheduled默认是单机单线程执行,但如果你的项目部署了多个实例,或者手动触发接口和定时任务同时跑,就可能出现同一条新闻被插入两次。我的解决方案是双保险:数据库层面给 title 加唯一索引(或者对标题 MD5 加唯一索引),代码层面在抓取前先按标题查询一次,命中就跳过。
爬虫本身的稳定性也要考虑。Jsoup 抓取时需要设置合理的超时时间和重试机制,我习惯用带重试的 HttpClient 封装:
RequestConfig config = RequestConfig.custom() .setConnectTimeout(3000) .setSocketTimeout(5000) .setConnectionRequestTimeout(3000) .build();抓取失败时不要立即重试,等下一次定时任务再来处理。如果连续失败超过 3 次,把源站标记为不可用,停止抓取,避免无意义的空转。另外,抓取时一定要伪装 User-Agent 和 Referer,这不是为了绕过什么限制,而是规范网络请求,尊重目标站点的访问策略。可以在配置文件里维护一个 UA 池,每次请求随机取一个,降低被限制的风险。
5. 项目扩展与我的实际体会
5.1 这个项目还能往哪些方向升级
如果做完基础功能还有时间,或者在简历上想再突出一点,我建议从三个方向扩展。
第一个方向是推荐系统。基于用户的历史搜索和点击行为,用协同过滤的思路,给用户推荐可能感兴趣的新闻类别。不需要复杂的算法库,MySQL 里存 user_behavior 表,记录用户浏览的新闻分类,再用简单的统计推荐(用户最常看的三个分类中,取热度最高的未读新闻)就能跑起来。这个功能非常契合"智能化"题眼,实现成本又不高,是性价比很高的扩展。
第二个方向是热词趋势分析。利用 Redis 里保存的搜索热词历史,按时段统计词频涨幅,找出"突然变热"的关键词,今天的热词榜就来自这里。这个功能可以让系统界面多一个"趋势上涨"区域,视觉上非常直观。
第三个方向是将检索升级为语义检索。引入向量化嵌入模型,把新闻标题和正文都转成向量存入 ES 的 dense_vector 字段,查询时把用户输入也转向量,用余弦相似度做召回。这一步会把你项目从"关键词匹配"拉到"语义理解"层,但模型选择和服务器资源要掂量一下,毕设阶段做个小模型 demo 即可,别让环境配置消耗太多时间。
5.2 做这个题目我踩过的几个关键坑
最后说点实在的。这个项目我前后迭代了三版,踩过的坑比看一个月文档都管用。第一,ES 的版本一定提前确认好。Spring Boot 和 ES 客户端的版本兼容关系很敏感,很多同学遇到max virtual memory areas vm.max_map_count错误或者客户端版本不匹配,一个下午就交代了。ES 7.17 配 spring-boot 2.7 是我用下来最稳的组合。
第二,Redis 连接池的坑。SpringBoot 2.x 之后默认用 Lettuce 连接 Redis,默认配置是无限期等待连接,高并发时会出现连接耗尽,表现为接口响应越来越慢,最终超时。在 application.yml 里显式配置 lettuce 连接池参数,比如最大连接数 16、最大空闲数 8,能避免很多莫名其妙的问题。
第三,中文乱码。这个问题看着基础,但在答辩现场栽跟头的人非常多。MySQL 连接串必须带characterEncoding=utf8mb4,数据库表字符集用 utf8mb4,ES 索引创建时 setting 需要指定"analysis": {"analyzer": "ik_max_word"}。这三处只要有一处漏了,搜索中文就会出乱码或空白,而且报错信息还不直观,排查起来特别耗时间。
如果你是用这个项目做毕业设计,我还有一个真诚建议:把你实现每个模块的"为什么"都记录下来。比如问自己,为什么热搜榜用 Redis 有序集合而不是数据库?为什么推送用 WebSocket 而不是轮询?这些"为什么"就是答辩时老师最爱问的问题。想清楚一个,你就能从容应对一个;想清楚的越多,这个项目就越是你的作品。