# 智能匹配算法设计文档 ## 🎯 算法目标 实现检查项目名称的智能匹配,能够处理: 1. **空格差异**:"CT 上腹部平扫" vs "CT上腹部平扫" 2. **词序不同**:"CT 上腹部平扫" vs "上腹部CT平扫" 3. **同义词识别**:"头部" vs "颅脑" 4. **简写/缩写**:"MR" vs "MRI" ## 📐 算法设计 ### 核心思想 将检查项目名称分解为三个维度: 1. **模态**(Modality):CT、MR、DR等 2. **身体部位**(Body Part):头、胸、腹、四肢等 3. **扫描方式**(Scan Method):平扫、增强、造影等 然后针对每个维度分别计算相似度,最后加权汇总。 ### 算法流程 ``` 输入:"CT 上腹部平扫" ↓ 【步骤1】规范化处理 - 去除多余空格 - 提取模态:CT - 提取扫描方式:平扫 - 提取身体部位:上腹部 - 提取关键词:{C, T, 上, 腹, 部, 平扫, 上腹, 腹部} ↓ 【步骤2】维度匹配 - 模态匹配:CT == CT → 0.3分 - 扫描方式:平扫 == 平扫 → 0.2分 - 身体部位相似度计算 - 关键词重合度计算 ↓ 【步骤3】相似度计算 总分 = 0.3(模态) + 0.2(扫描) + 0.3(部位) + 0.2(关键词) = 0.3 + 0.2 + 0.3*部位相似度 + 0.2*关键词重合度 ↓ 【步骤4】阈值判断 if (总分 >= 0.7) → 匹配成功 ✅ else → 匹配失败 ❌ ``` ### 相似度计算公式 ``` 相似度 = 0.3 × 模态匹配得分 + 0.2 × 扫描方式匹配得分 + 0.3 × 身体部位相似度 + 0.2 × 关键词重合度 ``` #### 1. 模态匹配(权重0.3) ```java if (modality1.equals(modality2)) { return 0.3; // 完全匹配 } else { return -0.5; // 不匹配,大幅降低分数 } ``` **示例**: - CT vs CT → 0.3 ✅ - CT vs MR → -0.5 ❌ #### 2. 扫描方式匹配(权重0.2) ```java if (scanMethod1.equals(scanMethod2)) { return 0.2; } ``` **示例**: - 平扫 vs 平扫 → 0.2 ✅ - 平扫 vs 增强 → 0.0 - 增强 vs CTA → 0.0(不同但相关,可优化) #### 3. 身体部位相似度(权重0.3) 使用多种算法计算: **a. 精确匹配** ```java if (part1.equals(part2)) { return 1.0; } ``` **b. 同义词匹配** ```java // 同义词映射 {"头", "颅", "脑", "头部"} // 返回 0.9 {"胸", "胸部", "肺"} // 返回 0.9 {"腹", "腹部", "上腹"} // 返回 0.9 ``` **c. 包含关系** ```java if (part1.contains(part2) || part2.contains(part1)) { return 0.8; } ``` **d. 编辑距离(Levenshtein Distance)** ```java distance = "上腹" vs "上腹部" → 1 maxLen = 3 similarity = 1 - 1/3 = 0.67 ``` #### 4. 关键词重合度(权重0.2) 使用Jaccard相似系数: ```java 交集 / 并集 示例: 关键词1 = {C, T, 上, 腹, 平扫} 关键词2 = {C, T, 上, 腹部, 平} 交集 = {C, T, 上} → 3个 并集 = {C, T, 上, 腹, 平扫, 腹部, 平} → 7个 相似度 = 3/7 = 0.43 ``` ### 匹配示例 #### 示例1:空格差异 ``` 目标:"CT 上腹部平扫" 候选:"CT上腹部平扫" 规范化: 目标 → CT+上腹部+平扫 候选 → CT+上腹部+平扫 维度匹配: 模态:CT == CT → 0.3 扫描:平扫 == 平扫 → 0.2 部位:上腹部 == 上腹部 → 0.3 关键词:完全重合 → 0.2 总分:0.3 + 0.2 + 0.3 + 0.2 = 1.0 ✅ ``` #### 示例2:词序不同 ``` 目标:"CT 上腹部平扫" 候选:"上腹部CT平扫" 规范化: 目标 → {模态:CT, 部位:上腹部, 扫描:平扫} 候选 → {模态:CT, 部位:上腹部, 扫描:平扫} 维度匹配: 模态:CT == CT → 0.3 扫描:平扫 == 平扫 → 0.2 部位:上腹部 == 上腹部 → 0.3 关键词:{C,T,上,腹,平扫} vs {上,腹,C,T,平扫} → 0.2 总分:0.3 + 0.2 + 0.3 + 0.2 = 1.0 ✅ ``` #### 示例3:同义词 ``` 目标:"CT 上腹部平扫" 候选:"CT 腹部平扫" 规范化: 目标 → {模态:CT, 部位:上腹部, 扫描:平扫} 候选 → {模态:CT, 部位:腹部, 扫描:平扫} 维度匹配: 模态:CT == CT → 0.3 扫描:平扫 == 平扫 → 0.2 部位:上腹部 ⊂ 腹部(包含关系)→ 0.3 × 0.8 = 0.24 关键词:部分重合 → 0.2 × 0.7 = 0.14 总分:0.3 + 0.2 + 0.24 + 0.14 = 0.88 ✅ ``` #### 示例4:完全不匹配 ``` 目标:"CT 上腹部平扫" 候选:"MR 头部增强" 规范化: 目标 → {模态:CT, 部位:上腹部, 扫描:平扫} 候选 → {模态:MR, 部位:头部, 扫描:增强} 维度匹配: 模态:CT != MR → -0.5 扫描:平扫 != 增强 → 0.0 部位:上腹部 != 头部 → 0.0 关键词:无重合 → 0.0 总分:-0.5 + 0.0 + 0.0 + 0.0 = -0.5 ❌ ``` ## 🔧 关键技术实现 ### 1. 字符串规范化 ```java // 去除多余空格 text.replaceAll("\\s+", "") // 提取关键词(支持中文) - 单字:{C, T, 上, 腹, 部, 平, 扫} - 双字:{CT, 上腹, 腹部, 平扫} - 三字:{上腹部, 平扫} ``` ### 2. 编辑距离算法 ```java // Levenshtein Distance int distance = "上腹部" vs "上腹" → 1 int maxLen = 3 similarity = 1 - 1/3 = 0.67 ``` ### 3. Jaccard相似系数 ```java Jaccard(A, B) = |A ∩ B| / |A ∪ B| 示例: A = {CT, 上, 腹, 平扫} B = {CT, 上, 腹部, 平} |A ∩ B| = 3 // {CT, 上, 腹} |A ∪ B| = 7 // {CT, 上, 腹, 平扫, 腹部, 平} Jaccard = 3/7 = 0.43 ``` ## 📊 算法参数 ### 权重分配 | 维度 | 权重 | 说明 | |------|------|------| | 模态匹配 | 0.3 | 最重要,不同模态不匹配 | | 扫描方式 | 0.2 | 较重要,增强vs平扫区别大 | | 身体部位 | 0.3 | 最重要,不同部位不匹配 | | 关键词重合 | 0.2 | 辅助判断,提升准确率 | | **总计** | **1.0** | | ### 阈值设置 | 阈值 | 含义 | 应用场景 | |------|------|----------| | 0.9 - 1.0 | 完全匹配 | 几乎相同,可直接使用 | | 0.7 - 0.9 | 良好匹配 | 可以接受,人工确认后使用 | | 0.5 - 0.7 | 一般匹配 | 需要人工审核 | | < 0.5 | 不匹配 | 不推荐使用 | **当前阈值**:0.7(相似度 >= 0.7 才匹配成功) ## 🎯 匹配优先级(完整版) ``` 优先级1:study_info.exam_item_name 精确匹配 示例:"CT 上腹部平扫" == "CT 上腹部平扫" → 1.0 触发条件:exam_item完全一致 优先级2:DICOM.examItem 精确匹配 示例:"CT ABDOMEN..." == "CT ABDOMEN..." → 1.0 触发条件:exam_item完全一致 优先级3:study_info.exam_item_name 智能匹配 ← NEW! 示例:"CT 上腹部平扫" ~ "CT上腹部平扫" → 0.95 示例:"CT 上腹部平扫" ~ "上腹部CT平扫" → 0.92 触发条件:相似度 >= 0.7 优先级4:DICOM.examItem 智能匹配 ← NEW! 示例:使用复杂DICOM标签进行智能匹配 触发条件:相似度 >= 0.7 优先级5:bodyPart 模糊匹配 示例:使用"ABDOMEN"等部位信息 触发条件:所有上述都失败 ``` ## 🧪 测试用例 ### 测试集 | 序号 | 目标 | 候选 | 预期相似度 | 结果 | |------|------|------|-----------|------| | 1 | CT 上腹部平扫 | CT上腹部平扫 | >= 0.9 | ✅ | | 2 | CT 上腹部平扫 | 上腹部CT平扫 | >= 0.9 | ✅ | | 3 | CT 上腹部平扫 | CT腹部平扫 | >= 0.8 | ✅ | | 4 | CT 上腹部平扫 | CT胸部平扫 | < 0.5 | ❌ | | 5 | MR 颅脑平扫 | MR头部平扫 | >= 0.9 | ✅ | | 6 | CT 冠脉CTA | CT冠状动脉增强 | >= 0.8 | ✅ | | 7 | DR 胸部后前位 | DR胸部PA | >= 0.7 | ✅ | ## ⚙️ 优化建议 ### 1. 性能优化 **当前实现**:每次匹配都查询所有标准 ```java List standards = qcStandardMapper.selectList(...); ``` **优化方案**:缓存质控标准 ```java @Cacheable("qc_standards") public List getAllActiveStandards() { return qcStandardMapper.selectList(...); } ``` ### 2. 准确率优化 **增加更多同义词映射** ```java BODY_PART_SYNONYMS.put("冠状动脉", Arrays.asList("冠脉", "冠状动脉", "冠")); BODY_PART_SYNONYMS.put("脑血管", Arrays.asList("脑血管", "脑血", "颈动脉")); ``` **增加扫描方式识别** ```java SCAN_METHOD_KEYWORDS.addAll(Arrays.asList( "CTA", "MRA", "SWI", "DWI", "FLAIR", "VR", "MIP" )); ``` ### 3. 可解释性优化 **返回匹配详情** ```java public class MatchDetail { private String candidate; private double similarity; private String matchReason; // 匹配原因说明 private Map dimensionScores; // 各维度得分 } ``` ## 📝 使用示例 ### Java代码 ```java // 注入匹配器 @Resource private ExamItemMatcher examItemMatcher; // 使用匹配 String target = "CT 上腹部平扫"; List candidates = Arrays.asList( "CT上腹部平扫", "上腹部CT平扫", "CT胸部平扫" ); MatchResult result = examItemMatcher.findBestMatch(target, candidates); if (result != null && result.getSimilarity() >= 0.7) { System.out.println("匹配成功:" + result.getCandidate()); System.out.println("相似度:" + result.getSimilarity()); } ``` ### SQL查询验证 ```sql -- 查看实际的exam_item数据 SELECT exam_item_name, COUNT(*) FROM study_info WHERE exam_item_name LIKE '%上腹部%' GROUP BY exam_item_name; -- 查看质控标准配置 SELECT exam_item, standard_name FROM qc_standard WHERE modality = 'CT' AND exam_item LIKE '%上腹部%'; ``` ## 🎁 算法优势 1. **容错性强**:处理空格、词序、同义词等问题 2. **可解释性好**:每个维度都有明确的得分 3. **易于扩展**:可以添加新的维度或调整权重 4. **性能可控**:时间复杂度 O(n),n为候选标准数量 5. **准确率高**:多维度综合判断,减少误匹配 --- **算法版本**:v1.0 **创建时间**:2026-01-27 **适用场景**:质控标准自动匹配