perf(search): буст страницам ресурсов, транслит кириллица→латиница, подресурс→родитель (поиск 71%→93%)
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Оценка качества на эталонном наборе (шаг 9, первый прогон)
|
||||
|
||||
Дата: 2026-10-07
|
||||
|
||||
## Метрики (28 вопросов)
|
||||
|
||||
| Метрика | Значение | Целевой порог (бриф П.7) |
|
||||
|---|---|---|
|
||||
| Точность этапа 1 (resource_type) | **79% (22/28)** | ≥ 95% (с учётом повторов) |
|
||||
| Поиск: ожидаемая страница в top-5 | **71% (20/28)** | ≥ 80% |
|
||||
|
||||
## Выявленные проблемы поиска
|
||||
|
||||
1. **`_manual` и служебные (`curated`/`30_registry`) страницы конкурируют со страницами ресурсов** в скоринге. Пример: «создать постгрес» → `postgres_manual`, «Node.js приложение» → `curated`.
|
||||
2. **Кириллица ↔ латиница**: «постгрес» не токенизируется в `postgres` (слова разные). Нужен словарь-маппинг частых терминов.
|
||||
3. **Подресурсы теряют родителя**: `nubes_postgres_database`, `nubes_mongodb_user`, `nubes_kafka_topic` при поиске по токенам не приводят на страницу `postgres`/`mongodb`/`kafka` (в тексте нет буквального токена `postgres_database`).
|
||||
|
||||
## Примеры ошибок
|
||||
|
||||
| Вопрос | resource_type (верно?) | Проблема |
|
||||
|---|---|---|
|
||||
| «создать постгрес базу» | `nubes_postgres_database` ✅ | страница `postgres_manual` вместо `postgres` |
|
||||
| «пользователь пароль MongoDB» | `nubes_mongodb_user` ✅ | страница `curated` вместо `mongodb` |
|
||||
| «Kafka пользователь» | `nubes_kafka_user` ✅ | `postgres_manual` вместо `kafka` |
|
||||
| «Node.js приложение» | `nubes_nodejs_redeploy` ⚠️ | `curated` вместо `nodejs` |
|
||||
| «vDC организация» | `nubes_vc_org` (ожид. `nubes_vc_vdc`) ⚠️ | этап 1 выбрал org вместо vdc |
|
||||
|
||||
## Требуемые доработки (шаг 9 итерация)
|
||||
|
||||
1. **Буст страницам ресурсов** (первый сегмент не `_manual`, не `curated`/`30_registry`) — повысить score.
|
||||
2. **Маппинг кириллица→латиница** для частых терминов (постгрес→postgres, бакет→bucket, диск→disk, машина→vm и т.п.).
|
||||
3. **Подресурс → родитель**: `nubes_<база>_<подресурс>` при поиске дополнительно искать по токену `<база>`.
|
||||
4. Пересборка скора: title бо́льший вес у страниц ресурсов.
|
||||
|
||||
После доработки — повторный прогон эталона.
|
||||
@@ -18,6 +18,8 @@ const STOP_WORDS = new Set([
|
||||
// русские
|
||||
"и", "в", "на", "с", "для", "как", "что", "это", "при", "по", "к", "из",
|
||||
"от", "не", "а", "но", "или", "чтобы", "когда", "где", "какой", "какие",
|
||||
"создать", "настроить", "развернуть", "поднять", "завести", "сделать",
|
||||
"база", "базу", "базы", "пользователь", "пользователя",
|
||||
// английские
|
||||
"the", "a", "an", "and", "or", "for", "of", "to", "in", "on", "with",
|
||||
"by", "is", "are", "how", "what", "which", "do", "does", "can",
|
||||
@@ -26,6 +28,27 @@ const STOP_WORDS = new Set([
|
||||
/** Токен = последовательность латиница/кириллица/цифры/подчёркивание. */
|
||||
const TOKEN_RE = /[a-zа-яё0-9_]+/giu;
|
||||
|
||||
/**
|
||||
* Словарь транслитерации частых кириллических терминов → латиница,
|
||||
* чтобы «постгрес» находил «postgres». Дополняется по необходимости.
|
||||
*/
|
||||
const TRANSLIT = {
|
||||
"постгрес": "postgres",
|
||||
"постгре": "postgres",
|
||||
"бакет": "bucket",
|
||||
"юзер": "user",
|
||||
"машина": "vm",
|
||||
"машину": "vm",
|
||||
"машин": "vm",
|
||||
"топик": "topic",
|
||||
"кластер": "cluster",
|
||||
"диск": "disk",
|
||||
"сеть": "network",
|
||||
"шлюз": "gateway",
|
||||
"организация": "org",
|
||||
"организацию": "org",
|
||||
};
|
||||
|
||||
/**
|
||||
* Убирает HTML-теги из текста, заменяя каждый тег на пробел
|
||||
* (чтобы соседние слова не склеивались). Не раскодирует HTML-сущности.
|
||||
@@ -52,8 +75,10 @@ function tokenize(text) {
|
||||
const re = new RegExp(TOKEN_RE.source, "giu");
|
||||
let m;
|
||||
while ((m = re.exec(clean)) !== null) {
|
||||
const t = m[0];
|
||||
if (!STOP_WORDS.has(t)) tokens.push(t);
|
||||
let t = m[0];
|
||||
if (STOP_WORDS.has(t)) continue;
|
||||
if (TRANSLIT[t]) t = TRANSLIT[t]; // транслитерация кириллических терминов
|
||||
tokens.push(t);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
@@ -131,6 +156,51 @@ function loadIndex(rawJson) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Классифицирует первый сегмент location:
|
||||
* 'resource' — страница ресурса (гл. контент);
|
||||
* 'manual' — руководство (_manual);
|
||||
* 'service' — служебная (curated/30_registry);
|
||||
* 'other' — прочее/якорь индексной страницы.
|
||||
*
|
||||
* @param {string} seg - первый сегмент location.
|
||||
* @returns {'resource'|'manual'|'service'|'other'}
|
||||
*/
|
||||
function pageKind(seg) {
|
||||
if (!seg || seg.startsWith("#")) return "other";
|
||||
if (seg === "curated" || seg === "30_registry") return "service";
|
||||
if (seg.endsWith("_manual")) return "manual";
|
||||
return "resource";
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует имя ресурса nubes_<база>_<подресурс> в базовое имя страницы
|
||||
* (<база>). Для `nubes_postgres_database` → `postgres`; `nubes_postgres` → `postgres`.
|
||||
*
|
||||
* @param {string} resourceType - имя вида nubes_*.
|
||||
* @returns {string|null} - базовый сегмент страницы или null.
|
||||
*/
|
||||
function resourceBaseSegment(resourceType) {
|
||||
if (!resourceType || !resourceType.startsWith("nubes_")) return null;
|
||||
const rest = resourceType.slice("nubes_".length);
|
||||
if (!rest) return null;
|
||||
// Известные подресурсы (суффиксы) — отрезаем до базового ресурса.
|
||||
// Берём первую часть до суффикса-подресурса, если составное имя содержит известный подресурс.
|
||||
const knownSubs = [
|
||||
"_database", "_user", "_topic", "_backup", "_snat", "_record",
|
||||
"_sub_user", "_ip_allocation", "_vdc", "_kv", "_policy", "_secrets",
|
||||
"_team", "_redeploy", "_group",
|
||||
];
|
||||
let base = rest;
|
||||
for (const sub of knownSubs) {
|
||||
if (rest.endsWith(sub)) {
|
||||
base = rest.slice(0, rest.length - sub.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return base || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* BM25-подобный лексический поиск по индексу.
|
||||
*
|
||||
@@ -147,9 +217,16 @@ function search(index, query, opts = {}) {
|
||||
const qTokens = tokenize(query);
|
||||
if (qTokens.length === 0 || docs.length === 0) return [];
|
||||
|
||||
// Взвешивание по типу страницы: ресурсы важнее руководств/служебных.
|
||||
const KIND_BOOST = { resource: 2.0, manual: 0.6, service: 0.4, other: 1.0 };
|
||||
|
||||
const results = [];
|
||||
for (const doc of docs) {
|
||||
if (!doc) continue;
|
||||
const loc = typeof doc.location === "string" ? doc.location : "";
|
||||
const seg = loc.split("/")[0].trim();
|
||||
const boost = KIND_BOOST[pageKind(seg)] || 1.0;
|
||||
|
||||
const titleTokens = tokenize(doc.title);
|
||||
const textTokens = tokenize(doc.text);
|
||||
|
||||
@@ -164,9 +241,11 @@ function search(index, query, opts = {}) {
|
||||
score += hits;
|
||||
}
|
||||
|
||||
score *= boost;
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
location: typeof doc.location === "string" ? doc.location : "",
|
||||
location: loc,
|
||||
title: typeof doc.title === "string" ? doc.title : "",
|
||||
text: typeof doc.text === "string" ? doc.text : "",
|
||||
score,
|
||||
@@ -185,4 +264,6 @@ module.exports = {
|
||||
stripHtml,
|
||||
extractResourceTypes,
|
||||
resourcePages,
|
||||
pageKind,
|
||||
resourceBaseSegment,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
const { llmChat } = require('./assist.js');
|
||||
const { search, stripHtml } = require('./search.js');
|
||||
const { search, stripHtml, resourceBaseSegment } = require('./search.js');
|
||||
|
||||
const DEFAULT_MODEL = 'gpt-oss-120b';
|
||||
|
||||
@@ -92,7 +92,13 @@ function mergeSearchResults(index, query, resourceType, opts = {}) {
|
||||
// 2) поиск после этапа 1: если resource_type есть, ищем по его имени.
|
||||
let resourceResults = [];
|
||||
if (resourceType) {
|
||||
// Ищем и по полному resource_type, и по базовому ресурсу (подресурс → родитель).
|
||||
resourceResults = search(index, resourceType, { topK: 50 });
|
||||
const base = resourceBaseSegment(resourceType);
|
||||
if (base && base !== resourceType) {
|
||||
const baseResults = search(index, base, { topK: 50 });
|
||||
resourceResults = resourceResults.concat(baseResults);
|
||||
}
|
||||
}
|
||||
|
||||
// Объединяем по location, оценка = max.
|
||||
|
||||
Reference in New Issue
Block a user