Spaces:
Sleeping
Sleeping
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 }; | |
} | |