|
|
@@ -1,6 +1,7 @@
|
|
|
package com.zsjz.ai.module.aiclean.match;
|
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
+import com.zsjz.ai.common.cache.GlobalCache;
|
|
|
import com.zsjz.ai.module.aiclean.model.ColStat;
|
|
|
import com.zsjz.ai.module.aiclean.model.TemplateCandidate;
|
|
|
import com.zsjz.ai.module.aiclean.support.HeaderFingerprint;
|
|
|
@@ -8,7 +9,8 @@ import com.zsjz.ai.module.aiclean.support.HeaderNormalizer;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.HashMap;
|
|
|
-import java.util.HashSet;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Locale;
|
|
|
import java.util.Map;
|
|
|
@@ -20,16 +22,18 @@ import java.util.Set;
|
|
|
* <p>与既有打分的区别:既有是「交集数 + 必填数」的<b>绝对值</b>(PreDataListener:386),
|
|
|
* 模板字段越多越容易得高分,且没有阈值概念 —— 分不清「像」和「就是」。
|
|
|
*
|
|
|
- * <p>2026-10-22 起不再只看表头集合:
|
|
|
+ * <p>2026-10-22 起不再只看表头集合:表头集合 / 字段名 / 列序 / 形状 / 必填覆盖五个组件。
|
|
|
+ * 2026-10-25 起词与词的比对统一走 {@link #matchWord} 三档:
|
|
|
* <ul>
|
|
|
- * <li>表头集合:覆盖率 + Jaccard(修复重复列把分母撑大的问题);</li>
|
|
|
- * <li>字段名:候选的字段中文名/英文名是否出现在文件表头里(同义改写场景);</li>
|
|
|
- * <li>列序:同序加分,乱序只给弱分(但不会因此丢掉候选);</li>
|
|
|
- * <li>形状:{@link ColStat#shape()} 与候选字段类型是否矛盾;</li>
|
|
|
- * <li>必填覆盖:候选的 required 字段是否都能在文件表头里找到。</li>
|
|
|
+ * <li><b>EXACT</b>:同义词库({@code ai_clean_synonym},经 {@link #canonicalForScore} 归一)后相等;</li>
|
|
|
+ * <li><b>SIMILAR</b>:代码内模糊规则(后缀包含 / 等长编辑距离,见 {@link #similarWords});</li>
|
|
|
+ * <li><b>MISS</b>:不匹配。</li>
|
|
|
* </ul>
|
|
|
- * 分数落在 0~1,出口 A 还必须额外通过「确定性分数 + 必填覆盖」闸门,
|
|
|
- * 模型自评置信度不能单独把候选升进出口 A。
|
|
|
+ * 按需求(2026-10-25)SIMILAR 与 EXACT <b>同权全量参与</b>所有组件(含必填覆盖与出口 A 直通判定);
|
|
|
+ * 出口 A 之后的抽样 verify、置信度放行线等运行时安全垫不变。
|
|
|
+ *
|
|
|
+ * <p><b>指纹纪律</b>:同义词与模糊规则只进本类,{@link HeaderFingerprint} 仍用纯
|
|
|
+ * {@link HeaderNormalizer#normalize} —— 不同表头被折叠成同一个 md5 会让指纹直通错配。
|
|
|
*
|
|
|
* @author cc
|
|
|
* @since 2026/9/21
|
|
|
@@ -42,32 +46,6 @@ public final class HeaderScorer {
|
|
|
/** 弱匹配线:低于它说明表头几乎没有共同点 */
|
|
|
public static final double WEAK = 0.55;
|
|
|
|
|
|
- /** 常见同义表头(只做评分归一,不参与指纹,避免指纹碰撞) */
|
|
|
- private static final Map<String, String> SYNONYMS = Map.ofEntries(
|
|
|
- Map.entry("账户名称", "户名"),
|
|
|
- Map.entry("客户名称", "户名"),
|
|
|
- Map.entry("户名", "户名"),
|
|
|
- Map.entry("姓名", "户名"),
|
|
|
- Map.entry("发生额", "交易金额"),
|
|
|
- Map.entry("交易额", "交易金额"),
|
|
|
- Map.entry("金额", "交易金额"),
|
|
|
- Map.entry("交易日期", "日期"),
|
|
|
- Map.entry("日期", "日期"),
|
|
|
- Map.entry("交易时间", "时间"),
|
|
|
- Map.entry("时间", "时间"),
|
|
|
- Map.entry("卡号", "账号"),
|
|
|
- Map.entry("银行账号", "账号"),
|
|
|
- Map.entry("账号", "账号"),
|
|
|
- Map.entry("账户", "账号"),
|
|
|
- Map.entry("手机号", "电话"),
|
|
|
- Map.entry("手机", "电话"),
|
|
|
- Map.entry("联系电话", "电话"),
|
|
|
- Map.entry("电话", "电话"),
|
|
|
- Map.entry("身份证", "身份证号"),
|
|
|
- Map.entry("身份证号码", "身份证号"),
|
|
|
- Map.entry("证件号码", "身份证号"),
|
|
|
- Map.entry("身份证号", "身份证号"));
|
|
|
-
|
|
|
private HeaderScorer() {
|
|
|
}
|
|
|
|
|
|
@@ -82,22 +60,29 @@ public final class HeaderScorer {
|
|
|
* @return 0~1 的分数
|
|
|
*/
|
|
|
public static double score(List<String> headers, TemplateCandidate cand, List<ColStat> colStats) {
|
|
|
- List<String> fileNames = canonicalNames(headers);
|
|
|
+ ScoreContext file = ScoreContext.of(headers);
|
|
|
List<String> tplNames = canonicalNames(cand.headers());
|
|
|
- if (fileNames.isEmpty() || tplNames.isEmpty()) {
|
|
|
+ if (file.names.isEmpty() || tplNames.isEmpty()) {
|
|
|
return 0d;
|
|
|
}
|
|
|
- Set<String> fileSet = new HashSet<>(fileNames);
|
|
|
- Set<String> tplSet = new HashSet<>(tplNames);
|
|
|
- long hit = fileSet.stream().filter(tplSet::contains).count();
|
|
|
+ Set<String> tplSet = new LinkedHashSet<>(tplNames);
|
|
|
+
|
|
|
+ // 逐模板列匹配:EXACT 与 SIMILAR 同权计入命中(全量参与)
|
|
|
+ int hit = 0;
|
|
|
+ for (String t : tplSet) {
|
|
|
+ if (file.match(t).type() != WordMatchType.MISS) {
|
|
|
+ hit++;
|
|
|
+ }
|
|
|
+ }
|
|
|
double coverage = (double) hit / tplSet.size();
|
|
|
- double jaccard = HeaderFingerprint.jaccard(fileSet, tplSet);
|
|
|
+ // 模糊感知的 Jaccard:命中数(含相似命中)充当交集,分母相应收缩;封顶防病态膨胀
|
|
|
+ double jaccard = Math.min(1d, (double) hit / Math.max(1, file.set.size() + tplSet.size() - hit));
|
|
|
double header = 0.7 * Math.min(1d, coverage) + 0.3 * jaccard;
|
|
|
|
|
|
- double field = fieldCoverage(fileSet, cand.fields());
|
|
|
- double order = orderScore(fileNames, tplNames);
|
|
|
- double shape = shapeScore(headers, cand.fields(), colStats);
|
|
|
- double required = requiredCoverage(fileSet, cand.fields());
|
|
|
+ double field = fieldCoverage(file, cand.fields());
|
|
|
+ double order = orderScore(file, tplNames);
|
|
|
+ double shape = shapeScore(cand.fields(), colStats, file);
|
|
|
+ double required = requiredCoverage(file, cand.fields());
|
|
|
return 0.55 * header + 0.15 * field + 0.10 * order + 0.10 * shape + 0.10 * required;
|
|
|
}
|
|
|
|
|
|
@@ -119,30 +104,190 @@ public final class HeaderScorer {
|
|
|
|
|
|
/** 形状与字段类型没有明显矛盾;没有样本/形状为 TEXT 时视为安全 */
|
|
|
public static boolean shapeSafe(List<String> headers, TemplateCandidate cand, List<ColStat> colStats) {
|
|
|
- return shapeScore(headers, cand.fields(), colStats) >= 0.5;
|
|
|
+ return shapeScore(cand.fields(), colStats, ScoreContext.of(headers)) >= 0.5;
|
|
|
}
|
|
|
|
|
|
/** 候选里所有 required 字段是否都能在文件表头中找到 */
|
|
|
public static boolean requiredCovered(List<String> headers, TemplateCandidate cand) {
|
|
|
- Set<String> fileSet = new HashSet<>(canonicalNames(headers));
|
|
|
- return requiredCoverage(fileSet, cand.fields()) >= 1d;
|
|
|
+ ScoreContext file = ScoreContext.of(headers);
|
|
|
+ return requiredCoverage(file, cand.fields()) >= 1d;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 模板里有多少列在文件表头中出现了(用于「必填全覆盖」判断)。
|
|
|
*/
|
|
|
public static long matchedCount(List<String> headers, List<String> needCols) {
|
|
|
- Set<String> fileSet = new HashSet<>(canonicalNames(headers));
|
|
|
- return canonicalNames(needCols).stream().filter(fileSet::contains).count();
|
|
|
+ ScoreContext file = ScoreContext.of(headers);
|
|
|
+ return canonicalNames(needCols).stream().filter(w -> file.match(w).type() != WordMatchType.MISS).count();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 逐列匹配明细(排障用):形如 {@code #0 交易日期=EXACT | #1 通信类别=SIMILAR(≈通信类型) | #2 备注=MISS}。
|
|
|
+ * 每个待匹配 sheet 只应对<b>最终命中的候选</b>调用一次,不要在候选循环里调。
|
|
|
+ */
|
|
|
+ public static String matchDetail(List<String> headers, TemplateCandidate cand) {
|
|
|
+ ScoreContext file = ScoreContext.of(headers);
|
|
|
+ List<String> tplNames = canonicalNames(cand.headers());
|
|
|
+ if (file.names.isEmpty() || tplNames.isEmpty()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (int i = 0; i < tplNames.size(); i++) {
|
|
|
+ WordHit hit = file.match(tplNames.get(i));
|
|
|
+ if (sb.length() > 0) {
|
|
|
+ sb.append(" | ");
|
|
|
+ }
|
|
|
+ sb.append('#').append(i).append(' ').append(tplNames.get(i)).append('=').append(hit.type());
|
|
|
+ if (hit.type() == WordMatchType.SIMILAR) {
|
|
|
+ sb.append("(≈").append(hit.fileWord()).append(')');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 词匹配:EXACT / SIMILAR / MISS ====================
|
|
|
+
|
|
|
+ enum WordMatchType {EXACT, SIMILAR, MISS}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 一个模板词在文件表头里的匹配结果;{@code fileWord} 是命中的文件词(MISS 时为 null)。
|
|
|
+ */
|
|
|
+ record WordHit(WordMatchType type, String fileWord) {
|
|
|
+
|
|
|
+ static final WordHit MISS = new WordHit(WordMatchType.MISS, null);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 评分归一:去括号/空白/后缀 + 同义词映射。指纹仍用 {@link HeaderNormalizer#normalize},
|
|
|
+ * 相似词判定(双方均应已过 {@link #canonicalForScore} 且不相等)。三条规则:
|
|
|
+ * <ol>
|
|
|
+ * <li><b>后缀包含</b>:短词(≥2 字)是长词的尾部。中文列名的业务限定词(本方/对方/车辆/通信…)
|
|
|
+ * 在前、单位等后缀(元/万元…)在后,所以只认尾部包含:
|
|
|
+ * {@code 类型 ⊂ 车辆类型 ✓}、{@code 姓名 ⊂ 本方姓名 ✓}、
|
|
|
+ * {@code 金额 ⊂ 金额元 ✗}(单位差异必须保持不同列)。</li>
|
|
|
+ * <li><b>等长编辑距离 ≤1</b>(双方 ≥3 字):{@code 通信类型 ↔ 通信类别}。
|
|
|
+ * 只认等长替换,插入/删除会造成 金额元 ↔ 金额万元 这类单位混淆,不放行。</li>
|
|
|
+ * <li><b>等长且共同前缀 ≥2、编辑距离 ≤2</b>(双方 ≥4 字):
|
|
|
+ * {@code 车辆类型 ↔ 车辆种类}(两处尾部替换)。</li>
|
|
|
+ * </ol>
|
|
|
+ */
|
|
|
+ static boolean similarWords(String a, String b) {
|
|
|
+ int la = a.length();
|
|
|
+ int lb = b.length();
|
|
|
+ if (la == 0 || lb == 0 || a.equals(b)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String shorter = la <= lb ? a : b;
|
|
|
+ String longer = la <= lb ? b : a;
|
|
|
+ if (shorter.length() >= 2 && longer.endsWith(shorter)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (la == lb && la >= 3 && editDistanceAtMost(a, b, 1)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return la == lb && la >= 4 && commonPrefixLen(a, b) >= 2 && editDistanceAtMost(a, b, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 编辑距离是否 ≤ k(DP,表头词都很短,无需滚动数组优化) */
|
|
|
+ private static boolean editDistanceAtMost(String a, String b, int k) {
|
|
|
+ int la = a.length();
|
|
|
+ int lb = b.length();
|
|
|
+ if (Math.abs(la - lb) > k) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ int[][] dp = new int[la + 1][lb + 1];
|
|
|
+ for (int i = 0; i <= la; i++) {
|
|
|
+ dp[i][0] = i;
|
|
|
+ }
|
|
|
+ for (int j = 0; j <= lb; j++) {
|
|
|
+ dp[0][j] = j;
|
|
|
+ }
|
|
|
+ for (int i = 1; i <= la; i++) {
|
|
|
+ for (int j = 1; j <= lb; j++) {
|
|
|
+ int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
|
|
|
+ dp[i][j] = Math.min(Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + cost);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return dp[la][lb] <= k;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int commonPrefixLen(String a, String b) {
|
|
|
+ int n = Math.min(a.length(), b.length());
|
|
|
+ int i = 0;
|
|
|
+ while (i < n && a.charAt(i) == b.charAt(i)) {
|
|
|
+ i++;
|
|
|
+ }
|
|
|
+ return i;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 文件表头的匹配上下文:一次 score 调用构建一次,模板列逐个来问,memo 消除重复比对。
|
|
|
+ */
|
|
|
+ private static final class ScoreContext {
|
|
|
+
|
|
|
+ /** 归一化后的文件表头(保序,可能含重复) */
|
|
|
+ final List<String> names;
|
|
|
+ final Set<String> set;
|
|
|
+ /** 归一化词 → 首次出现的原始下标 */
|
|
|
+ final Map<String, Integer> firstIndex;
|
|
|
+ private final Map<String, WordHit> memo = new HashMap<>();
|
|
|
+
|
|
|
+ private ScoreContext(List<String> names, Map<String, Integer> firstIndex) {
|
|
|
+ this.names = names;
|
|
|
+ this.set = Set.copyOf(names);
|
|
|
+ this.firstIndex = Map.copyOf(firstIndex);
|
|
|
+ }
|
|
|
+
|
|
|
+ static ScoreContext of(List<String> rawHeaders) {
|
|
|
+ List<String> names = new ArrayList<>();
|
|
|
+ Map<String, Integer> firstIndex = new LinkedHashMap<>();
|
|
|
+ for (String h : rawHeaders == null ? List.<String>of() : rawHeaders) {
|
|
|
+ String n = canonicalForScore(h);
|
|
|
+ if (!n.isEmpty()) {
|
|
|
+ names.add(n);
|
|
|
+ firstIndex.putIfAbsent(n, names.size() - 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return new ScoreContext(names, firstIndex);
|
|
|
+ }
|
|
|
+
|
|
|
+ WordHit match(String tplWord) {
|
|
|
+ if (tplWord == null || tplWord.isEmpty() || set.isEmpty()) {
|
|
|
+ return WordHit.MISS;
|
|
|
+ }
|
|
|
+ return memo.computeIfAbsent(tplWord, w -> {
|
|
|
+ if (set.contains(w)) {
|
|
|
+ return new WordHit(WordMatchType.EXACT, w);
|
|
|
+ }
|
|
|
+ for (String fw : names) {
|
|
|
+ if (!fw.equals(w) && similarWords(w, fw)) {
|
|
|
+ return new WordHit(WordMatchType.SIMILAR, fw);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return WordHit.MISS;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 模板词对应的文件列原始下标:先精确,再按相似命中的文件词;找不到 -1 */
|
|
|
+ int indexOf(String tplWord) {
|
|
|
+ Integer exact = firstIndex.get(tplWord);
|
|
|
+ if (exact != null) {
|
|
|
+ return exact;
|
|
|
+ }
|
|
|
+ WordHit hit = match(tplWord);
|
|
|
+ return hit.type() == WordMatchType.MISS ? -1
|
|
|
+ : firstIndex.getOrDefault(hit.fileWord(), -1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 评分归一 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 评分归一:去括号/空白/后缀 + 同义词库映射。指纹仍用 {@link HeaderNormalizer#normalize},
|
|
|
* 不受同义词影响,避免不同表头被折叠成同一个 md5。
|
|
|
*/
|
|
|
static String canonicalForScore(String raw) {
|
|
|
String n = HeaderNormalizer.normalize(raw);
|
|
|
- return SYNONYMS.getOrDefault(n, n);
|
|
|
+ return GlobalCache.synonymMap().getOrDefault(n, n);
|
|
|
}
|
|
|
|
|
|
private static List<String> canonicalNames(List<String> headers) {
|
|
|
@@ -159,7 +304,7 @@ public final class HeaderScorer {
|
|
|
return out;
|
|
|
}
|
|
|
|
|
|
- private static double fieldCoverage(Set<String> fileSet, List<TemplateCandidate.FieldBrief> fields) {
|
|
|
+ private static double fieldCoverage(ScoreContext file, List<TemplateCandidate.FieldBrief> fields) {
|
|
|
if (fields == null || fields.isEmpty()) {
|
|
|
// 历史无字段模板:不给正向加分,也不重罚
|
|
|
return 0.5d;
|
|
|
@@ -171,19 +316,21 @@ public final class HeaderScorer {
|
|
|
}
|
|
|
String cn = canonicalForScore(f.cn());
|
|
|
String en = canonicalForScore(f.en());
|
|
|
- if ((!cn.isEmpty() && fileSet.contains(cn)) || (!en.isEmpty() && fileSet.contains(en))) {
|
|
|
+ boolean cnHit = !cn.isEmpty() && file.match(cn).type() != WordMatchType.MISS;
|
|
|
+ boolean enHit = !en.isEmpty() && file.match(en).type() != WordMatchType.MISS;
|
|
|
+ if (cnHit || enHit) {
|
|
|
hit++;
|
|
|
}
|
|
|
}
|
|
|
return (double) hit / fields.size();
|
|
|
}
|
|
|
|
|
|
- private static double orderScore(List<String> fileName, List<String> tplNames) {
|
|
|
+ private static double orderScore(ScoreContext file, List<String> tplNames) {
|
|
|
int last = -1;
|
|
|
int found = 0;
|
|
|
boolean ordered = true;
|
|
|
for (String t : tplNames) {
|
|
|
- int idx = fileName.indexOf(t);
|
|
|
+ int idx = file.indexOf(t);
|
|
|
if (idx < 0) {
|
|
|
ordered = false;
|
|
|
continue;
|
|
|
@@ -200,8 +347,8 @@ public final class HeaderScorer {
|
|
|
return ordered ? 1d : 0.4d;
|
|
|
}
|
|
|
|
|
|
- static double shapeScore(List<String> headers,
|
|
|
- List<TemplateCandidate.FieldBrief> fields, List<ColStat> colStats) {
|
|
|
+ static double shapeScore(List<TemplateCandidate.FieldBrief> fields, List<ColStat> colStats,
|
|
|
+ ScoreContext file) {
|
|
|
if (fields == null || fields.isEmpty() || colStats == null || colStats.isEmpty()) {
|
|
|
return 0.5d;
|
|
|
}
|
|
|
@@ -217,7 +364,7 @@ public final class HeaderScorer {
|
|
|
if (f == null) {
|
|
|
continue;
|
|
|
}
|
|
|
- int pos = indexOfHeader(headers, f);
|
|
|
+ int pos = fieldPosition(file, f);
|
|
|
if (pos < 0) {
|
|
|
continue;
|
|
|
}
|
|
|
@@ -234,19 +381,38 @@ public final class HeaderScorer {
|
|
|
return known == 0 ? 0.5d : (double) compatible / known;
|
|
|
}
|
|
|
|
|
|
- private static int indexOfHeader(List<String> headers, TemplateCandidate.FieldBrief field) {
|
|
|
+ /** 字段(cn/en 双名,先精确后相似)在文件表头中的原始下标;找不到 -1 */
|
|
|
+ private static int fieldPosition(ScoreContext file, TemplateCandidate.FieldBrief field) {
|
|
|
String cn = canonicalForScore(field.cn());
|
|
|
String en = canonicalForScore(field.en());
|
|
|
- for (int i = 0; i < headers.size(); i++) {
|
|
|
- String n = canonicalForScore(headers.get(i));
|
|
|
- if (!n.isEmpty() && ((!cn.isEmpty() && n.equals(cn)) || (!en.isEmpty() && n.equals(en)))) {
|
|
|
- return i;
|
|
|
+ if (!cn.isEmpty()) {
|
|
|
+ Integer p = file.firstIndex.get(cn);
|
|
|
+ if (p != null) {
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!en.isEmpty()) {
|
|
|
+ Integer p = file.firstIndex.get(en);
|
|
|
+ if (p != null) {
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!cn.isEmpty()) {
|
|
|
+ WordHit hit = file.match(cn);
|
|
|
+ if (hit.type() != WordMatchType.MISS) {
|
|
|
+ return file.firstIndex.getOrDefault(hit.fileWord(), -1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!en.isEmpty()) {
|
|
|
+ WordHit hit = file.match(en);
|
|
|
+ if (hit.type() != WordMatchType.MISS) {
|
|
|
+ return file.firstIndex.getOrDefault(hit.fileWord(), -1);
|
|
|
}
|
|
|
}
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
- private static double requiredCoverage(Set<String> fileSet, List<TemplateCandidate.FieldBrief> fields) {
|
|
|
+ private static double requiredCoverage(ScoreContext file, List<TemplateCandidate.FieldBrief> fields) {
|
|
|
if (fields == null || fields.isEmpty()) {
|
|
|
return 1d;
|
|
|
}
|
|
|
@@ -259,7 +425,9 @@ public final class HeaderScorer {
|
|
|
required++;
|
|
|
String cn = canonicalForScore(f.cn());
|
|
|
String en = canonicalForScore(f.en());
|
|
|
- if ((!cn.isEmpty() && fileSet.contains(cn)) || (!en.isEmpty() && fileSet.contains(en))) {
|
|
|
+ boolean cnHit = !cn.isEmpty() && file.match(cn).type() != WordMatchType.MISS;
|
|
|
+ boolean enHit = !en.isEmpty() && file.match(en).type() != WordMatchType.MISS;
|
|
|
+ if (cnHit || enHit) {
|
|
|
hit++;
|
|
|
}
|
|
|
}
|