// ask-lib.js — 共享检索库(浏览器 + Node 通用,UMD)。 // 与 src/asr.py 的 _ask_tokens / _ask_retrieve 严格对应(同一套分词与打分), // 供 web/ask.html(客户端降级检索)与 api/ask.js(服务端 RAG 检索)复用,避免两份实现走样。 // 改这里就要同步改 asr.py,并跑 scripts/check_ask_parity.py 验一致。 (function (root, factory) { if (typeof module === "object" && module.exports) module.exports = factory(); else root.AskLib = factory(); })(typeof self !== "undefined" ? self : this, function () { "use strict"; // 与 asr._ASK_STOP 一致(中文单字停用词只过滤单字 token,不影响二元组)。 var STOP = new Set([ "the","a","an","of","to","in","on","at","for","and","or","is","are","be","do","does","did", "what","whats","how","when","where","which","who","can","could","would","i","me","my","you","your", "请问","一下","什么","怎么","怎样","如何","吗","呢","的","了","有","我","要","去","想","最近","一个","可以" ]); // 类别优先级(与 asr._CAT_PRIO 一致)。 var CAT_PRIO = { "handbook":3,"appliances":3,"manual":3,"estate-rules":3,"facilities-howto":2,"booking-rules":2, "bus":2,"facilities":2,"faq":2,"news":1 }; // ASCII 词(≥2、去停用词)+ 中文单字 + 相邻二元组。须与 asr._ask_tokens 一致。 function askTokens(text) { if (!text) return []; var s = String(text).toLowerCase(); var toks = []; var m, re1 = /[a-z0-9]+/g; while ((m = re1.exec(s)) !== null) { var w = m[0]; if (w.length >= 2 && !STOP.has(w)) toks.push(w); } var re2 = /[一-鿿]+/g; while ((m = re2.exec(s)) !== null) { var run = m[0]; for (var i = 0; i < run.length; i++) { var c = run[i]; if (!STOP.has(c)) toks.push(c); if (i + 1 < run.length) toks.push(c + run[i + 1]); } } return toks; } function setOf(arr) { return new Set(arr); } function inter(a, b) { var n = 0, out = []; b.forEach(function (t) { if (a.has(t)) out.push(t); }); return out; } function chunkSets(c) { var ti = setOf(askTokens(c.title || "")); var tx = setOf(askTokens(c.text || "")); var kw = new Set(); (c.keywords || []).forEach(function (k) { askTokens(k).forEach(function (t) { kw.add(t); }); }); return { ti: ti, tx: tx, kw: kw }; } // idf 加权词重叠检索;返回 top-k chunk。与 asr._ask_retrieve 一致。 function askRetrieve(index, question, k) { k = k || 5; var chunks = (index && index.chunks) || []; var df = Object.create(null), docs = []; chunks.forEach(function (c) { var s = chunkSets(c); docs.push({ c: c, s: s }); var all = new Set(); s.ti.forEach(function (t) { all.add(t); }); s.tx.forEach(function (t) { all.add(t); }); s.kw.forEach(function (t) { all.add(t); }); all.forEach(function (t) { df[t] = (df[t] || 0) + 1; }); }); var N = Math.max(1, docs.length); function idf(t) { return Math.log((N + 1) / ((df[t] || 0) + 1)) + 1.0; } var q = setOf(askTokens(question)); if (q.size === 0) return []; var scored = []; docs.forEach(function (d) { var s = 0; inter(d.s.tx, q).forEach(function (t) { s += idf(t); }); inter(d.s.kw, q).forEach(function (t) { s += 2.0 * idf(t); }); inter(d.s.ti, q).forEach(function (t) { s += 1.5 * idf(t); }); if (s > 0) { s *= 1.0 + 0.04 * (CAT_PRIO[d.c.category] || 1); scored.push({ s: s, c: d.c }); } }); scored.sort(function (a, b) { return b.s - a.s; }); return scored.slice(0, k).map(function (x) { return x.c; }); } return { askTokens: askTokens, askRetrieve: askRetrieve, STOP: STOP, CAT_PRIO: CAT_PRIO }; });