Skip to content

Commit 86c8c7f

Browse files
committed
fix(search): 收紧中文匹配,避免常见二字词误召回
原实现对中文查询按相邻二字切分后,只要有任意一个二字词命中就判定整篇匹配。 「完全不存在的词xyz」这类查询会因「存在」「在的」等常见词命中而返回无关文章。 现在改为: - 整词/整串命中的直接算匹配(英文词、完整中文词) - 仅有二字词命中时,要求覆盖率 >= 75% 且至少命中 3 个不同二字词 已验证:ESP32、命令行、jekyll、静态博客、自包含 等 14 组查询召回正常, 4 组无意义查询不再返回结果。
1 parent d614fe5 commit 86c8c7f

1 file changed

Lines changed: 39 additions & 17 deletions

File tree

‎assets/js/search.js‎

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,26 @@
3636
var out = [];
3737
var seen = {};
3838

39-
function push(n) {
39+
function push(n, kind) {
4040
n = String(n || '').toLowerCase().trim();
4141
if (!n || seen[n]) { return; }
4242
seen[n] = 1;
43-
out.push(n);
43+
out.push({ t: n, kind: kind || 'token' });
4444
}
4545

4646
String(query).toLowerCase().split(/[\s,,、;;]+/).forEach(function (token) {
4747
if (!token) { return; }
4848

4949
if (!CJK.test(token)) {
5050
push(token);
51-
token.split(/[^a-z0-9_\-\.\+#]+/).forEach(push);
51+
token.split(/[^a-z0-9_\-\.\+#]+/).forEach(function (p) { push(p); });
5252
return;
5353
}
5454

5555
push(token);
5656
token.replace(CJK_RUN, ' ').split(' ').filter(Boolean).forEach(function (run) {
5757
if (run.length === 1) { push(run); return; }
58-
for (var i = 0; i + 1 < run.length; i++) { push(run.substr(i, 2)); }
58+
for (var i = 0; i + 1 < run.length; i++) { push(run.substr(i, 2), 'gram'); }
5959
});
6060
});
6161

@@ -83,32 +83,54 @@
8383
var content = (doc.content || '').toLowerCase();
8484

8585
var score = 0;
86-
var matched = 0;
86+
var wholeHit = false; // 整词/整串命中(英文词、中文完整词)
87+
var gramTotal = 0; // 二字词总数
88+
var gramHit = 0; // 二字词命中数
8789

8890
needles.forEach(function (n) {
89-
var s = countOccurrences(title, n) * 12 +
90-
countOccurrences(tags, n) * 8 +
91-
countOccurrences(cats, n) * 5 +
92-
countOccurrences(excerpt, n) * 3 +
93-
countOccurrences(content, n);
94-
if (s > 0) {
95-
matched += 1;
96-
score += s;
91+
var hits = countOccurrences(title, n.t) * 12 +
92+
countOccurrences(tags, n.t) * 8 +
93+
countOccurrences(cats, n.t) * 5 +
94+
countOccurrences(excerpt, n.t) * 3 +
95+
countOccurrences(content, n.t);
96+
97+
if (n.kind === 'gram') {
98+
gramTotal += 1;
99+
}
100+
101+
if (hits > 0) {
102+
score += hits;
103+
if (n.kind !== 'gram') { wholeHit = true; }
104+
else { gramHit += 1; }
97105
}
98106
});
99107

100-
if (!matched) { return null; }
101-
score += matched * 10;
108+
// 匹配判定:
109+
// 1) 有整词命中,直接算匹配;
110+
// 2) 只有二字词时,要求覆盖率 ≥ 75% 且至少命中 3 个不同的二字词。
111+
// 只按「过半」会误召回:「完全」「存在」这类常见词在长正文里很容易凑数。
112+
if (!wholeHit) {
113+
if (!gramTotal) { return null; }
114+
var minGrams = Math.min(3, gramTotal);
115+
if (gramHit < minGrams || gramHit / gramTotal < 0.75) { return null; }
116+
}
117+
118+
score += (wholeHit ? 10 : 0) + gramHit * 4;
102119
return { doc: doc, score: score };
103120
}
104121

105122
/* ---------- 摘要与高亮 ---------- */
123+
function plainNeedles(needles) {
124+
return needles.map(function (n) { return n.t; })
125+
.filter(function (n, i, arr) { return n && arr.indexOf(n) === i; });
126+
}
127+
106128
function highlight(text, needles) {
107129
var escaped = escapeHtml(text);
108130
var lower = escaped.toLowerCase();
109131
var ranges = [];
110132

111-
needles.slice().sort(function (a, b) { return b.length - a.length; }).forEach(function (n) {
133+
plainNeedles(needles).sort(function (a, b) { return b.length - a.length; }).forEach(function (n) {
112134
var needle = escapeHtml(n).toLowerCase();
113135
if (!needle) { return; }
114136
var pos = lower.indexOf(needle);
@@ -152,7 +174,7 @@
152174

153175
var best = -1;
154176
var bestLen = 0;
155-
needles.forEach(function (n) {
177+
plainNeedles(needles).forEach(function (n) {
156178
var pos = lower.indexOf(n);
157179
if (pos !== -1 && (best === -1 || pos < best)) {
158180
best = pos;

0 commit comments

Comments
 (0)