☰
基于Java+Solr的搜索引擎设计与实现:从启动到避坑全攻略
2026/10/1 4:16:20 网站建设 项目流程

简介:这份基于Java的搜索引擎毕业设计项目,完整包含源代码、数据库SQL脚本、毕业论文、答辩PPT及演示视频,面向计算机相关专业学生、教师及从业者,可直接用于毕设、课设或项目初期立项。压缩包内共329个文件、约12.87MB,以Java服务端源码、Python辅助脚本、JS/JSX前端页面、XML/Solr索引配置和SQL数据库脚本为主体,另有docx论文文档及bat一键启动脚本。项目围绕图书信息搜索场景,实现索引构建、关键词查询、收藏与阅读记录等模块,Servlet和JSP页面完整,能清楚看到搜索引擎后端处理流程。目前已有63人学习下载,代码经严格测试,可正常运行并支持二次扩展;遇到配置或运行问题,作者提供远程教学指导,适合赶毕设或需要快速搭建演示系统的读者。

1. 基于 Java 的搜索引擎设计与实现:先让整套源码能在本地跑起来

以「基于 Java 的搜索引擎设计与实现」为题的毕设源码包,拿到手第一件事不是读论文,而是先确认它能不能在本地跑起来。这个包里源码、数据库脚本、论文、答辩 PPT 和演示视频都齐,但它不是单文件项目,而是索引、内容、用户三个模块组成的 Java Web 应用,启动顺序错了,页面就打不开。

适合谁?准备交毕设或课设的学生,项目初期要做立项演示的人,以及想搞懂 Java 搜索后端怎么串起来的人。新手按步骤走能跑通,老手直接跳到第 4 章看坑。

我按实际运行顺序拆解:四个 bat 脚本和 zoo.cfg 各管什么,索引、查询、回源链路和数据库设计,复现时的坑,以及能写进论文的验证技巧。

2. 拆包看结构:四个 bat 脚本、zoo.cfg 和 Servlet 类的关系

一个 Java Web 毕设包里的顶层文件不会太多,但每个文件背后都代表一个运行时角色。这个包里的四个 bat、一个 zoo.cfg、五个 Servlet 类,正好对应了这个搜索引擎的三个模块:索引、内容、用户。先把角色分清,后面部署时才不会乱。

2.1 四个 bat 脚本排出的启动顺序

先看文件清单:start_index.bat、start_content.bat、start_share_user.bat、start.bat。从命名上基本能推出,start.bat 是总入口,剩下三个是模块级启动脚本。这种写法在课程设计里很常见,目的是把服务拆开:索引服务起 Solr,内容服务负责数据预热或内容同步,共享用户服务负责用户相关的接口。这样答辩时可以单独演示某一模块,不影响整体。

我一般会在 start.bat 里干这么几件事:设置 JAVA_HOME、按依赖顺序调用三个模块脚本、等待端口就绪、最后提示访问地址。一个常见的示意脚本长这样:

@echo off set JAVA_HOME=C:\Program Files\Java\jdk-8 set SOLR_PORT=8983 set ZK_PORT=2181 echo [1/4] starting zookeeper and index service call start_index.bat timeout /t 8 echo [2/4] starting content service call start_content.bat timeout /t 5 echo [3/4] starting shared user service call start_share_user.bat timeout /t 3 echo [4/4] deploy web app, open http://localhost:8080/search

逻辑说明:start_index.bat 是第一步,它内部通常会先启动 ZooKeeper 再启动 Solr,ZooKeeper 起得慢,所以后面要留 8 秒等待;start_content.bat 把待索引的数据同步到 Solr,或者把内容详情库初始化好;start_share_user.bat 起用户服务,主要是收藏、阅读记录这些接口。参数上,JAVA_HOME 要指向实际 JDK 路径,SOLR_PORT 和 ZK_PORT 要和你部署时配置的端口保持一致,否则后续模块连不上。

这里最容易被忽略的是 timeout 参数。很多同学把 start_index.bat 和 start_content.bat 挨着执行,ZooKeeper 还没注册完,Solr 就报集群连接失败。我自己的习惯是把等待时间拉长到 8 秒以上,宁肯多等,也别让后面脚本抢跑。

为什么这么拆而不是做成一个 Tomcat 应用搞定一切?因为要演示分层:索引层是独立进程,内容层负责把数据库里的数据搬进索引,用户层维护个性化数据,Web 层只负责查。论文里的架构图好画,回答评委问题时也好解释。你甚至可以现场只打开 start_index.bat,用 Solr 自带的管理界面演示索引查询,证明搜索功能是独立的,Web 层只是一个壳。

2.2 zoo.cfg:为什么一个毕设里会有 ZooKeeper

很多第一次接触这个项目的同学会问:搜索引擎为什么要带一个 zoo.cfg?这其实是 ZooKeeper 的配置文件,而 ZooKeeper 出现在这里,通常说明 Solr 是以 SolrCloud 模式部署的。SolrCloud 需要 ZooKeeper 来管理 collection 的配置、分片状态和节点协调。单机也能用不带 ZooKeeper 的 Solr,但项目既然给了 zoo.cfg,说明原作者就是按集群那套思路搭的,复现时不要擅自去掉,否则可能起不来。

一个标准单机 zoo.cfg 大概是这样的:

tickTime=2000 initLimit=10 syncLimit=5 dataDir=D:/data/zookeeper clientPort=2181

逻辑说明:tickTime 是 ZooKeeper 最基本的时间单位,单位是毫秒,2000 表示一个 tick 为 2 秒;initLimit 是 follower 节点启动后同步到 leader 的超时倍数,10 就是 10 个 tick,也就是 20 秒;syncLimit 是运行期间 follower 与 leader 心跳超时倍数;dataDir 是快照和事务日志的目录,这个目录必须存在,不存在的话 ZooKeeper 会直接退出;clientPort 是客户端连接端口,SolrJ 或 Solr 客户端默认连 2181。

这里最坑的是 dataDir。很多人下载后直接双击启动,没注意 zoo.cfg 里的 dataDir 写的是“D:/data/zookeeper”这种绝对路径,D 盘没有这个目录,ZooKeeper 就闪退。我会改成相对路径,或者在部署文档里明确要求先创建目录。另外,2181 端口很容易被占用,尤其是本机跑过其他分布式组件时,启动前先用netstat -ano | findstr 2181看一眼。

还有一点要说清:Solr 的 collection 配置在 SolrCloud 模式下是交给 ZooKeeper 管理的,不是放在本地磁盘随便改。所以如果你新增了一个 core,只在 admin 界面点创建还不够,要确认 ZooKeeper 里已经写入了配置。很多同学在这上面卡半天,Solr 界面看起来正常,Java 客户端一查就报org.apache.solr.client.solrj.SolrServerException: No live SolrServer nodes available,其实大多是 ZooKeeper 和 Solr 的集群状态对不上。

2.3 Servlet 类名是业务功能的地图

剩下的几个 Servlet 类,其实是理解业务功能的关键。类名不是随便起的,从 SearchBookServlet、FindServlet、ShowReadServlet、ShowCollectServlet、ShowDislikeServlet 这五个名字就能看出项目在做什么:搜索书、查看详情、阅读记录、收藏、不感兴趣。下面这张表是它们的大致职责:

类名对应功能典型场景
SearchBookServlet搜索入口,接收关键词,查 Solr首页搜索框
FindServlet按书籍 ID 查询详情,或二次检索结果列表点击跳详情
ShowReadServlet展示阅读记录,写入阅读流水用户中心“最近阅读”
ShowCollectServlet展示收藏列表,新增/取消收藏用户中心“我的收藏”
ShowDislikeServlet展示不感兴趣列表,写入负样本推荐位“不感兴趣”

注意类名上的 Show 前缀,表示这些 Servlet 主要是展示型接口,但它们往往也会触发写操作,比如 ShowReadServlet 在展示阅读记录的同时,可能要把当前用户这次阅读行为写入日志表。这是这个项目比较有价值的地方:它不是一篇纯搜索 demo,而是把搜索行为和用户行为打通了,论文里可以有“用户画像”“个性化排序”这些点可以写。

另外,这几个文件后缀是 .class,说明压缩包里同时带了编译后的 class 和源码。用的时候建议以源码为准,在 IDEA 里重新编译一遍,免得 class 文件和你本机 Tomcat 环境不匹配,出现类版本不一致的问题。课设场景下,能把这里面的类名讲清楚,答辩开场的架构图就稳了一半。

从业务流来看,一次完整请求是:用户在首页输入关键词,SearchBookServlet 接收后查 Solr,拿到一批 bookId;用户点进详情,FindServlet 根据 bookId 回源 MySQL 查完整信息;用户开始阅读,ShowReadServlet 写一条阅读记录;用户觉得好点收藏,ShowCollectServlet 写收藏表;用户不想再看到某本书,ShowDislikeServlet 写不感兴趣表。这一套下来,搜索系统就从一个“查得到”的系统变成了“越查越懂你”的系统。这也是毕设答辩时最容易被认可的地方,因为功能闭环完整。

3. 核心链路:Solr 索引、SolrJ 查询与五张核心表

这一章讲的是搜索引擎的主干:数据怎么进 Solr,查询怎么写,MySQL 怎么回源。理解这条链路,后面改功能、改排序、接推荐都有抓手。

3.1 schema.xml 里需要重点修改的字段配置

Solr 的索引结构由 schema 决定,托管模式下叫 managed-schema,非托管模式叫 schema.xml。对书籍搜索这个场景,至少要定义书名、作者、简介、分类、销量这几个字段。我一般会这样配:

<schema name="books" version="1.6"> <field name="bookId" type="string" indexed="true" stored="true" docValues="true"/> <field name="title" type="text_ik" indexed="true" stored="true"/> <field name="author" type="string" indexed="true" stored="true"/> <field name="category" type="string" indexed="true" stored="true"/> <field name="summary" type="text_ik" indexed="true" stored="true"/> <field name="saleCount" type="plong" indexed="true" stored="true"/> </schema>

逻辑说明:bookId 用 string 而不是 long,是为了避免 Solr 和 MySQL 之间数据类型转换时出现意外;title 和 summary 用 text_ik,这是 IK 分词器的字段类型,中文搜索依赖它;author 和 category 用 string,适合精确匹配和分组;saleCount 用 plong,支持按销量排序。indexed=true 表示参与查询,stored=true 表示要把结果返回给前端,docValues=true 表示这个字段要为排序和聚合建列式存储。

参数调整建议:如果不需要按销量排序,saleCount 可以去掉 docValues,减少索引体积;如果简介太长不想返回,summary 可以设 stored=false,只在查询时参与匹配,结果里不带出来。这里很多人会犯一个错:把所有字段都设成 stored=true,索引文件膨胀得很厉害,搜索变慢,答辩时被问“搜索为什么慢”就很尴尬。

还要注意分词器版本匹配。IKAnalyzer 的 jar 包版本如果和 Solr 主版本不一致,字段类型 text_ik 解析直接报错,Solr 的 core 加载不成功。装好 IK 后,建议在 Solr 的 analysis 页面用“多线程”这个词测一下分词效果,能分出“多线程”就算正常。如果只有单字,说明 IK 没生效,搜索命中率会非常差。

3.2 用 SolrJ 写一个简单的搜索 Servlet

SolrJ 是 Solr 官方提供的 Java 客户端,课程设计里用起来最顺手。以下代码模拟 SearchBookServlet 的核心查询逻辑,省略了 JSON 序列化部分:

@WebServlet("/search") public class SearchBookServlet extends HttpServlet { private static final String SOLR_URL = "http://127.0.0.1:8983/solr/books"; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("UTF-8"); String keyword = req.getParameter("keyword"); int page = parsePage(req.getParameter("page")); try (SolrClient client = new HttpSolrClient.Builder(SOLR_URL).build()) { SolrQuery query = new SolrQuery(); // 对特殊字符做转义,这里简化了,正式场景要过滤 + - && || ! ( ) { } [ ] ^ ~ * ? : \ / query.setQuery("title:" + keyword + " OR summary:" + keyword); query.setStart((page - 1) * 10); query.setRows(10); query.setFields("bookId", "title", "author", "category"); QueryResponse response = client.query(query); SolrDocumentList docs = response.getResults(); // 用 docs 里的 bookId 去 MySQL 回源,补全价格、库存、封面 writeJson(resp, docs); } } }

逻辑说明:setQuery 里写的是 Lucene 查询语法,title:关键词 表示在 title 字段匹配,OR 表示只要标题或简介命中就算命中;setStart 是偏移量,页码从 1 开始,所以 (page-1)*10 得到偏移;setRows 是每页条数,这里写死 10;setFields 指定返回字段,减少网络传输。

参数说明:page 参数来自前端,parsePage 要做空值和负值处理;SOLR_URL 里的 8983 是 Solr 默认 HTTP 端口,如果改成 8984,这里要跟着改;如果 Solr 起了账号验证,还要在 Builder 里加认证信息,不过毕设项目一般不会开。

这里还要解释回源的必要性。Solr 里存的是索引数据,适合做关键词匹配和排序,但像价格、库存、封面路径这种变动频繁或存储成本高的字段,不建议全塞进 Solr。正确姿势是 Solr 返回 bookId,Java 代码再用WHERE book_id IN (...)去 MySQL 查详情。这样搜索速度和详情维护互不拖累,也方便你答辩时讲“索引库和业务库分离”这个点。

3.3 数据库表设计:五张核心表

数据库脚本在这个包里是单独放的,我建议拿到后先不要急着导入,先把表结构看一遍。按这个项目的功能,至少会有五张表:

表名作用关键字段
book书籍主表book_id, title, author, category, price, sale_count
sys_user用户表user_id, user_name, password
user_collect收藏表id, user_id, book_id, create_time
user_dislike不感兴趣表id, user_id, book_id, create_time
user_read_log阅读记录表id, user_id, book_id, read_time, read_page

设计上要注意的点:user_collect 和 user_dislike 拆成两张表,而不是在一张表里用一个状态字段区分。原因是查询场景完全相反——收藏是正样本,将来要做推荐补全;不感兴趣是负样本,查询时直接排除。如果合在一张表里,排除查询要写成status != 1,等到数据量大时索引效率不如WHERE book_id NOT IN (...)直观。这个设计细节写进论文数据模型部分,比大段概念堆砌更实在。

user_read_log 一般不做唯一约束,因为同一本书可能读很多次,但查询时只取最近一条。回源 MySQL 时建议用批量 IN 查询,不要循环单条查,否则 10 条搜索结果要发 10 次 SQL,答辩现场一刷新就卡顿,观感很差。连接串一定记得加characterEncoding=utf8,不然中文条件查出来是乱码,后面第 4 章我会重点讲这个坑。

4. 避坑记录:从启动到搜索连续翻车后留下的五条排查经验

这一章是我复现这类毕设项目时真实踩过的坑,按“现象 → 原因 → 解决”的顺序写,每条都可以直接对照排查。如果你遇到类似问题,先别拆代码,按顺序检查环境。

4.1 现象:index 模块一启动窗口就闪退

双击 start_index.bat,窗口一闪而过,Solr 服务根本没起来。原因几乎都是 ZooKeeper 没先启动,或者 zoo.cfg 里的 dataDir 目录不存在。SolrCloud 模式启动时,Solr 会先连接 ZooKeeper 注册节点,连不上就直接退出;如果 ZooKeeper 起来但 dataDir 不存在,ZooKeeper 自己也会退出,Solr 跟着失败。

解决分三步:第一步,在 zoo.cfg 里把 dataDir 改成本机真实存在的路径,比如dataDir=./zk-data,相对路径最省事;第二步,先单独启动 ZooKeeper,确认端口 2181 监听,再启动 Solr;第三步,查看 Solr 的solr.log,里面会有明确错误。窗口闪退时日志不会自动展开,建议用命令行启动,至少能看到错误输出。

4.2 现象:中文关键词搜索返回空,英文能查到

最典型的中文搜索翻车现场。原因有两个:第一,schema 里没用中文分词器,中文标题被按单字切分,搜“多线程”匹配不到“Java多线程编程”;第二,数据库和 Solr 的编码不一致,索引里存的是乱码,自然查不到。

解决:给 Solr 装上 IK 分词器,字段类型改成 text_ik,并重启 core;把 MySQL 表、字段、JDBC 连接全改成 utf8mb4;在 schema.xml 里确认 title 和 summary 用的是 text_ik。验证方法是在 Solr 的 analysis 页面输入“Java多线程”,看分词结果是否包含“多线程”这个词。如果分词正常还查不到,检查数据导入时是不是把整本书内容塞进了 summary,导致关键词匹配不到。

4.3 现象:Tomcat 里访问 /search 报 NoClassDefFoundError

Servlet 代码编译没问题,部署到 Tomcat 后启动不报错,一访问就抛NoClassDefFoundError: org/apache/solr/client/solrj/SolrQuery。原因很简单:SolrJ 相关的 jar 包没有打进 WEB-INF/lib,Tomcat 运行时找不到类。另一种情况是 Tomcat 自带的 lib 下有旧版本类,和项目里的新版本冲突。

解决:把 solr-solrj、httpclient、httpcore、commons-io 等依赖统一放到 WEB-INF/lib 下,并且不要重复放在 Tomcat 的 lib 目录里。用 Maven 的同学检查 pom.xml 里的 scope 是不是 compile,别一不小心标成了 provided。部署前记得 clean 一下 Tomcat 的 work 目录,把旧类的编译缓存清掉。

4.4 现象:搜索能返回 bookId,但点进详情页 404

Solr 查询正常,前端也拿到了结果,点击某本书详情却 404。原因通常是 url-pattern 没对上前端请求路径,或者回源 SQL 的字段类型不匹配。比如 Solr 里 bookId 是 string 类型,存的是"B001",MySQL 里 book_id 是 int 类型,回源时隐性转换失败,根本查不到数据。

解决:先看浏览器的请求路径,再对照 Servlet 上的 @WebServlet 注解或者 web.xml 里的 url-pattern,确保完全一致;回源 SQL 写WHERE book_id = ?,Java 层传参时把 Solr 返回的字符串转换成对应类型;建议索引字段和业务主键统一用 string,比如BK001这种带前缀的编码,既避免类型转换问题,论文里还能讲“业务主键与索引主键一致性”的设计。

4.5 现象:并发请求下收藏、阅读记录时灵时不灵

单机测试没问题,用 JMeter 压一下收藏接口,会出现部分请求返回 500 或者数据丢失。原因大概率是连接池没配,每次请求新建数据库连接,超时或被系统掐断;或者项目里手动管理主键,并发时生成了重复 ID,主键冲突直接被吞掉。

解决:换装 HikariCP 或 Druid 连接池,设置最大连接数和超时时间;主键用数据库自增,或者在代码里用雪花算法生成,别自己用 System.currentTimeMillis 硬拼;在 DAO 写操作外层做好事务控制,尤其是收藏这两个表同时要插入记录时,要么两条都成功要么都失败。答辩时不一定会压并发,但这些配置写了,评委问“并发场景怎么保证数据一致性”就能有话接。Java 面试里这也叫基础题,做好了是加分项。

5. 进阶改造:把“不感兴趣”变成搜索引擎的负反馈过滤器

前面讲了那么多,最后给一个能直接落地的改造:用 user_dislike 表过滤搜索结果。这个改动不大,但能让项目从“能搜”进阶到“会过滤”,论文里可以单独写一小节。

5.1 查询时排除不感兴趣内容

改造点全在 SearchBookServlet 里。先查当前用户的不感兴趣列表,再把这些 bookId 排除出 Solr 查询。SolrJ 代码只需要加一行:

SolrQuery query = new SolrQuery(); query.setQuery("title:" + keyword + " OR summary:" + keyword); // 负反馈过滤:排除用户明确不感兴趣的书 query.addFilterQuery("-bookId:(" + String.join(" OR ", dislikeIds) + ")");

逻辑说明:addFilterQuery 也就是 fq,作用是过滤结果集,不参与打分排序,性能比放在 q 里好。-bookId:(B001 OR B002)表示排除这两个 ID。参数上,dislikeIds 是从 user_dislike 表查出来的,注意判空,空列表就不要加这个 fq,否则查询结果全空。

5.2 用 curl 快速验证搜索链路

改完之后,先用 curl 验证 Solr 侧,再走页面,能快速定位问题:

curl "http://127.0.0.1:8983/solr/books/select?q=title:%E5%A4%9A%E7%BA%BF%E7%A8%8B&rows=10&wt=json"

这个请求直接查 Solr,不经过 Java 代码。返回 JSON 里看 numFound 和 docs 数组,如果这里正常,说明索引没问题;接着再访问 Tomcat 的/search?keyword=多线程,如果页面异常,问题就在 Java 层。用 r、rows、wt 是 Solr 查询的通用参数,前端调试时非常有用,论文的测试章节也可以截图放这个命令的输出。

5.3 写进论文的验证方法

最后可以做成一张验证表,方便放到论文测试章节:

验证对象方法预期结果
索引正确性对比 Solr 文档数与数据库 book 表记录数数量一致
基础搜索10 个中文关键词查 top10 结果无空结果,点击可打开详情
负反馈过滤对某本书点“不感兴趣”后重新搜索该书不再出现在结果列表
并发写入JMeter 100 线程执行收藏操作无 500,无重复收藏记录

这套验证做下来,论文的功能测试、性能测试都有素材,不用在最后凑字数。

这次复现给我的教训是:毕设项目能不能跑通,和代码写的漂不漂亮没关系,关键是启动链路要理顺。从那以后,我每次拿到新的源码包都强制自己走一遍“启动链路 → 核心查询 → 关键写操作 → 并发踩点”四步验收,第 4 章那五个问题基本能在 20 分钟内定位。希望这篇拆解笔记能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询