fix: классификатор не ловил кириллицу и склонения

- regex `\b` в JS не работает с кириллицей (русские буквы не word-char),
  поэтому ни одно русское ключевое слово не давало хитов;
- exact-match не учитывал склонения: «приложение» vs «приложении».

Заменил на ручной матч с допуском в 2 символа суффикса
(shared prefix ≥ max(keyword.length - 2, 4)). Сохранён exact-match
для коротких ключей (sms/ios/crm) чтобы избежать ложных срабатываний.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhanakova 2026-05-22 13:43:14 +05:00
parent d8f3cfea24
commit 0455123d8e
2 changed files with 56 additions and 20 deletions

View File

@ -448,19 +448,37 @@ function applyLang(){
function normalize(s){ function normalize(s){
return s.toLowerCase().replace(/ё/g,"е").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean); return s.toLowerCase().replace(/ё/g,"е").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean);
} }
function escapeReg(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"); } function matchTerm(tokens, term){
const norm = normalize(term);
if (norm.length === 0) return 0;
if (norm.length > 1) {
const joined = " " + tokens.join(" ") + " ";
const needle = " " + norm.join(" ") + " ";
let cnt = 0, idx = 0;
while ((idx = joined.indexOf(needle, idx)) !== -1) { cnt++; idx += needle.length - 1; }
return cnt;
}
const n = norm[0];
let cnt = 0;
for (const tok of tokens) {
if (tok === n) { cnt++; continue; }
if (n.length < 4) continue;
const k = Math.max(n.length - 2, 4);
if (tok.length < k || n.length < k) continue;
if (tok.slice(0, k) === n.slice(0, k)) cnt++;
}
return cnt;
}
function scoreZones(text){ function scoreZones(text){
const tokenStr = normalize(text).join(" "); const tokens = normalize(text);
return state.directory.zones.map(z => { return state.directory.zones.map(z => {
let score = 0; const hits = []; let score = 0; const hits = [];
for (const k of z.keywords) { for (const k of z.keywords) {
const n = normalize(k).join(" "); const c = matchTerm(tokens, k);
const c = (tokenStr.match(new RegExp("\\b"+escapeReg(n)+"\\b","g"))||[]).length;
if (c > 0) { score += c * 1.0; hits.push(k); } if (c > 0) { score += c * 1.0; hits.push(k); }
} }
for (const sys of z.systems) { for (const sys of z.systems) {
const n = normalize(sys).join(" "); const c = matchTerm(tokens, sys);
const c = (tokenStr.match(new RegExp("\\b"+escapeReg(n)+"\\b","g"))||[]).length;
if (c > 0) { score += c * 1.5; hits.push(sys); } if (c > 0) { score += c * 1.5; hits.push(sys); }
} }
return { zoneId:z.id, score, hits }; return { zoneId:z.id, score, hits };

View File

@ -252,19 +252,37 @@ function applyLang() {
function normalize(s){ function normalize(s){
return s.toLowerCase().replace(/ё/g,"е").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean); return s.toLowerCase().replace(/ё/g,"е").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean);
} }
function escapeReg(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"); } function matchTerm(tokens, term){
const norm = normalize(term);
if (norm.length === 0) return 0;
if (norm.length > 1) {
const joined = " " + tokens.join(" ") + " ";
const needle = " " + norm.join(" ") + " ";
let cnt = 0, idx = 0;
while ((idx = joined.indexOf(needle, idx)) !== -1) { cnt++; idx += needle.length - 1; }
return cnt;
}
const n = norm[0];
let cnt = 0;
for (const tok of tokens) {
if (tok === n) { cnt++; continue; }
if (n.length < 4) continue;
const k = Math.max(n.length - 2, 4);
if (tok.length < k || n.length < k) continue;
if (tok.slice(0, k) === n.slice(0, k)) cnt++;
}
return cnt;
}
function scoreZones(text){ function scoreZones(text){
const tokenStr = normalize(text).join(" "); const tokens = normalize(text);
return state.directory.zones.map(z => { return state.directory.zones.map(z => {
let score = 0; const hits = []; let score = 0; const hits = [];
for (const k of z.keywords) { for (const k of z.keywords) {
const n = normalize(k).join(" "); const c = matchTerm(tokens, k);
const c = (tokenStr.match(new RegExp("\\b"+escapeReg(n)+"\\b","g"))||[]).length;
if (c > 0) { score += c * 1.0; hits.push(k); } if (c > 0) { score += c * 1.0; hits.push(k); }
} }
for (const sys of z.systems) { for (const sys of z.systems) {
const n = normalize(sys).join(" "); const c = matchTerm(tokens, sys);
const c = (tokenStr.match(new RegExp("\\b"+escapeReg(n)+"\\b","g"))||[]).length;
if (c > 0) { score += c * 1.5; hits.push(sys); } if (c > 0) { score += c * 1.5; hits.push(sys); }
} }
return { zoneId:z.id, score, hits }; return { zoneId:z.id, score, hits };