资讯动态

Java+Elasticsearch构建多源司法搜索系统:从数据归一化到BM25调优

发布时间:2026/9/14 3:56:25 来源:尧图企业网站定制
简介面向智能司法的多源信息搜索系统项目代码是一份基于Java开发的毕业设计/课程设计资源面向计算机相关专业学生聚焦司法信息检索场景可帮助掌握多源数据整合、全文检索Elasticsearch、自然语言处理、信息检索算法等关键技术并学习JavaFX/Swing界面构建、数据库管理、安全权限控制及性能优化等工程化实践。资源包共56个文件以25个Java源文件为核心辅以7个XML配置、5个JavaScript、4个JSP页面、CSS与字体等静态资源覆盖后端逻辑、前端展示与项目配置整体仅274KB目录结构分明便于按模块阅读与二次开发。目前已有99人学习使用适合需要毕业设计参考、课程设计提升或司法信息化方向入门的学习者。1. 多源信息搜索系统司法检索到底难在哪多源信息搜索系统在司法场景里最常见的痛点是数据不在一个地方裁判文书在文书网法条在法规库案例摘要散在各家平台新闻又是另一套格式。这套 esJudicatureSearch 毕业设计项目就是用 Java 把这些来源统一收敛到 Elasticsearch 里再做关键词检索和语义扩展。它适合两类人一类是准备拿司法搜索当毕设或课设题目、想找一个完整工程做底子的学生另一类是刚接手一个 Spring Boot ES 后端的初级工程师想看看正规一点的多源检索系统目录该怎么组织。源码包里带 data 词典、keywords 关键词表和完整 Maven 工程能直接跑起来改。2. 工程骨架与数据层从 pom.xml 到多源文书归一化2.1 先读懂 esJudicatureSearch 的目录结构拿到 zip 解压后第一眼看到的是 esJudicatureSearch-master 根目录下那串文件pom.xml、.gitattributes、.idea、src、test、data。很多第一次做毕设的人会直接打开 src 找代码但我的习惯是先读 pom.xml 和 data 目录因为一个多源搜索系统的技术选型和数据来源都写在这两个地方。pom.xml 决定整个项目的依赖边界。这是一个标准 Maven 工程不是 Gradle根目录没有 gradlew所以后续构建都以 mvn 为主。src/test 与 src/main 分开数据文件放在 data 下baidu_dictionary 和 keywords 明显是给分词和查询扩展用的词表说明作者把「词典」当成资源文件独立管理而不是硬编码在 Java 代码里。!-- pom.xml 关键依赖版本号给的是 7.x 常用组合以你本地仓库实际版本为准 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version /dependency dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version7.17.9/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependency /dependenciesSpring Boot Web 负责提供 REST 检索接口elasticsearch-rest-high-level-client 是 ES 7.x 时代的官方 Java 客户端8.x 之后官方主推 elasticsearch-java 新客户端所以如果你手头工程里客户端版本和 ES 集群大版本不一致握手阶段就会直接报错。MySQL 驱动用来存原始文书和检索日志HanLP 负责分词、实体识别和关键词扩展。版本兼容是这里最常见的坑ES 客户端版本必须与服务器大版本一致7.x 客户端连 8.x 集群会报 version mismatch。另一个容易被忽略的点是.idea 目录里 workspace.xml、misc.xml 记录的是 IntelliJ 的窗口布局和 JDK 级别这些文件不要提交到 Git否则每次 clone 后打开都会有奇怪的配置冲突.gitattributes 则是给 Git 配换行符用的保证 Windows 和 Linux 之间 checkout 代码不乱码。2.2 多源数据标准化先把四类文书拧成一种结构司法检索的多源主要体现在这里法院判决书、法规法条、案例库摘要、相关新闻报道它们的字段命名完全不同。判决书有「案号」「审判法院」「判决日期」法条有「效力级别」「发布机关」新闻只有标题、正文和发布时间。如果不做归一化ES 里就得建四个索引查询时挨个搜再合并排序权重很难统一。这个项目的做法是定义一个 StandardDoc 模型把不同来源映射成 title、content、source、court、caseNo、publishDate 等通用字段。这样后续索引、查询、排序都只面向一份标准结构新增数据源时只需扩展一个解析器。public class DocumentNormalizer { public StandardDoc normalize(MapString, String raw) { StandardDoc doc new StandardDoc(); // 用来源 原文 URL 生成稳定 docId避免同一篇文书重复入库 doc.setDocId(hashWithSource(raw.get(source), raw.get(url))); doc.setSource(raw.getOrDefault(source, unknown)); doc.setTitle(cleanText(raw.get(title))); doc.setContent(cleanText(raw.get(content))); // 判决书才有案号法条和新闻没有用空串兜底 doc.setCaseNo(raw.getOrDefault(caseNo, )); doc.setCourt(raw.getOrDefault(court, )); doc.setPublishDate(parseDate(raw.get(publishDate))); return doc; } }setDocId 用 hashWithSource 把数据源和 URL 映射成稳定 ID重复抓取同一地址不会新增文档ES 会按 docId 做 upsert。cleanText 负责去掉 HTML 标签、全角空格和重复换行不加这一步分词质量会差很多比如「北京 市」中间的全角空格会被误判成两个词。下面是四类数据源归一化后的字段对照也是设计 mapping 前的依据字段判决书来源法条来源新闻来源ES 字段类型title文书标题法规名称新闻标题text ik_max_wordcontent裁判理由条文正文新闻正文text ik_max_wordsourcecourt_judgmentlaw_regulationnewskeywordcourt法院名发布机关空串keywordcaseNo2023京01民终123号空串空串keywordpublishDate判决日期发布日期报道日期datesource 字段用 keyword 而不是 text是因为它只做筛选不做全文匹配publishDate 用 date后续按时间倒序排序才能正常工作。如果你在课程设计里把日期设成 text后面 sort 的时候会直接报错——这是很多初学者会踩的坑。2.3 词典文件怎么加载data 目录的正确用法data/baidu_dictionary 和 data/keywords 是本项目里容易被忽略但很关键的部分。baidu_dictionary 一般是从百度词典抓下来的基础词表keywords 则是司法领域自定义关键词比如「民间借贷」「执行异议」「再审申请」这类分词器默认词库里没有的词。常见做法是在系统启动时把这两个文件读进一个 Set然后注册到分词器扩展词典里让 IK 或 HanLP 在切词时优先识别这些司法名词。下面是用 HanLP 运行时添加自定义词的示例public class DictionaryLoader { private static final SetString LEGAL_TERMS new HashSet(); public static void load(String basePath) throws IOException { // baidu_dictionary 每行一个词注释以 # 开头 Files.lines(Paths.get(basePath, data, baidu_dictionary)) .map(String::trim) .filter(line - !line.isEmpty() !line.startsWith(#)) .forEach(LEGAL_TERMS::add); // keywords 每行格式标准词\t同义词1\t同义词2 Files.lines(Paths.get(basePath, data, keywords)) .map(String::trim) .filter(line - line.contains(\t)) .map(line - line.split(\t)[0]) .forEach(LEGAL_TERMS::add); } public static void registerToHanLP() { // 将所有司法术语注入 HanLP 自定义词典 for (String term : LEGAL_TERMS) { CustomDictionary.add(term); } } }注意 load 方法里两个文件的解析规则不同。baidu_dictionary 是纯词表直接整行读入keywords 是带同义词的 TSV取第一列作为标准词其余列留给查询扩展用。registerToHanLP 在应用启动时把所有词注入 CustomDictionary确保用户搜「执行异议之诉」时不会被切成「执行/异议/之/诉」。这里有个运行时路径问题直接写相对路径 data/... 在 IDEA 里跑没问题因为工作目录是项目根目录但打成 jar 之后这个路径就不存在了。我的建议是把词典放到 src/main/resources 下用 classpath 读取这样发布时不需要额外指定外部路径。3. Elasticsearch 检索层索引映射与 BM25 排序调优3.1 为什么是 Elasticsearch 而不是 MySQL LIKE司法文书检索和普通业务搜索不一样用户输入一句话期望返回整篇相关判决而不是精确匹配某一行。MySQL 的 LIKE %关键词% 有三个问题无法处理分词、无法按相关性排序、数据量大时全表扫描性能很差。ES 的核心是倒排索引写入时把文本切成词元查询时直接命中词元对应的倒排链表。所以这个项目把 ES 作为检索主库MySQL 只当原始数据仓库两边的职责分得很清楚。维度MySQL LIKEElasticsearch分词不支持只能整串匹配ik_max_word / ik_smart相关性无只能自行排序BM25 评分百万级数据全表扫描响应不可控分片并行亚秒级同义词扩展需要自己改 SQL查询 DSL 词典运维成本低需要独立集群如果你只是几百条测试数据MySQL LIKE 够用但司法文书动辄几十万篇还要做同义词、加权、分页ES 几乎是绕不开的选择。这个项目导入 data 目录里的文书数据后检索响应基本都能稳定在几百毫秒内。3.2 索引映射把司法字段与 BM25 参数一次配好创建索引之前先设计 mapping。我见过很多课程设计直接在代码里拼一段 JSON文本字段全用 keyword导致搜「民间借贷」必须一字不差日期字段用 text排序直接失效。一个可用的司法文书索引映射应该在创建时就同时解决分词、类型和相关性参数三个问题。PUT /judicature_doc { settings: { number_of_shards: 3, number_of_replicas: 1, analysis: { analyzer: { legal_analyzer: { type: custom, tokenizer: ik_max_word, filter: [lowercase] } } }, similarity: { legal_bm25: { type: BM25, k1: 1.2, b: 0.3 } } }, mappings: { properties: { title: { type: text, analyzer: legal_analyzer, similarity: legal_bm25 }, content: { type: text, analyzer: legal_analyzer, similarity: legal_bm25 }, court: { type: keyword }, caseNo: { type: keyword }, source: { type: keyword }, publishDate: { type: date, format: yyyy-MM-dd }, importance: { type: integer } } } }number_of_shards 是分片数3 个分片适合课程设计的数据体量单机环境不要太小replicas 副本数设为 1但如果你只在本地单节点演示建议改为 0否则集群状态会一直是 yellow。title 和 content 用 legal_analyzer 做最大粒度分词召回率高court、caseNo、source 只做精确过滤不用分词。publishDate 用 date 类型并按 yyyy-MM-dd 格式化这是按时间排序的前提。importance 是自定义权重字段指导性案例、公报案例可以给高分值后面查询加权会用到。这里我加了自定义 similarity 配置 legal_bm25把 b 从默认的 0.75 调到 0.3。BM25 的 b 参数控制文档长度对评分的影响b 越大长文档被惩罚越狠。司法文书正文动辄几千字默认参数会让长文书普遍被压到后面调低之后长正文和短标题的评分更公平。k1 控制词频饱和度一般不动1.2 是 BM25 的经典取值。mapping 一旦创建字段类型不能修改只能新建索引再 reindex。所以前期字段设计和分词器选择一定要想清楚上线后改 mapping 的成本很高。3.3 查询 DSL 和 Java 客户端用 should boost 做业务排序索引建好后查询是核心部分。司法检索里有两个常见诉求标题命中比正文命中更相关带指导性案例标签的文书应该往前排。用 bool query 的 should 子句配合 boost 参数可以同时解决。Java 侧如果用 RestHighLevelClient查询代码大致是这样SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); BoolQueryBuilder bool QueryBuilders.boolQuery(); // 标题匹配权重 3.0正文匹配权重 1.0 bool.should(QueryBuilders.matchQuery(title, keyword).boost(3.0f)); bool.should(QueryBuilders.matchQuery(content, keyword).boost(1.0f)); // importance 命中直接加 5 分用于指导性案例置顶 bool.should(QueryBuilders.termQuery(importance, 10).boost(5.0f)); sourceBuilder.query(bool); sourceBuilder.from(0).size(20); sourceBuilder.sort(new FieldSortBuilder(publishDate).order(SortOrder.DESC)); SearchRequest request new SearchRequest(judicature_doc); request.source(sourceBuilder); SearchResponse response client.search(request, RequestOptions.DEFAULT);should 子句之间是 OR 关系ES 会给命中的子句分别算分再合并。boost 控制权重标题权重要给到 3因为标题高度概括案情命中标题的文书通常比正文偶发提到关键词的文书更相关。importance 字段的 termQuery 是业务规则只有被标记为指导性案例的文档才会在这个字段写入 10命中后额外加 5 分可以稳定地把这类文档顶到前排。from 和 size 是分页参数size 默认最大 10000超过这个值要改 search_after 或 scroll。sort 按 publishDate 倒序是司法检索的刚需注意 text 字段不能参与排序所以前面 mapping 里 publishDate 才必须定义成 date。如果你在查询里同时用了 should、sort 和 boostES 默认按 _score 和后续 sort 字段混合排序sort 优先级更高这一点在调参时要记住。4. 查询语义层HanLP 关键词扩展与 REST 搜索接口4.1 用 HanLP 做关键词抽取和实体识别用户输入「许霆盗窃案再审有什么结果」如果直接把整句丢给 ES分词后每个词都参与匹配噪音很大。常见做法是先做关键词抽取只保留「盗窃案」「再审」「结果」这类核心词再交给检索模块。HanLP 在 Java 生态里是用的最多的开源 NLP 工具之一它的标准和短语抽取接口可以直接复用。// 从用户 query 中抽取最重要的 5 个词 ListString keywords HanLP.extractKeyword(许霆盗窃案再审有什么结果, 5); System.out.println(keywords); // 抽取关键短语适合案件主题类查询 ListString phrases HanLP.extractPhrase(北京市高级人民法院关于民间借贷纠纷的判决, 3);extractKeyword 内部用 TextRank 算法计算词权重返回 top N 词extractPhrase 抽取关键短语适合「民间借贷纠纷」这类名词性表达。但对很短的用户 query比如「盗窃案再审」HanLP 的结果不稳定所以一般会叠加词典规则强制把 data/keywords 里的标准词保留下来两者取并集作为最终搜索词。HanLP 默认词典不包含大量法律术语所以刚才加载的自定义词典在查询阶段会发挥作用。代码层面可以这样验证// 启动时先执行 DictionaryLoader.registerToHanLP() CustomDictionary.add(执行异议之诉); ListTerm terms HanLP.segment(案外人执行异议之诉的审理范围); for (Term term : terms) { // 打印格式词语/词性例如执行异议之诉/nz System.out.println(term.word / term.nature); }不同 HanLP 版本的 API 有差异portable-1.8.x 直接调 CustomDictionary.add 即可如果是 2.x 版本字典加载方式会不一样以你引入的版本对应文档为准。校验标准很简单看「执行异议之诉」是否被切成一个完整词元如果被切开说明自定义词典没有生效。4.2 查询扩展把「打官司」扩展成「诉讼」「起诉」同一个意思判决书写「诉讼」用户搜「打官司」如果只做字面匹配这篇文书永远搜不到。data/keywords 文件的作用就在这里每行放一个同义词组运行时加载成 Map查询前把用户输入做同义词替换和 OR 扩展。public class QueryExpander { private final MapString, ListString synonymMap new HashMap(); public void load(Path keywordFile) throws IOException { for (String line : Files.readAllLines(keywordFile, StandardCharsets.UTF_8)) { String[] parts line.trim().split(\\t); if (parts.length 2) { continue; } // 整行所有词互相映射任何一个词都能找到整个同义词组 ListString synonyms Arrays.asList(parts); for (String part : parts) { synonymMap.putIfAbsent(part, synonyms); } } } public String expand(String query) { String[] words query.split(\\s); ListString expanded new ArrayList(); for (String word : words) { ListString list synonymMap.get(word); if (list null) { expanded.add(word); } else { expanded.add(( String.join( OR , list) )); } } return String.join( AND , expanded); } }load 方法把同义词组建成双向索引这样从「打官司」也能找到「诉讼」「起诉」。expand 方法把用户 query 转成布尔查询串例如「诉讼 时效」会被扩展成「(诉讼 OR 打官司 OR 起诉) AND (时效 OR 期限)」。AN D 的作用是强制不同语义组都必须出现避免召回范围失控OR 则让同义词之间任意命中即可。真正上线时不会直接拼查询字符串而是用 BoolQueryBuilder 构造 should 和 must 嵌套。这里用字符串拼接是为了快速验证扩展逻辑写单元测试时也更直观。keywords 文件的分隔符不固定有的项目用逗号有的用制表符加载逻辑里的 split 正则要跟着实际文件调整。4.3 搜索接口、权限与翻页设计检索入口一般用 Spring Boot 写 REST 接口。考虑到司法信息的敏感性接口不能裸奔常见做法是网关层做 JWT 或 OAuth2 校验服务内部再按角色过滤可见数据源。毕设项目里通常只做到 JWT 校验这里给一个最小可用的接口实现。RestController RequestMapping(/api/search) public class SearchController { private final SearchService searchService; public SearchController(SearchService searchService) { this.searchService searchService; } GetMapping(/docs) public SearchResponse search( RequestParam String q, RequestParam(defaultValue 0) int page, RequestParam(defaultValue 20) int size, RequestHeader(value Authorization, required false) String token) { if (token null || !JwtUtil.verify(token)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, 登录已过期); } return searchService.search(QueryExpander.expand(q), page, size); } }q 是用户输入page 和 size 控制分页Authorization 头携带 JWT。verify 失败直接抛 401不返回任何检索数据。SearchService 内部会执行两路检索一路走 QueryExpander 扩展后的词一路直接走 HanLP 抽取的关键词最后按评分合并结果。page 默认从 0 开始对应 ES 的 from 偏移size 限制 100防止一次拉太多数据。如果以后要支持深翻页不要继续用 from/size要改 search_after否则页码深了 ES 会报 result window 超限。JwtUtil 是简化版实际项目建议接 spring-security把认证逻辑从 Controller 里抽出去不然每个接口都要重复校验一次。5. 拿到 zip 之后导入 IDEA、构建排错与 Local History 找回从 GitHub 或毕设管理平台下载的 esJudicatureSearch-master.zip第一件事不是双击解压而是先校验包完整性。用 unzip -t 测试文件结构能省掉后续一堆莫名其妙的编译错误。这是一个 Maven 工程根目录没有 gradlew所以构建用 mvn。# 1. 校验 zip 结构避免压缩包损坏 unzip -t esJudicatureSearch-master.zip # 2. 解压到工作目录 unzip esJudicatureSearch-master.zip -d ~/workspace # 3. 进入根目录确认 pom.xml 存在 cd ~/workspace/esJudicatureSearch-master # 4. 跳过测试打包首次会下载大量依赖 mvn -DskipTests clean package-t 是 test 模式只检查压缩包 CRC 和目录结构不实际释放文件-d 指定解压目标目录。-DskipTests 跳过测试执行但保留测试类编译如果是课程设计验收建议先不要加这个参数把 src/test 里的测试完整跑一遍很多隐藏 bug 会在测试里暴露。构建时最常见的报错是「error read zip archive」或 failed to read artifact descriptor。这个工程用 Maven如果你在 IDEA 里用 Gradle 方式导入 pom.xml 工程或者本地仓库里有损坏的 lastUpdated 文件就会出现这个问题。处理办法是删除本地仓库中对应 jar 的目录强制重新下载。# 找到本地仓库中损坏的 jar 目录删除后重试 rm -rf ~/.m2/repository/org/elasticsearch/client/elasticsearch-rest-high-level-client/7.17.9 mvn -U clean compile-U 强制刷新远程仓库元数据能解决九成依赖不完整的问题。如果 IDEA 里仍然报错执行 File - Invalidate Caches - Invalidate and Restart清一次本地索引再导入。接下来说 git pull 丢失本地代码的场景。这类项目通常配合 Git 管理常见操作是本地改了一版 SearchController执行 git pull 拉远端代码IDEA 提示冲突或直接覆盖本地修改看起来「丢失」了。其实 IDEA 的 Local History 默认是开启的不依赖 Git commit 也能找回右键丢失文件所在目录选择 Local History - Show History在左侧时间线里找到修改前的版本右键 Revert 即可。这个功能比 git reflog 更细粒度它记录的是 IDEA 本地编辑快照哪怕你从来没有 commit 过也能恢复。JDK 版本也是一个高频坑。Spring Boot 2.x ES 7.x 的组合本地 JDK 建议 8 或 11。如果要用 JDK17注意 reflection 相关报错和 ES 客户端的模块访问限制。去镜像站下载 temurin jdk8 的 windows x64 zip 包时解压后要在 IDEA 的 Project Structure 里把 SDK 指到解压目录的父级路径不要只配置系统 PATH否则 IDE 内运行用的还是旧版本。演示前用 spring-boot-maven-plugin 打一个可执行 jarjava -jar 一行启动比在 IDEA 里点绿色运行按钮稳定得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价