import { open } from 'sqlite'; import sqlite3 from 'sqlite3'; import fetch from 'node-fetch'; import { createGunzip } from 'zlib'; import { createInterface } from 'readline'; const DB_FILE = './movies_search.db'; const DATA_URL = 'https://huggingface.co/datasets/opex792/kinopoisk/resolve/main/consolidated/kinopoisk.jsonl.gz?download=true'; let db; export async function initializeDatabase() { db = await open({ filename: DB_FILE, driver: sqlite3.Database }); console.log('Подключение к SQLite (Search API) установлено.'); await db.exec('PRAGMA journal_mode = WAL;'); await db.exec('PRAGMA synchronous = NORMAL;'); await db.exec(` CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY, data TEXT NOT NULL ); `); await db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS movie_names USING fts5( movie_id UNINDEXED, name, tokenize = 'porter unicode61' ); `); console.log('Таблицы `movies` и `movie_names` (FTS5) готовы к работе.'); return db; } export async function refreshData() { console.log('Начало полной загрузки и индексации данных...'); const response = await fetch(DATA_URL); if (!response.ok) throw new Error(`Ошибка загрузки данных: ${response.statusText}`); const gunzip = createGunzip(); response.body.pipe(gunzip); const rl = createInterface({ input: gunzip, crlfDelay: Infinity }); await db.exec('BEGIN TRANSACTION;'); await db.exec('DELETE FROM movies;'); await db.exec('DELETE FROM movie_names;'); console.log('Старые данные и индекс очищены.'); const movieStmt = await db.prepare('INSERT INTO movies (id, data) VALUES (?, ?)'); const nameStmt = await db.prepare('INSERT INTO movie_names (movie_id, name) VALUES (?, ?)'); let processedCount = 0; for await (const line of rl) { if (!line.trim()) continue; const movie = JSON.parse(line); if (!movie.id) continue; await movieStmt.run(movie.id, JSON.stringify(movie)); const names = new Set(); if (movie.name) names.add(movie.name); if (movie.alternativeName) names.add(movie.alternativeName); if (movie.enName) names.add(movie.enName); movie.names?.forEach(n => names.add(n.name)); for (const name of names) { if (name) await nameStmt.run(movie.id, name); } processedCount++; if (processedCount % 10000 === 0) { console.log(`Обработано и проиндексировано ${processedCount} фильмов...`); } } await movieStmt.finalize(); await nameStmt.finalize(); await db.exec('COMMIT;'); console.log(`✅ Процесс завершен. Всего загружено и проиндексировано ${processedCount} фильмов.`); } /** * Выполняет полнотекстовый поиск по проиндексированным названиям с пагинацией. * @param {string} queryString - Поисковый запрос. * @param {number} page - Номер страницы. * @param {number} limit - Количество результатов на странице. * @returns {Promise<{docs: Array, total: number}>} Объект с результатами и общим количеством. */ export async function searchByName(queryString, page = 1, limit = 10) { const countResult = await db.get( `SELECT count(DISTINCT movie_id) as total FROM movie_names WHERE movie_names MATCH ?`, [queryString] ); const total = countResult.total || 0; if (total === 0) { return { docs: [], total: 0 }; } const offset = (page - 1) * limit; // ИСПРАВЛЕНИЕ: Убрана несовместимая функция BM25(). Используется стандартная сортировка по релевантности `rank`. const searchResults = await db.all( `SELECT DISTINCT movie_id FROM movie_names WHERE movie_names MATCH ? ORDER BY rank LIMIT ? OFFSET ?`, [queryString, limit, offset] ); if (searchResults.length === 0) { return { docs: [], total }; } const movieIds = searchResults.map(r => r.movie_id); const placeholders = movieIds.map(() => '?').join(','); const moviesData = await db.all(`SELECT data FROM movies WHERE id IN (${placeholders})`, movieIds); const movieMap = new Map(moviesData.map(m => { const movieJson = JSON.parse(m.data); return [movieJson.id, movieJson]; })); const docs = movieIds.map(id => movieMap.get(id)); return { docs, total }; }