简介:这是一套基于Java实现的搜索引擎毕业设计资源包,面向计算机相关专业(人工智能、通信、电子信息、物联网等)的高校学生、教师及科研工作者,用于解决课程设计、毕业设计或项目初期立项开发中缺乏完整可运行代码和配套交付文档的问题,也适合有Java基础的学习者进阶参考。压缩包共329个文件,大小12.87MB,包含71个Java源文件、45个Python脚本、31个JS文件、18个JSX组件,以及XML配置、JSP页面、启动批处理、项目配置文件等,既有后端Servlet与搜索逻辑,也涵盖前端显示与辅助工具。资源内附数据库SQL脚本、论文文档、答辩PPT及视频演示,代码经严格测试可正常运行,配套start.bat等启动脚本能快速搭建本地环境,便于理解搜索引擎的索引构建、检索和结果展示流程,目录结构也按模块清晰组织。目前已有64人学习下载,可直接作为毕业设计或课设交付内容,也可在此代码基础上扩展其他功能;遇到配置或运行问题,还能获得作者的远程教学支持。
1. 搜索引擎毕设不是写爬虫,而是写一个能说服答辩老师的「找东西」系统
基于Java搜索引擎的设计与实现,这个毕设题目听起来像爬虫项目,其实核心是索引与检索:数据已经躺在数据库里,你怎么让用户在几百毫秒内找到最相关的内容。它同时覆盖Java后端、数据结构(倒排索引)、算法(BM25排序)、数据库设计,是一个难得的全栈题目。标题里的“源码+数据库+论文”,对应你要交付的三样东西:一套能跑的代码、一份能初始化库表的SQL脚本、一篇能把设计讲清楚的论文。适合正在选题、想快速落地一个可演示系统的计算机专业学生。
2. 技术选型先立住:Lucene 还是手写倒排索引,数据库到底管哪一块
2.1 Lucene 是搜索引擎的心脏,但直接调 API 会被答辩老师看穿
Lucene 是一个用 Java 写的全文检索库,倒排索引、分词、查询解析、相关性排序、高亮它全都封装好了。常见做法是把它作为搜索引擎的核心引擎,外面用 Spring Boot 包一层 REST 接口。但注意,如果你的论文里只写“基于 Lucene 实现”,答辩老师随便一问就能确认你到底只是调了 API,还是真理解了索引结构。我一般建议的做法是:Lucene 负责底层索引文件的写入和读取,你在它外面做自己的设计点——比如自研一个中文分词落地的策略类、给文档动态调整权重、加一个搜索热词统计模块。这样既不会输在稳定性上,又能把工作量写进论文。
Lucene 内部有几个核心类,论文里值得写清楚:IndexWriter 负责写索引,IndexReader 负责读索引,IndexSearcher 负责查询,Analyzer 负责分词。索引文件采用段(segment)结构,写入时先进内存缓冲区,超过阈值再刷到磁盘;每次 commit 后生成一个段,后台还会把多个小段合并成一个大段。这个机制直接影响后面的参数调优和避坑,所以先把概念立住,后面才有依据。
2.2 数据库不是用来搜的,它存的是文档原文、搜索日志和统计报表
很多同学把文档内容塞进 MySQL,搜索时用 LIKE '%关键词%',然后告诉老师这是搜索引擎。这是个典型的认知错误。MySQL 的 LIKE 前缀匹配最多能用 B+ 树索引,前后通配的 LIKE '%词%' 只能全表扫描,数据量到十万条就开始卡。搜索引擎的常见做法是:MySQL 存文章原文、标题、URL、作者、发布时间、状态等业务字段,Lucene 里只存分词后的倒排索引。用户搜索时先查索引拿到文档 ID 列表,再回 MySQL 取详情。这样 MySQL 管增删改查和统计,索引管快速定位,两边各司其职。
一套完整的毕业设计,数据库至少要有文档表、搜索日志表,以及可选的用户行为表。搜索日志表的作用常被忽略,它是论文里“用户行为分析”章节的素材来源:每个查询关键词、命中条数、用户点击了哪一篇,全部落库。后续做热词统计、画词云、生成热门搜索列表,都靠这张表。你也可以在日志表上做点后端优化,比如用定时任务把高频关键词刷进 Redis,这又是一个加分项。
2.3 模块划分:索引模块、分词模块、搜索模块、Web 展示模块
做毕设前先画清楚模块边界。索引模块负责从 MySQL 读取需要被搜索的文档,调用分词器切词,构建倒排索引,并且要考虑全量重建和增量更新的时机。分词模块的核心难点是中文歧义,“南京市长江大桥”会被切出两种结果,你需要决定用词典还是用统计模型。搜索模块负责接收用户查询、分词、检索、按相关度排序、返回结果。Web 展示模块就是一个 Spring Boot 应用,提供查询接口,前端页面把结果渲染出来,顺便展示论文里常用的查询耗时和命中数。
这里要提醒一句:不要一上来就引入 Elasticsearch。ES 确实也是搜索引擎,但它是分布式系统,部署需要 JVM 堆、节点配置、分片调优,整套东西太重。答辩时老师会问“你为什么要用 ES 而不是自己写”,解释成本很高。Lucene 是 ES 底层的实现基础,用它做毕设,往上写设计,往下写算法,正好落在课程知识范围内。
3. 从数据库建表到跑通第一个查询:一套 Java 搜索引擎的最小骨架
3.1 数据库表设计:文档表、索引状态表、搜索日志表
搜索引擎的数据源头是业务表。我先建一张 doc 表,字段尽量贴近论文里的“文档管理”模块:
CREATE TABLE doc ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, url VARCHAR(500) DEFAULT '', author VARCHAR(100) DEFAULT '', publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1, -- 1上架 0下架 2删除 update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_publish_time (publish_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;doc 表的 status 字段决定哪些文档可以被搜索。搜索引擎不能只盯着索引,要跟业务表状态保持一致。update_time 字段是增量索引的关键,后面会用它做定时增量更新。需要注意的是,content 用 TEXT 类型就足够,尽量不要用 LONGTEXT,否则 MySQL 查询和索引时的 IO 压力都更大。
搜索日志表单独建一张:
CREATE TABLE search_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, keyword VARCHAR(255) NOT NULL, result_count INT DEFAULT 0, clicked_doc_id BIGINT DEFAULT 0, search_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_keyword (keyword), KEY idx_search_time (search_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;keyword 字段不要加唯一索引,搜索引擎要记录重复查询,才能统计热度。每次查询插入一条日志,成本很低,但对论文的数据支撑很有用。你在答辩时可以展示一条 SQL:按 keyword 分组统计出现次数,这就是热门搜索词排行。
3.2 工程结构:Spring Boot 项目依赖与包划分
用 Spring Boot 搭一个 web 工程。pom 里的关键依赖如下,这里不写死版本号,用你本机 Maven 能拉到的稳定版即可:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>com.janeluo</groupId> <artifactId>ikanalyzer</artifactId> <version>${ik.version}</version> </dependency> </dependencies>IK Analyzer 是中文分词库,和 Lucene 集成容易踩坑,一定要选与 Lucene 版本匹配的版本。如果 Maven 仓库拉不下来,就下载 jar 包手动 install 到本地仓库。工程包结构按职责分:controller 放搜索接口,service 放索引构建和搜索逻辑,dao 负责 MySQL 访问,lucene 包放索引工具类,model 放数据库实体。
3.3 建立索引:从 MySQL 读出文档并写入 Lucene 索引
索引构建是搜索引擎的起点。我写了一个完整可跑的构建服务,用 JDBC 查询 doc 表,遍历结果,调用 Lucene 的 IndexWriter 写入索引文件:
public class IndexService { private final Analyzer analyzer = new IKAnalyzer(true); private final String indexPath = "/data/lucene/index"; public void buildIndex() { // try-with-resources 确保数据库连接和结果集都释放 try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement( "SELECT id, title, content, url, author, publish_time " + "FROM doc WHERE status = 1")) { ResultSet rs = ps.executeQuery(); // 初始化索引目录 Directory directory = FSDirectory.open(Paths.get(indexPath)); IndexWriterConfig config = new IndexWriterConfig(analyzer); config.setRAMBufferSizeMB(64.0); config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); IndexWriter writer = new IndexWriter(directory, config); while (rs.next()) { long id = rs.getLong("id"); Document doc = new Document(); // LongPoint负责数值查询,StoredField负责取回原始值 doc.add(new LongPoint("id", id)); doc.add(new StoredField("id", id)); doc.add(new TextField("title", rs.getString("title"), Field.Store.YES)); doc.add(new TextField("content", rs.getString("content"), Field.Store.NO)); doc.add(new StringField("url", rs.getString("url"), Field.Store.YES)); doc.add(new StringField("author", rs.getString("author"), Field.Store.YES)); writer.addDocument(doc); } writer.commit(); writer.close(); } catch (Exception e) { log.error("build index failed", e); } } }代码逻辑说明:先用 JDBC 查出所有状态正常的文档,这是全量索引的常见做法。TextField 会把内容分词并建立倒排索引,title 需要回显到页面,所以 Store.YES;content 只在搜索时参与匹配,不需要回显,所以 Store.NO,这样索引文件体积更小。id 用 LongPoint 作为数值类型参与过滤,同时必须加一个 StoredField 才能从搜索结果里取回真实 id。setRAMBufferSizeMB(64.0) 控制写索引时的内存缓冲阈值,缓冲区越大写入越快,但 JVM 压力也越大,我一般控制在 64MB 到 128MB 之间。
3.4 搜索接口:用户输入关键词到返回 JSON 的全流程
搜索端的关键是用 QueryParser 把用户输入解析成 Lucene Query,然后交给 IndexSearcher 执行。一个可直接改的搜索方法:
public List<Map<String, Object>> search(String keyword, int from, int size) { // 每次查询打开一个 IndexReader,用完关闭 try (IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(indexPath)))) { IndexSearcher searcher = new IndexSearcher(reader); // 用IK分词器解析查询 QueryParser parser = new QueryParser("content", analyzer); parser.setDefaultOperator(QueryParser.Operator.AND); Query query = parser.parse(keyword); TopDocs topDocs = searcher.search(query, from + size); ScoreDoc[] hits = topDocs.scoreDocs; List<Map<String, Object>> result = new ArrayList<>(); for (int i = from; i < from + size && i < hits.length; i++) { Document d = searcher.doc(hits[i].doc); Map<String, Object> item = new HashMap<>(); item.put("id", d.get("id")); item.put("title", d.get("title")); item.put("url", d.get("url")); item.put("score", hits[i].score); result.add(item); } return result; } catch (Exception e) { log.error("search failed", e); return Collections.emptyList(); } }说明:查询默认作用在 content 字段,你可以改成 title 或同时查多个字段。QueryParser 默认 Operator 是 OR,搜索“java 数据库”会把包含任意一个词的结果都返回,相关性比较差;改成 AND 提高精确度,但召回率会下降。我的习惯是保留 OR,让 BM25 排序把相关性高的结果顶到前面,同时论文里讨论这个取舍。这里还有一个细节:每次查询打开新的 IndexReader 简单可靠,但频繁创建会有打开文件句柄的成本;更高效的做法是复用 IndexReader,并监听目录变化后重新打开。毕设里能做到前者就够,后者属于性能优化章节的加分项。
4. 搜索引擎的 3 个必调参数:分词器、BM25、深分页
4.1 中文分词器的选择:为什么 StandardTokenizer 在中文上直接翻车
Lucene 自带的 StandardTokenizer 按空格、标点切分单词。英文没问题,但中文整句话会被切成一个词,比如“搜索引擎设计”变成一个 token,搜索“搜索”完全匹配不到。这就是中文搜索的经典翻车现场。解决办法是用 IKAnalyzer 这类中文分词器,它支持词典和歧义处理。初始化方式很简单:
Analyzer analyzer = new IKAnalyzer(true); // true表示智能切分IK 的智能切分会结合词典把句子切成更合理的词,比如“南京市长江大桥”会切出“南京市/长江大桥”,而不是“南京/市长/江大桥”。你还可以把 IK 的自定义词典文件 IKAnalyzer.cfg.xml 放到 classpath,在 ext.dic 里加入“搜索引擎”“毕设”“数据库”这类业务词,让它们尽量不被切碎。这一步是论文“系统优化”部分很加分的实操细节。
IK Analyzer 不是 Lucene 官方发布的组件,和 Lucene 版本不兼容时,最常见的报错是 NoClassDefFoundError。解决方法是确认 IK 版本对应的 Lucene 版本,或者直接下载源码编译进工程。很多同学在这上面花掉一整天,其实只要找对依赖坐标就解决了。
4.2 相关性排序:BM25 的 k1 和 b 到底影响了什么
Lucene 7 以后默认使用 BM25Similarity,替代了老的 TF-IDF。BM25 有两个关键参数:k1 控制词频的饱和度,默认 1.2;b 控制文档长度归一化程度,默认 0.75。k1 越大,同一词在文档里出现的次数对分数的影响越明显;b 越大,越短的文档分词后越占便宜。如果你的文档是长短混合的商品标题和详情,建议 b 调小到 0.5。如果文档长度普遍接近,b 可以维持默认。
自定义 Similarity 的代码:
IndexWriterConfig config = new IndexWriterConfig(analyzer); BM25Similarity bm25 = new BM25Similarity(1.2f, 0.5f); config.setSimilarity(bm25);注意,写索引时的 Similarity 和搜索时的 Similarity 必须一致,否则算分结果会不一致。很多同学只在搜索端设置了 BM25,索引端还是默认值,结果排序效果不对。这是排查相关性问题时第一个要查的点。你可以准备一小组测试数据,调整参数后跑同一个查询,观察前十条结果的顺序变化,这就是论文里的对比实验。
4.3 深分页:from+size 为什么越翻越慢,上万页怎么办
Lucene 的 TopDocs 查询必须一次性算出 from+size 条结果,然后截取其中一段。如果用户翻到第 1000 页,from=1000, size=20,Lucene 要取前 1020 条再截掉前 1000 条,代价随 from 线性增长。这就是为什么很多搜索系统只允许翻前一百页。
常见做法是改用 searchAfter,它基于上一页最后一条的 ScoreDoc,直接拿到下一页,不需要丢弃前面的大段结果。实现思路是:
TopDocs topDocs = searcher.searchAfter(lastScoreDoc, query, pageSize);pageSize 比实际需要的多取几条,用来计算下一页的 lastScoreDoc。代价是相关性排序必须稳定,如果 score 相同,需要增加一个 tie-breaker 字段,比如 docId,否则可能重复或漏数据。毕设里能把这个点讲清楚,答辩老师会认为你有实战经验,而不是只会调接口。
5. 毕设避坑:索引不一致、中文乱码、内存溢出的 5 个现场恢复记录
5.1 索引重建后搜不到刚插入的数据:提交时机没对齐
现象:数据库里明明有一条新记录,但搜索接口查不到,重启项目后又能查到。原因:索引构建程序只跑了一次,之后没有增量索引;或者 IndexWriter 没有 commit,数据仍在内存缓冲中。解决:给 doc 表加 update_time 字段,写一个定时任务,每隔五分钟扫描 update_time 大于上次索引时间的记录,增量加入索引;同时每次 addDocument 后按批次 commit。注意 commit 不是免费操作,频繁 commit 会生成大量小段,导致搜索变慢,可以用段合并参数控制。
5.2 中文乱码:文件编码、请求编码、数据库连接串三处不一致
现象:搜索中文关键词返回的结果乱码,或者索引里存进去就是问号。原因:三处编码不一致。IDE 里源文件用了 GBK,Tomcat 请求默认 ISO-8859-1,MySQL 连接串没带 characterEncoding。解决:统一用 UTF-8。在 application.properties 里写:
spring.datasource.url=jdbc:mysql://localhost:3306/search?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai server.servlet.encoding.force=true代码里不要手动 new String(keyword.getBytes(), "UTF-8"),这种写法反复出现基本就是编码问题的源头。正确姿势是让 Spring Boot 的 CharacterEncodingFilter 统一处理请求和响应编码。
5.3 JVM 堆内存溢出:Lucene 段合并把老年代占满
现象:全量索引几万条文档时,直接 OutOfMemory。原因:一次把所有文档读取到内存写入索引,且 IndexWriterConfig 默认 RAM 缓冲区较大,段合并时还需要额外堆空间。解决:分批读取数据,每批五千条,writer.commit;把 setRAMBufferSizeMB 调小到 64MB;如果数据真上百万,全量索引不要一次性做,改成按发布时间分批重建。另外全量索引最好单独跑一个定时任务,不要在用户访问高峰期触发。
5.4 数据库连接池被占满:连接没释放,接口开始卡死
现象:系统跑了几十个并发请求后,数据库连接池报 Connection is not available。原因:JDBC 或 MyBatis 查询数据库后没有在 finally 里关闭 Connection,或者结果集循环里抛异常没有释放。解决:所有连接相关代码用 try-with-resources;注意 Lucene 的 IndexReader 也需要在查询完成后关闭,否则文件句柄泄漏,Linux 上会报 Too many open files。数据库连接和文件句柄是程序运行中最容易漏掉的两类资源,写代码时养成随手释放的习惯。
5.5 答辩被问倒:你们的搜索引擎和 MySQL 的 LIKE 有什么区别
现象:老师问 LIKE 也能搜,为什么还要写搜索引擎,学生答不上来。原因:没有从数据结构角度准备这个必问点。解决:答三点。第一,LIKE '%词%' 匹配时无法利用 B+ 树索引,必须全表扫描,复杂度 O(n);搜索引擎用倒排索引,先按词找到倒排列表,复杂度接近 O(词频)。第二,搜索引擎能算相关度,BM25 可以给词频高、文档短的记录加权,LIKE 返回的行没有排序。第三,搜索引擎支持分词、同义词、模糊匹配,LIKE 只能进行简单字符串模式匹配。把这个答清楚,你就是把它当成数据库面试题准备过。
6. 从能跑到能答辩:增量索引、搜索建议、性能自测
增量索引不一定上消息队列。我的做法是:一个 Spring @Scheduled 方法,每五分钟查一次 update_time,把新增或修改的文档写入索引;删除的文档用 IndexWriter.deleteDocuments(new LongPoint("id", id)) 删除。这套逻辑稳定、好复现,论文里也能画成流程图。
搜索建议可以做得很轻:对搜索日志按关键词分组统计频次,用户输入前缀时,返回频次最高的前十条。这个方案不需要额外引入 Elasticsearch 或 Redis,一个 SQL 加一个 HashMap 就能实现,答辩时又能讲清楚原理。
答辩通过前,最好做一个性能自测记录:准备一万条测试数据,记录全量索引耗时、平均查询耗时、首屏耗时。用 JMeter 跑并发 100 的查询,观察接口在什么阈值下开始超时,把数据和结论写进论文的测试章节。这个习惯让我躲过不少问题。希望帮到你。
本文还有配套的精品资源,点击获取