'use strict'; /* index-textbooks.js — наполняет textbook_chunks текстом учебников для RAG * «Спроси Квантика». Парсит HTML учебников (frontend/textbooks/) по * параграфам (.sec-h + тело секции), снимает теги, режет на куски. * * Запуск: node backend/scripts/index-textbooks.js (полная переиндексация) * Также вызывается из админки (POST /api/admin/assistant/reindex) через reindex(). * * Ограничение: учебники, рендерящие контент через JS-виджеты (напр. physics-9), * в статическом HTML текста почти не содержат — они покрываются контекстом * текущей страницы (getPageContext на клиенте), а не этим индексом. */ const path = require('path'); const fs = require('fs'); const db = require('../src/db/db'); const TEXTBOOKS_DIR = path.join(__dirname, '..', '..', 'frontend', 'textbooks'); function stripTags(html) { return String(html || '') .replace(//gi, ' ') .replace(//gi, ' ') .replace(//gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/&[a-z]+;/gi, ' ') .replace(/\s+/g, ' ') .trim(); } function chunksFromHtml(html) { const body = String(html || '').replace(//gi, ' ').replace(//gi, ' '); const out = []; const re = /]*class="[^"]*sec-h[^"]*"[^>]*>([\s\S]*?)<\/h2>([\s\S]*?)(?=]*class="[^"]*sec-h[^"]*"|<\/body|$)/gi; let m; while ((m = re.exec(body))) { const title = stripTags(m[1]).slice(0, 160); const text = stripTags(m[2]); if (text.length >= 80) out.push({ section: title, text: text.slice(0, 2000) }); } if (!out.length) { const all = stripTags(body); for (let i = 0; i < all.length && out.length < 6; i += 1500) out.push({ section: '', text: all.slice(i, i + 1500) }); if (out.length && out[0].text.length < 80) out.length = 0; } return out; } function reindex() { let books; try { books = db.prepare('SELECT slug, title, html_path FROM textbooks WHERE is_active = 1').all(); } catch (e) { return { error: 'textbooks table missing', chunks: 0 }; } // Замещаем чанки только тех книг, что реально распарсились — не трогаем // данные, наполненные headless-индексатором (JS-рендеримые учебники). const del = db.prepare('DELETE FROM textbook_chunks WHERE slug = ?'); const ins = db.prepare('INSERT INTO textbook_chunks (slug, textbook_title, section_title, text) VALUES (?, ?, ?, ?)'); let total = 0, files = 0; for (const b of books) { const fp = path.join(TEXTBOOKS_DIR, b.html_path || ''); let html; try { html = fs.readFileSync(fp, 'utf8'); } catch (e) { continue; } files++; const chunks = chunksFromHtml(html); if (!chunks.length) continue; del.run(b.slug); for (const c of chunks) { ins.run(b.slug, b.title || b.slug, c.section || '', c.text); total++; } } return { books: books.length, files, chunks: total }; } module.exports = { reindex }; if (require.main === module) { const r = reindex(); console.log('[index-textbooks]', JSON.stringify(r)); }