opex792's picture
Upload database.js
7163a20 verified
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<object>, 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 };
}