Fix search: stem matching and skip query words absent from corpus.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -582,18 +582,18 @@ function toggleKw(ev, kw) {
|
||||
}
|
||||
|
||||
function tokenizeQuery(q) {
|
||||
// Паразитни / служебни думи — прескачат се при търсене
|
||||
// Служебни думи (винаги се прескачат)
|
||||
const STOP = new Set([
|
||||
'аз','ти','той','тя','то','ние','вие','те','ме','ми','му','го','я','ги','ни','ви','се','си',
|
||||
'съм','си','е','сме','сте','са','бях','беше','бяха','ще','да','не','ли','дали',
|
||||
'съм','е','сме','сте','са','бях','беше','бяха','ще','да','не','ли','дали',
|
||||
'и','или','но','а','нито','че','като','ако','защото','понеже',
|
||||
'за','от','на','в','във','с','със','по','към','до','при','след','преди','без','над','под','през','около','между',
|
||||
'този','тази','това','тези','онзи','онази','онова','онези','един','една','едно','едни',
|
||||
'как','кой','коя','кое','кои','къде','кога','защо','колко','какъв','каква','какво','какви',
|
||||
'инфо','информация','търся','търсим','търсите','искам','искаме','искате','моля','моляте',
|
||||
'инфо','информация','търся','търсим','търсите','искам','искаме','искате','моля',
|
||||
'покажи','покажете','кажи','кажете','дай','дайте','намери','намерете','имам','имаме','има','няма',
|
||||
'може','могат','трябва','също','още','вече','само','много','малко','добре','така','тук','там',
|
||||
'сега','тогава','пак','отново','примерно','например','относно','във','връзка','относно',
|
||||
'сега','тогава','пак','отново','примерно','например','относно','връзка',
|
||||
'please','info','information','about','for','the','a','an','i','am','looking','find','show','get',
|
||||
]);
|
||||
const tokens = [];
|
||||
@@ -601,17 +601,63 @@ function tokenizeQuery(q) {
|
||||
let m;
|
||||
while ((m = re.exec(q)) !== null) {
|
||||
let t = (m[1] || m[2] || '').toLowerCase();
|
||||
// махаме пунктуация в края/началото: "карти:," -> "карти"
|
||||
t = t.replace(/^[\s.,;:!?()\[\]{}"'«»]+|[\s.,;:!?()\[\]{}"'«»]+$/g, '');
|
||||
if (!t) continue;
|
||||
// quoted phrases keep even if short; unquoted stop-words skip
|
||||
const quoted = !!m[1];
|
||||
if (!quoted && (STOP.has(t) || t.length < 2)) continue;
|
||||
tokens.push(t);
|
||||
tokens.push({ raw: t, quoted });
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** Грубо стемване за BG: лимитни/лимитна → лимит ; карти/карта → карт */
|
||||
function stemBg(w) {
|
||||
w = String(w || '').toLowerCase();
|
||||
if (w.length < 4) return w;
|
||||
const ends = [
|
||||
'ите','ата','ята','ият','ия','ът','ят','та','те','то',
|
||||
'ове','еве','ам','ят','ях','иш','ит',
|
||||
'ни','на','но','ен','ни','и','а','о','я','у','ю','е',
|
||||
];
|
||||
for (const e of ends) {
|
||||
if (w.length > e.length + 3 && w.endsWith(e)) return w.slice(0, -e.length);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
function wordsStemmed(s) {
|
||||
return (String(s || '').toLowerCase().match(/[а-яa-z0-9]+/gi) || []).map(stemBg);
|
||||
}
|
||||
|
||||
/** Дали терминът се среща някъде в корпуса (ключови + заглавие + текст) */
|
||||
function tokenInCorpus(token) {
|
||||
const st = stemBg(token);
|
||||
if (st.length < 2) return false;
|
||||
return ALL.some(r => {
|
||||
const words = wordsStemmed(
|
||||
(r.keywords || '') + ' ' + (r.title || '') + ' ' + (r.text || '')
|
||||
);
|
||||
return words.some(w => w === st || (st.length >= 4 && (w.startsWith(st) || st.startsWith(w))));
|
||||
});
|
||||
}
|
||||
|
||||
function fieldHay(r, mode) {
|
||||
const kw = r.keywords || '';
|
||||
const title = r.title || '';
|
||||
const text = r.text || '';
|
||||
if (mode === 'keywords') return kw;
|
||||
if (mode === 'text') return text + ' ' + title;
|
||||
return kw + ' ' + title + ' ' + text;
|
||||
}
|
||||
|
||||
function rowMatchesToken(r, token, mode) {
|
||||
const hay = fieldHay(r, mode);
|
||||
if (token.quoted) return hay.toLowerCase().includes(token.raw);
|
||||
const st = stemBg(token.raw);
|
||||
const words = wordsStemmed(hay);
|
||||
return words.some(w => w === st || (st.length >= 4 && w.startsWith(st)));
|
||||
}
|
||||
|
||||
function searchMode() {
|
||||
const el = document.querySelector('input[name="search-mode"]:checked');
|
||||
return el ? el.value : 'all';
|
||||
@@ -619,24 +665,29 @@ function searchMode() {
|
||||
|
||||
function doSearch() {
|
||||
const raw = document.getElementById('search-input').value.trim();
|
||||
const q = raw.toLowerCase();
|
||||
const tokens = q ? tokenizeQuery(q) : [];
|
||||
const mode = searchMode();
|
||||
const results = tokens.length === 0 ? ALL : ALL.filter(r => {
|
||||
const kw = (r.keywords || '').toLowerCase();
|
||||
const title = (r.title || '').toLowerCase();
|
||||
const text = (r.text || '').toLowerCase();
|
||||
let hay = '';
|
||||
if (mode === 'keywords') hay = kw;
|
||||
else if (mode === 'text') hay = text + ' ' + title;
|
||||
else hay = kw + ' ' + title + ' ' + text;
|
||||
return tokens.every(t => hay.includes(t));
|
||||
});
|
||||
const skippedNote = (raw && tokens.length === 0)
|
||||
? ' · само паразитни думи — показани всички'
|
||||
: (tokens.length ? ' · филтър: ' + tokens.join(', ') : '');
|
||||
let tokens = raw ? tokenizeQuery(raw.toLowerCase()) : [];
|
||||
|
||||
// Паразити = думи, които ги няма в ключовите думи / зоната за търсене → прескачат се
|
||||
const kept = [];
|
||||
const skipped = [];
|
||||
for (const t of tokens) {
|
||||
if (t.quoted || tokenInCorpus(t.raw)) kept.push(t);
|
||||
else skipped.push(t.raw);
|
||||
}
|
||||
tokens = kept;
|
||||
|
||||
const results = tokens.length === 0 ? ALL : ALL.filter(r =>
|
||||
tokens.every(t => rowMatchesToken(r, t, mode))
|
||||
);
|
||||
|
||||
let note = '';
|
||||
if (tokens.length) note += ' · филтър: ' + tokens.map(t => t.raw).join(', ');
|
||||
if (skipped.length) note += ' · пропуснати: ' + skipped.join(', ');
|
||||
if (raw && !tokens.length) note = ' · само паразитни думи — показани всички';
|
||||
|
||||
document.getElementById('search-stats').textContent =
|
||||
results.length + ' извлечения' + skippedNote;
|
||||
results.length + ' извлечения' + note;
|
||||
document.getElementById('search-results').innerHTML = results.map(r => `
|
||||
<div class="card ${selected.has(r.code)?'selected':''}" onclick="toggleSelect('${r.code}', this)">
|
||||
<div class="card-header">
|
||||
|
||||
Reference in New Issue
Block a user